summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2020-08-27 20:56:56 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2020-08-27 23:17:05 -0400
commite3ed15ed375674729d65e4f594a8958ea91ae684 (patch)
treea05524c8f7fc3c56776e32648b5808252c7e6d51
parent47f8652fa467b2d140b1097b3167f968b0188451 (diff)
downloadcmd2-git-e3ed15ed375674729d65e4f594a8958ea91ae684.tar.gz
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.
-rw-r--r--CHANGELOG.md9
-rw-r--r--cmd2/cmd2.py18
-rw-r--r--docs/features/modular_commands.rst4
-rw-r--r--examples/modular_subcommands.py4
-rw-r--r--tests/test_argparse.py46
-rw-r--r--tests_isolated/test_commandset/test_commandset.py18
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')