diff options
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | cmd2/cmd2.py | 66 | ||||
-rwxr-xr-x | examples/subcommands.py | 4 | ||||
-rwxr-xr-x | examples/tab_autocompletion.py | 2 | ||||
-rw-r--r-- | tests/test_argparse.py | 2 | ||||
-rw-r--r-- | tests/test_argparse_completer.py | 2 | ||||
-rw-r--r-- | tests/test_argparse_custom.py | 32 | ||||
-rwxr-xr-x | tests/test_completion.py | 2 |
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 |