Skip to content

Commit 3b3bcce

Browse files
weakcamelnejch
authored andcommitted
feat(cli): add a custom help formatter
Add a custom argparse help formatter that overrides the output format to list items vertically. The formatter is derived from argparse.HelpFormatter with minimal changes.
1 parent 4b798fc commit 3b3bcce

File tree

3 files changed

+143
-1
lines changed

3 files changed

+143
-1
lines changed

gitlab/_formatter.py

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

gitlab/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from requests.structures import CaseInsensitiveDict
2929

30+
import gitlab._formatter
3031
import gitlab.config
3132
from gitlab.base import RESTObject
3233

@@ -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._formatter.VerticalHelpFormatter, # type: ignore
113115
allow_abbrev=False,
114116
)
115117
parser.add_argument("--version", help="Display the version.", action="store_true")

gitlab/v4/cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,10 @@ def extend_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
377377

378378
for cls in sorted(classes, key=operator.attrgetter("__name__")):
379379
arg_name = cli.cls_to_gitlab_resource(cls)
380-
object_group = subparsers.add_parser(arg_name)
380+
object_group = subparsers.add_parser(
381+
arg_name,
382+
formatter_class=gitlab._formatter.VerticalHelpFormatter, # type: ignore
383+
)
381384

382385
object_subparsers = object_group.add_subparsers(
383386
title="action",

0 commit comments

Comments
 (0)