From e3ed15ed375674729d65e4f594a8958ea91ae684 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 27 Aug 2020 20:56:56 -0400 Subject: Fixed issue where subcommand added with @as_subcommand_to decorator did not display help when called with -h/--help. 'add_help=False' no longer has to be passed to parsers used in @as_subcommand_to decorator. --- CHANGELOG.md | 9 +++++ cmd2/cmd2.py | 18 ++++----- docs/features/modular_commands.rst | 4 +- examples/modular_subcommands.py | 4 +- tests/test_argparse.py | 46 ++++++++++++++++++----- tests_isolated/test_commandset/test_commandset.py | 18 ++++----- 6 files changed, 68 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12478a29..5c65bb8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.3.8 (August 28, 2020) +* Bug Fixes + * Fixed issue where subcommand added with `@as_subcommand_to` decorator did not display help + when called with `-h/--help`. +* Enhancements + * `add_help=False` no longer has to be passed to parsers used in `@as_subcommand_to` decorator. + Only pass this if your subcommand should not have the `-h/--help` help option (as stated in + argparse documentation). + ## 1.3.7 (August 27, 2020) * Bug Fixes * Fixes an issue introduced in 1.3.0 with processing command strings containing terminator/separator diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 103508c5..d768085a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -701,9 +701,11 @@ class Cmd(cmd.Cmd): add_parser_kwargs['fromfile_prefix_chars'] = subcmd_parser.fromfile_prefix_chars add_parser_kwargs['argument_default'] = subcmd_parser.argument_default add_parser_kwargs['conflict_handler'] = subcmd_parser.conflict_handler - add_parser_kwargs['add_help'] = subcmd_parser.add_help add_parser_kwargs['allow_abbrev'] = subcmd_parser.allow_abbrev + # Set add_help to False and use whatever help option subcmd_parser already has + add_parser_kwargs['add_help'] = False + attached_parser = action.add_parser(subcommand_name, **add_parser_kwargs) setattr(attached_parser, constants.PARSER_ATTR_COMMANDSET, cmdset) break @@ -2703,8 +2705,7 @@ class Cmd(cmd.Cmd): " alias create show_log !cat \"log file.txt\"\n" " alias create save_results print_results \">\" out.txt\n") - alias_create_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=alias_create_description, - epilog=alias_create_epilog) + alias_create_parser = DEFAULT_ARGUMENT_PARSER(description=alias_create_description, epilog=alias_create_epilog) alias_create_parser.add_argument('name', help='name of this alias') alias_create_parser.add_argument('command', help='what the alias resolves to', choices_method=_get_commands_aliases_and_macros_for_completion) @@ -2748,7 +2749,7 @@ class Cmd(cmd.Cmd): alias_delete_help = "delete aliases" alias_delete_description = "Delete specified aliases or all aliases if --all is used" - alias_delete_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=alias_delete_description) + alias_delete_parser = DEFAULT_ARGUMENT_PARSER(description=alias_delete_description) alias_delete_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='alias(es) to delete', choices_method=_get_alias_completion_items, descriptive_header='Value') alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases") @@ -2776,7 +2777,7 @@ class Cmd(cmd.Cmd): "\n" "Without arguments, all aliases will be listed.") - alias_list_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=alias_list_description) + alias_list_parser = DEFAULT_ARGUMENT_PARSER(description=alias_list_description) alias_list_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='alias(es) to list', choices_method=_get_alias_completion_items, descriptive_header='Value') @@ -2854,8 +2855,7 @@ class Cmd(cmd.Cmd): " Because macros do not resolve until after hitting Enter, tab completion\n" " will only complete paths while typing a macro.") - macro_create_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=macro_create_description, - epilog=macro_create_epilog) + macro_create_parser = DEFAULT_ARGUMENT_PARSER(description=macro_create_description, epilog=macro_create_epilog) macro_create_parser.add_argument('name', help='name of this macro') macro_create_parser.add_argument('command', help='what the macro resolves to', choices_method=_get_commands_aliases_and_macros_for_completion) @@ -2945,7 +2945,7 @@ class Cmd(cmd.Cmd): # macro -> delete macro_delete_help = "delete macros" macro_delete_description = "Delete specified macros or all macros if --all is used" - macro_delete_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=macro_delete_description) + macro_delete_parser = DEFAULT_ARGUMENT_PARSER(description=macro_delete_description) macro_delete_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='macro(s) to delete', choices_method=_get_macro_completion_items, descriptive_header='Value') macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros") @@ -2973,7 +2973,7 @@ class Cmd(cmd.Cmd): "\n" "Without arguments, all macros will be listed.") - macro_list_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=macro_list_description) + macro_list_parser = DEFAULT_ARGUMENT_PARSER(description=macro_list_description) macro_list_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='macro(s) to list', choices_method=_get_macro_completion_items, descriptive_header='Value') diff --git a/docs/features/modular_commands.rst b/docs/features/modular_commands.rst index 4abeda2d..43779872 100644 --- a/docs/features/modular_commands.rst +++ b/docs/features/modular_commands.rst @@ -244,7 +244,7 @@ command and each CommandSet def do_apple(self, _: cmd2.Statement): self._cmd.poutput('Apple') - banana_parser = cmd2.Cmd2ArgumentParser(add_help=False) + banana_parser = cmd2.Cmd2ArgumentParser() banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) @cmd2.as_subcommand_to('cut', 'banana', banana_parser) @@ -261,7 +261,7 @@ command and each CommandSet def do_arugula(self, _: cmd2.Statement): self._cmd.poutput('Arugula') - bokchoy_parser = cmd2.Cmd2ArgumentParser(add_help=False) + bokchoy_parser = cmd2.Cmd2ArgumentParser() bokchoy_parser.add_argument('style', choices=['quartered', 'diced']) @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser) diff --git a/examples/modular_subcommands.py b/examples/modular_subcommands.py index 0b1f4ed3..94959349 100644 --- a/examples/modular_subcommands.py +++ b/examples/modular_subcommands.py @@ -24,7 +24,7 @@ class LoadableFruits(CommandSet): self._cmd.poutput('Apple') banana_description = "Cut a banana" - banana_parser = cmd2.Cmd2ArgumentParser(add_help=False, description=banana_description) + banana_parser = cmd2.Cmd2ArgumentParser(description=banana_description) banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help=banana_description.lower()) @@ -42,7 +42,7 @@ class LoadableVegetables(CommandSet): self._cmd.poutput('Arugula') bokchoy_description = "Cut some bokchoy" - bokchoy_parser = cmd2.Cmd2ArgumentParser(add_help=False, description=bokchoy_description) + bokchoy_parser = cmd2.Cmd2ArgumentParser(description=bokchoy_description) bokchoy_parser.add_argument('style', choices=['quartered', 'diced']) @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser, help=bokchoy_description.lower()) diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 20d05bed..7059e9d3 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -289,24 +289,32 @@ class SubcommandApp(cmd2.Cmd): func = getattr(args, 'func') func(self, args) - # Add a subcommand using as_subcommand_to decorator - has_subcmd_parser = cmd2.Cmd2ArgumentParser(description="Tests as_subcmd_to decorator") - has_subcmd_subparsers = has_subcmd_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') - has_subcmd_subparsers.required = True + # Add subcommands using as_subcommand_to decorator + has_subcmds_parser = cmd2.Cmd2ArgumentParser(description="Tests as_subcmd_to decorator") + has_subcmds_subparsers = has_subcmds_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') + has_subcmds_subparsers.required = True - @cmd2.with_argparser(has_subcmd_parser) + @cmd2.with_argparser(has_subcmds_parser) def do_test_subcmd_decorator(self, args: argparse.Namespace): handler = args.cmd2_handler.get() handler(args) - subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="The subcommand") + subcmd_parser = cmd2.Cmd2ArgumentParser(description="A subcommand") - @cmd2.as_subcommand_to('test_subcmd_decorator', 'subcmd', subcmd_parser, help='the subcommand') + @cmd2.as_subcommand_to('test_subcmd_decorator', 'subcmd', subcmd_parser, help=subcmd_parser.description.lower()) def subcmd_func(self, args: argparse.Namespace): - # Make sure printing the Namespace works. The way we originally added get_hander() - # to it resulted in a RecursionError when printing. + # Make sure printing the Namespace works. The way we originally added cmd2_hander to it resulted in a RecursionError. self.poutput(args) + helpless_subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="A subcommand with no help") + + @cmd2.as_subcommand_to('test_subcmd_decorator', 'helpless_subcmd', helpless_subcmd_parser, + help=helpless_subcmd_parser.description.lower()) + def helpless_subcmd_func(self, args: argparse.Namespace): + # Make sure vars(Namespace) works. The way we originally added cmd2_hander to it resulted in a RecursionError. + self.poutput(vars(args)) + + @pytest.fixture def subcommand_app(): app = SubcommandApp() @@ -391,9 +399,29 @@ def test_add_another_subcommand(subcommand_app): def test_subcmd_decorator(subcommand_app): + # Test subcommand that has help option out, err = run_cmd(subcommand_app, 'test_subcmd_decorator subcmd') assert out[0].startswith('Namespace(') + out, err = run_cmd(subcommand_app, 'help test_subcmd_decorator subcmd') + assert out[0] == 'Usage: test_subcmd_decorator subcmd [-h]' + + out, err = run_cmd(subcommand_app, 'test_subcmd_decorator subcmd -h') + assert out[0] == 'Usage: test_subcmd_decorator subcmd [-h]' + + # Test subcommand that has no help option + out, err = run_cmd(subcommand_app, 'test_subcmd_decorator helpless_subcmd') + assert "'subcommand': 'helpless_subcmd'" in out[0] + + out, err = run_cmd(subcommand_app, 'help test_subcmd_decorator helpless_subcmd') + assert out[0] == 'Usage: test_subcmd_decorator helpless_subcmd' + assert not err + + out, err = run_cmd(subcommand_app, 'test_subcmd_decorator helpless_subcmd -h') + assert not out + assert err[0] == 'Usage: test_subcmd_decorator [-h] SUBCOMMAND ...' + assert err[1] == 'Error: unrecognized arguments: -h' + def test_unittest_mock(): from unittest import mock diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py index 939aa5b4..5b670601 100644 --- a/tests_isolated/test_commandset/test_commandset.py +++ b/tests_isolated/test_commandset/test_commandset.py @@ -76,7 +76,7 @@ class CommandSetA(CommandSetBase): handler(args) # main -> sub - subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="Sub Command") + subcmd_parser = cmd2.Cmd2ArgumentParser(description="Sub Command") @cmd2.as_subcommand_to('main', 'sub', subcmd_parser, help="sub command") def subcmd_func(self, args: argparse.Namespace) -> None: @@ -339,7 +339,7 @@ class LoadableBase(cmd2.CommandSet): self._cmd.pwarning('This command does nothing without sub-parsers registered') self._cmd.do_help('stir') - stir_pasta_parser = cmd2.Cmd2ArgumentParser('pasta', add_help=False) + stir_pasta_parser = cmd2.Cmd2ArgumentParser() stir_pasta_parser.add_argument('--option', '-o') stir_pasta_parser.add_subparsers(title='style', help='Stir style') @@ -379,7 +379,7 @@ class LoadableFruits(cmd2.CommandSet): def do_apple(self, _: cmd2.Statement): self._cmd.poutput('Apple') - banana_parser = cmd2.Cmd2ArgumentParser(add_help=False) + banana_parser = cmd2.Cmd2ArgumentParser() banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help='Cut banana', aliases=['bananer']) @@ -393,7 +393,7 @@ class LoadablePastaStir(cmd2.CommandSet): super(LoadablePastaStir, self).__init__() self._dummy = dummy # prevents autoload - stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser('vigor', add_help=False) + stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser() stir_pasta_vigor_parser.add_argument('frequency') @cmd2.as_subcommand_to('stir pasta', 'vigorously', stir_pasta_vigor_parser) @@ -413,7 +413,7 @@ class LoadableVegetables(cmd2.CommandSet): def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: return ['quartered', 'diced'] - bokchoy_parser = cmd2.Cmd2ArgumentParser(add_help=False) + bokchoy_parser = cmd2.Cmd2ArgumentParser() bokchoy_parser.add_argument('style', completer_method=complete_style_arg) @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser) @@ -561,7 +561,7 @@ def test_nested_subcommands(command_sets_manual): super(BadNestedSubcommands, self).__init__() self._dummy = dummy # prevents autoload - stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser('vigor', add_help=False) + stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser() stir_pasta_vigor_parser.add_argument('frequency') # stir sauce doesn't exist anywhere, this should fail @@ -607,7 +607,7 @@ class AppWithSubCommands(cmd2.Cmd): self.poutput('This command does nothing without sub-parsers registered') self.do_help('cut') - banana_parser = cmd2.Cmd2ArgumentParser(add_help=False) + banana_parser = cmd2.Cmd2ArgumentParser() banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help='Cut banana', aliases=['bananer']) @@ -618,7 +618,7 @@ class AppWithSubCommands(cmd2.Cmd): def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: return ['quartered', 'diced'] - bokchoy_parser = cmd2.Cmd2ArgumentParser(add_help=False) + bokchoy_parser = cmd2.Cmd2ArgumentParser() bokchoy_parser.add_argument('style', completer_method=complete_style_arg) @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser) @@ -861,7 +861,7 @@ def test_bad_subcommand(): """Cut something""" pass - banana_parser = cmd2.Cmd2ArgumentParser(add_help=False) + banana_parser = cmd2.Cmd2ArgumentParser() banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) @cmd2.as_subcommand_to('cut', 'bad name', banana_parser, help='This should fail') -- cgit v1.2.1