summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md3
-rw-r--r--cmd2/cmd2.py66
-rwxr-xr-xexamples/subcommands.py4
-rwxr-xr-xexamples/tab_autocompletion.py2
-rw-r--r--tests/test_argparse.py2
-rw-r--r--tests/test_argparse_completer.py2
-rw-r--r--tests/test_argparse_custom.py32
-rwxr-xr-xtests/test_completion.py2
8 files changed, 69 insertions, 44 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45b726eb..93bede62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@
showed no record of the run_script command in history.
* Made it easier for developers to override `edit` command by having `do_history` no longer call `do_edit`. This
also removes the need to exclude `edit` command from history list.
+ * It is no longer necessary to set the `prog` attribute of an argparser with subcommands. cmd2 now automatically
+ sets the prog value of it and all its subparsers so that all usage statements contain the top level command name
+ and not sys.argv[0].
## 0.9.19 (October 14, 2019)
* Bug Fixes
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 1af5e932..0a7097ba 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -177,14 +177,38 @@ def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) ->
return arg_decorator
-def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
+# noinspection PyProtectedMember
+def set_parser_prog(parser: argparse.ArgumentParser, prog: str):
+ """
+ Recursively set prog attribute of a parser and all of its subparsers so that the root command
+ is a command name and not sys.argv[0].
+ :param parser: the parser being edited
+ :param prog: value for the current parsers prog attribute
+ """
+ # Set the prog value for this parser
+ parser.prog = prog
+
+ # Set the prog value for the parser's subcommands
+ for action in parser._actions:
+ if isinstance(action, argparse._SubParsersAction):
+
+ # Set the prog value for each subcommand
+ for sub_cmd, sub_cmd_parser in action.choices.items():
+ sub_cmd_prog = parser.prog + ' ' + sub_cmd
+ set_parser_prog(sub_cmd_parser, sub_cmd_prog)
+
+ # We can break since argparse only allows 1 group of subcommands per level
+ break
+
+
+def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *,
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
preserve_quotes: bool = False) -> \
Callable[[argparse.Namespace, List], Optional[bool]]:
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given
instance of argparse.ArgumentParser, but also returning unknown args as a list.
- :param argparser: unique instance of ArgumentParser
+ :param parser: unique instance of ArgumentParser
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
state data that affects parsing.
@@ -209,27 +233,26 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
namespace = ns_provider(cmd2_app)
try:
- args, unknown = argparser.parse_known_args(parsed_arglist, namespace)
+ args, unknown = parser.parse_known_args(parsed_arglist, namespace)
except SystemExit:
return
else:
setattr(args, '__statement__', statement)
return func(cmd2_app, args, unknown)
- # argparser defaults the program name to sys.argv[0]
- # we want it to be the name of our command
+ # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
- argparser.prog = command_name
+ set_parser_prog(parser, command_name)
# If the description has not been set, then use the method docstring if one exists
- if argparser.description is None and func.__doc__:
- argparser.description = func.__doc__
+ if parser.description is None and func.__doc__:
+ parser.description = func.__doc__
# Set the command's help text as argparser.description (which can be None)
- cmd_wrapper.__doc__ = argparser.description
+ cmd_wrapper.__doc__ = parser.description
# Set some custom attributes for this command
- setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, argparser)
+ setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, parser)
setattr(cmd_wrapper, CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)
return cmd_wrapper
@@ -238,13 +261,13 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
return arg_decorator
-def with_argparser(argparser: argparse.ArgumentParser, *,
+def with_argparser(parser: argparse.ArgumentParser, *,
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
preserve_quotes: bool = False) -> Callable[[argparse.Namespace], Optional[bool]]:
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
with the given instance of argparse.ArgumentParser.
- :param argparser: unique instance of ArgumentParser
+ :param parser: unique instance of ArgumentParser
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
state data that affects parsing.
@@ -268,27 +291,26 @@ def with_argparser(argparser: argparse.ArgumentParser, *,
namespace = ns_provider(cmd2_app)
try:
- args = argparser.parse_args(parsed_arglist, namespace)
+ args = parser.parse_args(parsed_arglist, namespace)
except SystemExit:
return
else:
setattr(args, '__statement__', statement)
return func(cmd2_app, args)
- # argparser defaults the program name to sys.argv[0]
- # we want it to be the name of our command
+ # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
- argparser.prog = command_name
+ set_parser_prog(parser, command_name)
# If the description has not been set, then use the method docstring if one exists
- if argparser.description is None and func.__doc__:
- argparser.description = func.__doc__
+ if parser.description is None and func.__doc__:
+ parser.description = func.__doc__
# Set the command's help text as argparser.description (which can be None)
- cmd_wrapper.__doc__ = argparser.description
+ cmd_wrapper.__doc__ = parser.description
# Set some custom attributes for this command
- setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, argparser)
+ setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, parser)
setattr(cmd_wrapper, CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)
return cmd_wrapper
@@ -2396,7 +2418,7 @@ class Cmd(cmd.Cmd):
"An alias is a command that enables replacement of a word by another string.")
alias_epilog = ("See also:\n"
" macro")
- alias_parser = Cmd2ArgumentParser(description=alias_description, epilog=alias_epilog, prog='alias')
+ alias_parser = Cmd2ArgumentParser(description=alias_description, epilog=alias_epilog)
# Add subcommands to alias
alias_subparsers = alias_parser.add_subparsers(dest='subcommand')
@@ -2573,7 +2595,7 @@ class Cmd(cmd.Cmd):
"A macro is similar to an alias, but it can contain argument placeholders.")
macro_epilog = ("See also:\n"
" alias")
- macro_parser = Cmd2ArgumentParser(description=macro_description, epilog=macro_epilog, prog='macro')
+ macro_parser = Cmd2ArgumentParser(description=macro_description, epilog=macro_epilog)
# Add subcommands to macro
macro_subparsers = macro_parser.add_subparsers(dest='subcommand')
diff --git a/examples/subcommands.py b/examples/subcommands.py
index 0b228e79..4f569b1e 100755
--- a/examples/subcommands.py
+++ b/examples/subcommands.py
@@ -12,7 +12,7 @@ import cmd2
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
# create the top-level parser for the base command
-base_parser = argparse.ArgumentParser(prog='base')
+base_parser = argparse.ArgumentParser()
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
# create the parser for the "foo" subcommand
@@ -38,7 +38,7 @@ sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport', cho
# create the top-level parser for the alternate command
# The alternate command doesn't provide its own help flag
-base2_parser = argparse.ArgumentParser(prog='alternate', add_help=False)
+base2_parser = argparse.ArgumentParser(add_help=False)
base2_subparsers = base2_parser.add_subparsers(title='subcommands', help='subcommand help')
# create the parser for the "foo" subcommand
diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py
index 2142fe2e..3561f968 100755
--- a/examples/tab_autocompletion.py
+++ b/examples/tab_autocompletion.py
@@ -204,7 +204,7 @@ class TabCompleteExample(cmd2.Cmd):
'\n '.join(ep_list)))
print()
- video_parser = Cmd2ArgumentParser(prog='media')
+ video_parser = Cmd2ArgumentParser()
video_types_subparsers = video_parser.add_subparsers(title='Media Types', dest='type')
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index e5fa6dd0..21ec17e8 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -207,7 +207,7 @@ class SubcommandApp(cmd2.Cmd):
self.poutput('((%s))' % args.z)
# create the top-level parser for the base command
- base_parser = argparse.ArgumentParser(prog='base')
+ base_parser = argparse.ArgumentParser()
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
# create the parser for the "foo" subcommand
diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py
index e54e49c2..b904a6ac 100644
--- a/tests/test_argparse_completer.py
+++ b/tests/test_argparse_completer.py
@@ -63,7 +63,7 @@ class AutoCompleteTester(cmd2.Cmd):
# Begin code related to help and command name completion
############################################################################################################
# Top level parser for music command
- music_parser = Cmd2ArgumentParser(description='Manage music', prog='music')
+ music_parser = Cmd2ArgumentParser(description='Manage music')
# Add subcommands to music
music_subparsers = music_parser.add_subparsers()
diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py
index 99afe2dd..65cbc8da 100644
--- a/tests/test_argparse_custom.py
+++ b/tests/test_argparse_custom.py
@@ -49,7 +49,7 @@ def fake_func():
({'completer_function': fake_func, 'completer_method': fake_func}, False),
])
def test_apcustom_choices_callable_count(kwargs, is_valid):
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
try:
parser.add_argument('name', **kwargs)
assert is_valid
@@ -66,7 +66,7 @@ def test_apcustom_choices_callable_count(kwargs, is_valid):
])
def test_apcustom_no_choices_callables_alongside_choices(kwargs):
with pytest.raises(TypeError) as excinfo:
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
parser.add_argument('name', choices=['my', 'choices', 'list'], **kwargs)
assert 'None of the following parameters can be used alongside a choices parameter' in str(excinfo.value)
@@ -79,7 +79,7 @@ def test_apcustom_no_choices_callables_alongside_choices(kwargs):
])
def test_apcustom_no_choices_callables_when_nargs_is_0(kwargs):
with pytest.raises(TypeError) as excinfo:
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
parser.add_argument('name', action='store_true', **kwargs)
assert 'None of the following parameters can be used on an action that takes no arguments' in str(excinfo.value)
@@ -126,40 +126,40 @@ def test_apcustom_nargs_range_validation(cust_app):
])
def test_apcustom_narg_invalid_tuples(nargs_tuple):
with pytest.raises(ValueError) as excinfo:
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
parser.add_argument('invalid_tuple', nargs=nargs_tuple)
assert 'Ranged values for nargs must be a tuple of 1 or 2 integers' in str(excinfo.value)
def test_apcustom_narg_tuple_order():
with pytest.raises(ValueError) as excinfo:
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
parser.add_argument('invalid_tuple', nargs=(2, 1))
assert 'Invalid nargs range. The first value must be less than the second' in str(excinfo.value)
def test_apcustom_narg_tuple_negative():
with pytest.raises(ValueError) as excinfo:
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
parser.add_argument('invalid_tuple', nargs=(-1, 1))
assert 'Negative numbers are invalid for nargs range' in str(excinfo.value)
# noinspection PyUnresolvedReferences
def test_apcustom_narg_tuple_zero_base():
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(0,))
assert arg.nargs == argparse.ZERO_OR_MORE
assert arg.nargs_range is None
assert "[arg [...]]" in parser.format_help()
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(0, 1))
assert arg.nargs == argparse.OPTIONAL
assert arg.nargs_range is None
assert "[arg]" in parser.format_help()
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(0, 3))
assert arg.nargs == argparse.ZERO_OR_MORE
assert arg.nargs_range == (0, 3)
@@ -168,13 +168,13 @@ def test_apcustom_narg_tuple_zero_base():
# noinspection PyUnresolvedReferences
def test_apcustom_narg_tuple_one_base():
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(1,))
assert arg.nargs == argparse.ONE_OR_MORE
assert arg.nargs_range is None
assert "arg [...]" in parser.format_help()
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(1, 5))
assert arg.nargs == argparse.ONE_OR_MORE
assert arg.nargs_range == (1, 5)
@@ -185,13 +185,13 @@ def test_apcustom_narg_tuple_one_base():
def test_apcustom_narg_tuple_other_ranges():
# Test range with no upper bound on max
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(2,))
assert arg.nargs == argparse.ONE_OR_MORE
assert arg.nargs_range == (2, INFINITY)
# Test finite range
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(2, 5))
assert arg.nargs == argparse.ONE_OR_MORE
assert arg.nargs_range == (2, 5)
@@ -202,13 +202,13 @@ def test_apcustom_print_message(capsys):
test_message = 'The test message'
# Specify the file
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
parser._print_message(test_message, file=sys.stdout)
out, err = capsys.readouterr()
assert test_message in out
# Make sure file defaults to sys.stderr
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
parser._print_message(test_message)
out, err = capsys.readouterr()
assert test_message in err
@@ -239,6 +239,6 @@ def test_generate_range_error():
def test_apcustom_required_options():
# Make sure a 'required arguments' section shows when a flag is marked required
- parser = Cmd2ArgumentParser(prog='test')
+ parser = Cmd2ArgumentParser()
parser.add_argument('--required_flag', required=True)
assert 'required arguments' in parser.format_help()
diff --git a/tests/test_completion.py b/tests/test_completion.py
index c7d9bd21..3b26b044 100755
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -1078,7 +1078,7 @@ class SubcommandsWithUnknownExample(cmd2.Cmd):
self.poutput('Sport is {}'.format(args.sport))
# create the top-level parser for the base command
- base_parser = argparse.ArgumentParser(prog='base')
+ base_parser = argparse.ArgumentParser()
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
# create the parser for the "foo" subcommand