Skip to content

Commit 245dc33

Browse files
authored
Merge pull request #796 from python-cmd2/set_prog
Recursively set parser.prog
2 parents b1873c3 + 5a58199 commit 245dc33

File tree

8 files changed

+69
-44
lines changed

8 files changed

+69
-44
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
showed no record of the run_script command in history.
88
* Made it easier for developers to override `edit` command by having `do_history` no longer call `do_edit`. This
99
also removes the need to exclude `edit` command from history list.
10+
* It is no longer necessary to set the `prog` attribute of an argparser with subcommands. cmd2 now automatically
11+
sets the prog value of it and all its subparsers so that all usage statements contain the top level command name
12+
and not sys.argv[0].
1013

1114
## 0.9.19 (October 14, 2019)
1215
* Bug Fixes

cmd2/cmd2.py

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,38 @@ def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
177177
return arg_decorator
178178

179179

180-
def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
180+
# noinspection PyProtectedMember
181+
def set_parser_prog(parser: argparse.ArgumentParser, prog: str):
182+
"""
183+
Recursively set prog attribute of a parser and all of its subparsers so that the root command
184+
is a command name and not sys.argv[0].
185+
:param parser: the parser being edited
186+
:param prog: value for the current parsers prog attribute
187+
"""
188+
# Set the prog value for this parser
189+
parser.prog = prog
190+
191+
# Set the prog value for the parser's subcommands
192+
for action in parser._actions:
193+
if isinstance(action, argparse._SubParsersAction):
194+
195+
# Set the prog value for each subcommand
196+
for sub_cmd, sub_cmd_parser in action.choices.items():
197+
sub_cmd_prog = parser.prog + ' ' + sub_cmd
198+
set_parser_prog(sub_cmd_parser, sub_cmd_prog)
199+
200+
# We can break since argparse only allows 1 group of subcommands per level
201+
break
202+
203+
204+
def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *,
181205
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
182206
preserve_quotes: bool = False) -> \
183207
Callable[[argparse.Namespace, List], Optional[bool]]:
184208
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given
185209
instance of argparse.ArgumentParser, but also returning unknown args as a list.
186210
187-
:param argparser: unique instance of ArgumentParser
211+
:param parser: unique instance of ArgumentParser
188212
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
189213
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
190214
state data that affects parsing.
@@ -209,27 +233,26 @@ def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
209233
namespace = ns_provider(cmd2_app)
210234

211235
try:
212-
args, unknown = argparser.parse_known_args(parsed_arglist, namespace)
236+
args, unknown = parser.parse_known_args(parsed_arglist, namespace)
213237
except SystemExit:
214238
return
215239
else:
216240
setattr(args, '__statement__', statement)
217241
return func(cmd2_app, args, unknown)
218242

219-
# argparser defaults the program name to sys.argv[0]
220-
# we want it to be the name of our command
243+
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
221244
command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
222-
argparser.prog = command_name
245+
set_parser_prog(parser, command_name)
223246

224247
# If the description has not been set, then use the method docstring if one exists
225-
if argparser.description is None and func.__doc__:
226-
argparser.description = func.__doc__
248+
if parser.description is None and func.__doc__:
249+
parser.description = func.__doc__
227250

228251
# Set the command's help text as argparser.description (which can be None)
229-
cmd_wrapper.__doc__ = argparser.description
252+
cmd_wrapper.__doc__ = parser.description
230253

231254
# Set some custom attributes for this command
232-
setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, argparser)
255+
setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, parser)
233256
setattr(cmd_wrapper, CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)
234257

235258
return cmd_wrapper
@@ -238,13 +261,13 @@ def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
238261
return arg_decorator
239262

240263

241-
def with_argparser(argparser: argparse.ArgumentParser, *,
264+
def with_argparser(parser: argparse.ArgumentParser, *,
242265
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
243266
preserve_quotes: bool = False) -> Callable[[argparse.Namespace], Optional[bool]]:
244267
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
245268
with the given instance of argparse.ArgumentParser.
246269
247-
:param argparser: unique instance of ArgumentParser
270+
:param parser: unique instance of ArgumentParser
248271
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
249272
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
250273
state data that affects parsing.
@@ -268,27 +291,26 @@ def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
268291
namespace = ns_provider(cmd2_app)
269292

270293
try:
271-
args = argparser.parse_args(parsed_arglist, namespace)
294+
args = parser.parse_args(parsed_arglist, namespace)
272295
except SystemExit:
273296
return
274297
else:
275298
setattr(args, '__statement__', statement)
276299
return func(cmd2_app, args)
277300

278-
# argparser defaults the program name to sys.argv[0]
279-
# we want it to be the name of our command
301+
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
280302
command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
281-
argparser.prog = command_name
303+
set_parser_prog(parser, command_name)
282304

283305
# If the description has not been set, then use the method docstring if one exists
284-
if argparser.description is None and func.__doc__:
285-
argparser.description = func.__doc__
306+
if parser.description is None and func.__doc__:
307+
parser.description = func.__doc__
286308

287309
# Set the command's help text as argparser.description (which can be None)
288-
cmd_wrapper.__doc__ = argparser.description
310+
cmd_wrapper.__doc__ = parser.description
289311

290312
# Set some custom attributes for this command
291-
setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, argparser)
313+
setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, parser)
292314
setattr(cmd_wrapper, CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)
293315

294316
return cmd_wrapper
@@ -2396,7 +2418,7 @@ def _alias_list(self, args: argparse.Namespace) -> None:
23962418
"An alias is a command that enables replacement of a word by another string.")
23972419
alias_epilog = ("See also:\n"
23982420
" macro")
2399-
alias_parser = Cmd2ArgumentParser(description=alias_description, epilog=alias_epilog, prog='alias')
2421+
alias_parser = Cmd2ArgumentParser(description=alias_description, epilog=alias_epilog)
24002422

24012423
# Add subcommands to alias
24022424
alias_subparsers = alias_parser.add_subparsers(dest='subcommand')
@@ -2573,7 +2595,7 @@ def _macro_list(self, args: argparse.Namespace) -> None:
25732595
"A macro is similar to an alias, but it can contain argument placeholders.")
25742596
macro_epilog = ("See also:\n"
25752597
" alias")
2576-
macro_parser = Cmd2ArgumentParser(description=macro_description, epilog=macro_epilog, prog='macro')
2598+
macro_parser = Cmd2ArgumentParser(description=macro_description, epilog=macro_epilog)
25772599

25782600
# Add subcommands to macro
25792601
macro_subparsers = macro_parser.add_subparsers(dest='subcommand')

examples/subcommands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
1313

1414
# create the top-level parser for the base command
15-
base_parser = argparse.ArgumentParser(prog='base')
15+
base_parser = argparse.ArgumentParser()
1616
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
1717

1818
# create the parser for the "foo" subcommand
@@ -38,7 +38,7 @@
3838

3939
# create the top-level parser for the alternate command
4040
# The alternate command doesn't provide its own help flag
41-
base2_parser = argparse.ArgumentParser(prog='alternate', add_help=False)
41+
base2_parser = argparse.ArgumentParser(add_help=False)
4242
base2_subparsers = base2_parser.add_subparsers(title='subcommands', help='subcommand help')
4343

4444
# create the parser for the "foo" subcommand

examples/tab_autocompletion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ def _do_vid_shows(self, args) -> None:
204204
'\n '.join(ep_list)))
205205
print()
206206

207-
video_parser = Cmd2ArgumentParser(prog='media')
207+
video_parser = Cmd2ArgumentParser()
208208

209209
video_types_subparsers = video_parser.add_subparsers(title='Media Types', dest='type')
210210

tests/test_argparse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ def base_bar(self, args):
207207
self.poutput('((%s))' % args.z)
208208

209209
# create the top-level parser for the base command
210-
base_parser = argparse.ArgumentParser(prog='base')
210+
base_parser = argparse.ArgumentParser()
211211
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
212212

213213
# create the parser for the "foo" subcommand

tests/test_argparse_completer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def __init__(self, *args, **kwargs):
6363
# Begin code related to help and command name completion
6464
############################################################################################################
6565
# Top level parser for music command
66-
music_parser = Cmd2ArgumentParser(description='Manage music', prog='music')
66+
music_parser = Cmd2ArgumentParser(description='Manage music')
6767

6868
# Add subcommands to music
6969
music_subparsers = music_parser.add_subparsers()

tests/test_argparse_custom.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def fake_func():
4949
({'completer_function': fake_func, 'completer_method': fake_func}, False),
5050
])
5151
def test_apcustom_choices_callable_count(kwargs, is_valid):
52-
parser = Cmd2ArgumentParser(prog='test')
52+
parser = Cmd2ArgumentParser()
5353
try:
5454
parser.add_argument('name', **kwargs)
5555
assert is_valid
@@ -66,7 +66,7 @@ def test_apcustom_choices_callable_count(kwargs, is_valid):
6666
])
6767
def test_apcustom_no_choices_callables_alongside_choices(kwargs):
6868
with pytest.raises(TypeError) as excinfo:
69-
parser = Cmd2ArgumentParser(prog='test')
69+
parser = Cmd2ArgumentParser()
7070
parser.add_argument('name', choices=['my', 'choices', 'list'], **kwargs)
7171
assert 'None of the following parameters can be used alongside a choices parameter' in str(excinfo.value)
7272

@@ -79,7 +79,7 @@ def test_apcustom_no_choices_callables_alongside_choices(kwargs):
7979
])
8080
def test_apcustom_no_choices_callables_when_nargs_is_0(kwargs):
8181
with pytest.raises(TypeError) as excinfo:
82-
parser = Cmd2ArgumentParser(prog='test')
82+
parser = Cmd2ArgumentParser()
8383
parser.add_argument('name', action='store_true', **kwargs)
8484
assert 'None of the following parameters can be used on an action that takes no arguments' in str(excinfo.value)
8585

@@ -126,40 +126,40 @@ def test_apcustom_nargs_range_validation(cust_app):
126126
])
127127
def test_apcustom_narg_invalid_tuples(nargs_tuple):
128128
with pytest.raises(ValueError) as excinfo:
129-
parser = Cmd2ArgumentParser(prog='test')
129+
parser = Cmd2ArgumentParser()
130130
parser.add_argument('invalid_tuple', nargs=nargs_tuple)
131131
assert 'Ranged values for nargs must be a tuple of 1 or 2 integers' in str(excinfo.value)
132132

133133

134134
def test_apcustom_narg_tuple_order():
135135
with pytest.raises(ValueError) as excinfo:
136-
parser = Cmd2ArgumentParser(prog='test')
136+
parser = Cmd2ArgumentParser()
137137
parser.add_argument('invalid_tuple', nargs=(2, 1))
138138
assert 'Invalid nargs range. The first value must be less than the second' in str(excinfo.value)
139139

140140

141141
def test_apcustom_narg_tuple_negative():
142142
with pytest.raises(ValueError) as excinfo:
143-
parser = Cmd2ArgumentParser(prog='test')
143+
parser = Cmd2ArgumentParser()
144144
parser.add_argument('invalid_tuple', nargs=(-1, 1))
145145
assert 'Negative numbers are invalid for nargs range' in str(excinfo.value)
146146

147147

148148
# noinspection PyUnresolvedReferences
149149
def test_apcustom_narg_tuple_zero_base():
150-
parser = Cmd2ArgumentParser(prog='test')
150+
parser = Cmd2ArgumentParser()
151151
arg = parser.add_argument('arg', nargs=(0,))
152152
assert arg.nargs == argparse.ZERO_OR_MORE
153153
assert arg.nargs_range is None
154154
assert "[arg [...]]" in parser.format_help()
155155

156-
parser = Cmd2ArgumentParser(prog='test')
156+
parser = Cmd2ArgumentParser()
157157
arg = parser.add_argument('arg', nargs=(0, 1))
158158
assert arg.nargs == argparse.OPTIONAL
159159
assert arg.nargs_range is None
160160
assert "[arg]" in parser.format_help()
161161

162-
parser = Cmd2ArgumentParser(prog='test')
162+
parser = Cmd2ArgumentParser()
163163
arg = parser.add_argument('arg', nargs=(0, 3))
164164
assert arg.nargs == argparse.ZERO_OR_MORE
165165
assert arg.nargs_range == (0, 3)
@@ -168,13 +168,13 @@ def test_apcustom_narg_tuple_zero_base():
168168

169169
# noinspection PyUnresolvedReferences
170170
def test_apcustom_narg_tuple_one_base():
171-
parser = Cmd2ArgumentParser(prog='test')
171+
parser = Cmd2ArgumentParser()
172172
arg = parser.add_argument('arg', nargs=(1,))
173173
assert arg.nargs == argparse.ONE_OR_MORE
174174
assert arg.nargs_range is None
175175
assert "arg [...]" in parser.format_help()
176176

177-
parser = Cmd2ArgumentParser(prog='test')
177+
parser = Cmd2ArgumentParser()
178178
arg = parser.add_argument('arg', nargs=(1, 5))
179179
assert arg.nargs == argparse.ONE_OR_MORE
180180
assert arg.nargs_range == (1, 5)
@@ -185,13 +185,13 @@ def test_apcustom_narg_tuple_one_base():
185185
def test_apcustom_narg_tuple_other_ranges():
186186

187187
# Test range with no upper bound on max
188-
parser = Cmd2ArgumentParser(prog='test')
188+
parser = Cmd2ArgumentParser()
189189
arg = parser.add_argument('arg', nargs=(2,))
190190
assert arg.nargs == argparse.ONE_OR_MORE
191191
assert arg.nargs_range == (2, INFINITY)
192192

193193
# Test finite range
194-
parser = Cmd2ArgumentParser(prog='test')
194+
parser = Cmd2ArgumentParser()
195195
arg = parser.add_argument('arg', nargs=(2, 5))
196196
assert arg.nargs == argparse.ONE_OR_MORE
197197
assert arg.nargs_range == (2, 5)
@@ -202,13 +202,13 @@ def test_apcustom_print_message(capsys):
202202
test_message = 'The test message'
203203

204204
# Specify the file
205-
parser = Cmd2ArgumentParser(prog='test')
205+
parser = Cmd2ArgumentParser()
206206
parser._print_message(test_message, file=sys.stdout)
207207
out, err = capsys.readouterr()
208208
assert test_message in out
209209

210210
# Make sure file defaults to sys.stderr
211-
parser = Cmd2ArgumentParser(prog='test')
211+
parser = Cmd2ArgumentParser()
212212
parser._print_message(test_message)
213213
out, err = capsys.readouterr()
214214
assert test_message in err
@@ -239,6 +239,6 @@ def test_generate_range_error():
239239

240240
def test_apcustom_required_options():
241241
# Make sure a 'required arguments' section shows when a flag is marked required
242-
parser = Cmd2ArgumentParser(prog='test')
242+
parser = Cmd2ArgumentParser()
243243
parser.add_argument('--required_flag', required=True)
244244
assert 'required arguments' in parser.format_help()

tests/test_completion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1078,7 +1078,7 @@ def base_sport(self, args):
10781078
self.poutput('Sport is {}'.format(args.sport))
10791079

10801080
# create the top-level parser for the base command
1081-
base_parser = argparse.ArgumentParser(prog='base')
1081+
base_parser = argparse.ArgumentParser()
10821082
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
10831083

10841084
# create the parser for the "foo" subcommand

0 commit comments

Comments
 (0)