Skip to content

Commit c6156e2

Browse files
committed
add a custom help formatter
add a custom argparse help formatter which overrides output format for long (over 10 elements) list of choices formatter is derived from HelpFormatter with minimal changes
1 parent ca3b438 commit c6156e2

File tree

2 files changed

+144
-0
lines changed

2 files changed

+144
-0
lines changed

gitlab/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from requests.structures import CaseInsensitiveDict
2929

3030
import gitlab.config
31+
import gitlab.cli_internal
3132
from gitlab.base import RESTObject
3233

3334
# This regex is based on:
@@ -110,6 +111,7 @@ def _get_base_parser(add_help: bool = True) -> argparse.ArgumentParser:
110111
parser = argparse.ArgumentParser(
111112
add_help=add_help,
112113
description="GitLab API Command Line Interface",
114+
formatter_class=gitlab.cli_internal.LongChoiceListHelpFormatter,
113115
allow_abbrev=False,
114116
)
115117
parser.add_argument("--version", help="Display the version.", action="store_true")

gitlab/cli_internal.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Derived from Python argparse standard module with necessary modifications.
5+
#
6+
# Original author of argparse: Steven J. Bethard <steven.bethard@gmail.com>.
7+
# New maintainer of argparse as of 29 August 2019: Raymond Hettinger <raymond.hettinger@gmail.com>
8+
#
9+
10+
11+
import argparse
12+
import re as _re
13+
14+
15+
class LongChoiceListHelpFormatter(argparse.HelpFormatter):
16+
def add_usage(self, usage, actions, groups, prefix=None):
17+
if prefix is None:
18+
prefix = 'Usage: '
19+
return super(LongChoiceListHelpFormatter, self).add_usage(
20+
usage, actions, groups, prefix)
21+
22+
def _metavar_formatter(self, action, default_metavar):
23+
if action.metavar is not None:
24+
result = action.metavar
25+
elif action.choices is not None:
26+
choice_strs = [str(choice) for choice in action.choices]
27+
if len(action.choices) > 10:
28+
indentation = self._indent_increment * ' '
29+
sep = ',\n' + (indentation * 2)
30+
result = '{\n' + 2 * indentation + sep.join(choice_strs) + '\n' + (self._indent_increment* ' ') + '}'
31+
else:
32+
sep = ','
33+
result = '{' + sep.join(choice_strs) + '}'
34+
else:
35+
result = default_metavar
36+
37+
def format(tuple_size):
38+
if isinstance(result, tuple):
39+
return result
40+
else:
41+
return (result, ) * tuple_size
42+
return format
43+
44+
# This method was copied almost as-is from parent class,
45+
# the only change required was disabling some validation which doesn't make
46+
# sense with multi-line choice lists
47+
def _format_usage(self, usage, actions, groups, prefix):
48+
if prefix is None:
49+
prefix = _('usage: ')
50+
51+
# if usage is specified, use that
52+
if usage is not None:
53+
usage = usage % dict(prog=self._prog)
54+
55+
# if no optionals or positionals are available, usage is just prog
56+
elif usage is None and not actions:
57+
usage = '%(prog)s' % dict(prog=self._prog)
58+
59+
# if optionals and positionals are available, calculate usage
60+
elif usage is None:
61+
prog = '%(prog)s' % dict(prog=self._prog)
62+
63+
# split optionals from positionals
64+
optionals = []
65+
positionals = []
66+
for action in actions:
67+
if action.option_strings:
68+
optionals.append(action)
69+
else:
70+
positionals.append(action)
71+
72+
# build full usage string
73+
format = self._format_actions_usage
74+
action_usage = format(optionals + positionals, groups)
75+
usage = ' '.join([s for s in [prog, action_usage] if s])
76+
77+
# wrap the usage parts if it's too long
78+
text_width = self._width - self._current_indent
79+
if len(prefix) + len(usage) > text_width:
80+
81+
# break usage into wrappable parts
82+
part_regexp = (
83+
r'\(.*?\)+(?=\s|$)|'
84+
r'\[.*?\]+(?=\s|$)|'
85+
r'\S+'
86+
)
87+
opt_usage = format(optionals, groups)
88+
pos_usage = format(positionals, groups)
89+
opt_parts = _re.findall(part_regexp, opt_usage)
90+
pos_parts = _re.findall(part_regexp, pos_usage)
91+
# Disabled the validation due to possible multiline output
92+
#assert ' '.join(opt_parts) == opt_usage
93+
#assert ' '.join(pos_parts) == pos_usage
94+
95+
# helper for wrapping lines
96+
def get_lines(parts, indent, prefix=None):
97+
lines = []
98+
line = []
99+
if prefix is not None:
100+
line_len = len(prefix) - 1
101+
else:
102+
line_len = len(indent) - 1
103+
for part in parts:
104+
if line_len + 1 + len(part) > text_width and line:
105+
lines.append(indent + ' '.join(line))
106+
line = []
107+
line_len = len(indent) - 1
108+
line.append(part)
109+
line_len += len(part) + 1
110+
if line:
111+
lines.append(indent + ' '.join(line))
112+
if prefix is not None:
113+
lines[0] = lines[0][len(indent):]
114+
return lines
115+
116+
# if prog is short, follow it with optionals or positionals
117+
if len(prefix) + len(prog) <= 0.75 * text_width:
118+
indent = ' ' * (len(prefix) + len(prog) + 1)
119+
if opt_parts:
120+
lines = get_lines([prog] + opt_parts, indent, prefix)
121+
lines.extend(get_lines(pos_parts, indent))
122+
elif pos_parts:
123+
lines = get_lines([prog] + pos_parts, indent, prefix)
124+
else:
125+
lines = [prog]
126+
127+
# if prog is long, put it on its own line
128+
else:
129+
indent = ' ' * len(prefix)
130+
parts = opt_parts + pos_parts
131+
lines = get_lines(parts, indent)
132+
if len(lines) > 1:
133+
lines = []
134+
lines.extend(get_lines(opt_parts, indent))
135+
lines.extend(get_lines(pos_parts, indent))
136+
lines = [prog] + lines
137+
138+
# join lines into usage
139+
usage = '\n'.join(lines)
140+
141+
# prefix with 'usage:'
142+
return '%s%s\n\n' % (prefix, usage)

0 commit comments

Comments
 (0)