Skip to content

Commit 36b0b75

Browse files
authored
Merge pull request #988 from python-cmd2/add_help
Removed need to set add_help to False for @as_subcommand_to decorator
2 parents 47f8652 + e3ed15e commit 36b0b75

File tree

6 files changed

+68
-31
lines changed

6 files changed

+68
-31
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 1.3.8 (August 28, 2020)
2+
* Bug Fixes
3+
* Fixed issue where subcommand added with `@as_subcommand_to` decorator did not display help
4+
when called with `-h/--help`.
5+
* Enhancements
6+
* `add_help=False` no longer has to be passed to parsers used in `@as_subcommand_to` decorator.
7+
Only pass this if your subcommand should not have the `-h/--help` help option (as stated in
8+
argparse documentation).
9+
110
## 1.3.7 (August 27, 2020)
211
* Bug Fixes
312
* Fixes an issue introduced in 1.3.0 with processing command strings containing terminator/separator

cmd2/cmd2.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -701,9 +701,11 @@ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) ->
701701
add_parser_kwargs['fromfile_prefix_chars'] = subcmd_parser.fromfile_prefix_chars
702702
add_parser_kwargs['argument_default'] = subcmd_parser.argument_default
703703
add_parser_kwargs['conflict_handler'] = subcmd_parser.conflict_handler
704-
add_parser_kwargs['add_help'] = subcmd_parser.add_help
705704
add_parser_kwargs['allow_abbrev'] = subcmd_parser.allow_abbrev
706705

706+
# Set add_help to False and use whatever help option subcmd_parser already has
707+
add_parser_kwargs['add_help'] = False
708+
707709
attached_parser = action.add_parser(subcommand_name, **add_parser_kwargs)
708710
setattr(attached_parser, constants.PARSER_ATTR_COMMANDSET, cmdset)
709711
break
@@ -2703,8 +2705,7 @@ def do_alias(self, args: argparse.Namespace) -> None:
27032705
" alias create show_log !cat \"log file.txt\"\n"
27042706
" alias create save_results print_results \">\" out.txt\n")
27052707

2706-
alias_create_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=alias_create_description,
2707-
epilog=alias_create_epilog)
2708+
alias_create_parser = DEFAULT_ARGUMENT_PARSER(description=alias_create_description, epilog=alias_create_epilog)
27082709
alias_create_parser.add_argument('name', help='name of this alias')
27092710
alias_create_parser.add_argument('command', help='what the alias resolves to',
27102711
choices_method=_get_commands_aliases_and_macros_for_completion)
@@ -2748,7 +2749,7 @@ def _alias_create(self, args: argparse.Namespace) -> None:
27482749
alias_delete_help = "delete aliases"
27492750
alias_delete_description = "Delete specified aliases or all aliases if --all is used"
27502751

2751-
alias_delete_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=alias_delete_description)
2752+
alias_delete_parser = DEFAULT_ARGUMENT_PARSER(description=alias_delete_description)
27522753
alias_delete_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='alias(es) to delete',
27532754
choices_method=_get_alias_completion_items, descriptive_header='Value')
27542755
alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases")
@@ -2776,7 +2777,7 @@ def _alias_delete(self, args: argparse.Namespace) -> None:
27762777
"\n"
27772778
"Without arguments, all aliases will be listed.")
27782779

2779-
alias_list_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=alias_list_description)
2780+
alias_list_parser = DEFAULT_ARGUMENT_PARSER(description=alias_list_description)
27802781
alias_list_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='alias(es) to list',
27812782
choices_method=_get_alias_completion_items, descriptive_header='Value')
27822783

@@ -2854,8 +2855,7 @@ def do_macro(self, args: argparse.Namespace) -> None:
28542855
" Because macros do not resolve until after hitting Enter, tab completion\n"
28552856
" will only complete paths while typing a macro.")
28562857

2857-
macro_create_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=macro_create_description,
2858-
epilog=macro_create_epilog)
2858+
macro_create_parser = DEFAULT_ARGUMENT_PARSER(description=macro_create_description, epilog=macro_create_epilog)
28592859
macro_create_parser.add_argument('name', help='name of this macro')
28602860
macro_create_parser.add_argument('command', help='what the macro resolves to',
28612861
choices_method=_get_commands_aliases_and_macros_for_completion)
@@ -2945,7 +2945,7 @@ def _macro_create(self, args: argparse.Namespace) -> None:
29452945
# macro -> delete
29462946
macro_delete_help = "delete macros"
29472947
macro_delete_description = "Delete specified macros or all macros if --all is used"
2948-
macro_delete_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=macro_delete_description)
2948+
macro_delete_parser = DEFAULT_ARGUMENT_PARSER(description=macro_delete_description)
29492949
macro_delete_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='macro(s) to delete',
29502950
choices_method=_get_macro_completion_items, descriptive_header='Value')
29512951
macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros")
@@ -2973,7 +2973,7 @@ def _macro_delete(self, args: argparse.Namespace) -> None:
29732973
"\n"
29742974
"Without arguments, all macros will be listed.")
29752975

2976-
macro_list_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, description=macro_list_description)
2976+
macro_list_parser = DEFAULT_ARGUMENT_PARSER(description=macro_list_description)
29772977
macro_list_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='macro(s) to list',
29782978
choices_method=_get_macro_completion_items, descriptive_header='Value')
29792979

docs/features/modular_commands.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ command and each CommandSet
244244
def do_apple(self, _: cmd2.Statement):
245245
self._cmd.poutput('Apple')
246246
247-
banana_parser = cmd2.Cmd2ArgumentParser(add_help=False)
247+
banana_parser = cmd2.Cmd2ArgumentParser()
248248
banana_parser.add_argument('direction', choices=['discs', 'lengthwise'])
249249
250250
@cmd2.as_subcommand_to('cut', 'banana', banana_parser)
@@ -261,7 +261,7 @@ command and each CommandSet
261261
def do_arugula(self, _: cmd2.Statement):
262262
self._cmd.poutput('Arugula')
263263
264-
bokchoy_parser = cmd2.Cmd2ArgumentParser(add_help=False)
264+
bokchoy_parser = cmd2.Cmd2ArgumentParser()
265265
bokchoy_parser.add_argument('style', choices=['quartered', 'diced'])
266266
267267
@cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser)

examples/modular_subcommands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def do_apple(self, _: cmd2.Statement):
2424
self._cmd.poutput('Apple')
2525

2626
banana_description = "Cut a banana"
27-
banana_parser = cmd2.Cmd2ArgumentParser(add_help=False, description=banana_description)
27+
banana_parser = cmd2.Cmd2ArgumentParser(description=banana_description)
2828
banana_parser.add_argument('direction', choices=['discs', 'lengthwise'])
2929

3030
@cmd2.as_subcommand_to('cut', 'banana', banana_parser, help=banana_description.lower())
@@ -42,7 +42,7 @@ def do_arugula(self, _: cmd2.Statement):
4242
self._cmd.poutput('Arugula')
4343

4444
bokchoy_description = "Cut some bokchoy"
45-
bokchoy_parser = cmd2.Cmd2ArgumentParser(add_help=False, description=bokchoy_description)
45+
bokchoy_parser = cmd2.Cmd2ArgumentParser(description=bokchoy_description)
4646
bokchoy_parser.add_argument('style', choices=['quartered', 'diced'])
4747

4848
@cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser, help=bokchoy_description.lower())

tests/test_argparse.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -289,24 +289,32 @@ def do_base(self, args):
289289
func = getattr(args, 'func')
290290
func(self, args)
291291

292-
# Add a subcommand using as_subcommand_to decorator
293-
has_subcmd_parser = cmd2.Cmd2ArgumentParser(description="Tests as_subcmd_to decorator")
294-
has_subcmd_subparsers = has_subcmd_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
295-
has_subcmd_subparsers.required = True
292+
# Add subcommands using as_subcommand_to decorator
293+
has_subcmds_parser = cmd2.Cmd2ArgumentParser(description="Tests as_subcmd_to decorator")
294+
has_subcmds_subparsers = has_subcmds_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
295+
has_subcmds_subparsers.required = True
296296

297-
@cmd2.with_argparser(has_subcmd_parser)
297+
@cmd2.with_argparser(has_subcmds_parser)
298298
def do_test_subcmd_decorator(self, args: argparse.Namespace):
299299
handler = args.cmd2_handler.get()
300300
handler(args)
301301

302-
subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="The subcommand")
302+
subcmd_parser = cmd2.Cmd2ArgumentParser(description="A subcommand")
303303

304-
@cmd2.as_subcommand_to('test_subcmd_decorator', 'subcmd', subcmd_parser, help='the subcommand')
304+
@cmd2.as_subcommand_to('test_subcmd_decorator', 'subcmd', subcmd_parser, help=subcmd_parser.description.lower())
305305
def subcmd_func(self, args: argparse.Namespace):
306-
# Make sure printing the Namespace works. The way we originally added get_hander()
307-
# to it resulted in a RecursionError when printing.
306+
# Make sure printing the Namespace works. The way we originally added cmd2_hander to it resulted in a RecursionError.
308307
self.poutput(args)
309308

309+
helpless_subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="A subcommand with no help")
310+
311+
@cmd2.as_subcommand_to('test_subcmd_decorator', 'helpless_subcmd', helpless_subcmd_parser,
312+
help=helpless_subcmd_parser.description.lower())
313+
def helpless_subcmd_func(self, args: argparse.Namespace):
314+
# Make sure vars(Namespace) works. The way we originally added cmd2_hander to it resulted in a RecursionError.
315+
self.poutput(vars(args))
316+
317+
310318
@pytest.fixture
311319
def subcommand_app():
312320
app = SubcommandApp()
@@ -391,9 +399,29 @@ def test_add_another_subcommand(subcommand_app):
391399

392400

393401
def test_subcmd_decorator(subcommand_app):
402+
# Test subcommand that has help option
394403
out, err = run_cmd(subcommand_app, 'test_subcmd_decorator subcmd')
395404
assert out[0].startswith('Namespace(')
396405

406+
out, err = run_cmd(subcommand_app, 'help test_subcmd_decorator subcmd')
407+
assert out[0] == 'Usage: test_subcmd_decorator subcmd [-h]'
408+
409+
out, err = run_cmd(subcommand_app, 'test_subcmd_decorator subcmd -h')
410+
assert out[0] == 'Usage: test_subcmd_decorator subcmd [-h]'
411+
412+
# Test subcommand that has no help option
413+
out, err = run_cmd(subcommand_app, 'test_subcmd_decorator helpless_subcmd')
414+
assert "'subcommand': 'helpless_subcmd'" in out[0]
415+
416+
out, err = run_cmd(subcommand_app, 'help test_subcmd_decorator helpless_subcmd')
417+
assert out[0] == 'Usage: test_subcmd_decorator helpless_subcmd'
418+
assert not err
419+
420+
out, err = run_cmd(subcommand_app, 'test_subcmd_decorator helpless_subcmd -h')
421+
assert not out
422+
assert err[0] == 'Usage: test_subcmd_decorator [-h] SUBCOMMAND ...'
423+
assert err[1] == 'Error: unrecognized arguments: -h'
424+
397425

398426
def test_unittest_mock():
399427
from unittest import mock

tests_isolated/test_commandset/test_commandset.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def do_main(self, args: argparse.Namespace) -> None:
7676
handler(args)
7777

7878
# main -> sub
79-
subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="Sub Command")
79+
subcmd_parser = cmd2.Cmd2ArgumentParser(description="Sub Command")
8080

8181
@cmd2.as_subcommand_to('main', 'sub', subcmd_parser, help="sub command")
8282
def subcmd_func(self, args: argparse.Namespace) -> None:
@@ -339,7 +339,7 @@ def do_stir(self, ns: argparse.Namespace):
339339
self._cmd.pwarning('This command does nothing without sub-parsers registered')
340340
self._cmd.do_help('stir')
341341

342-
stir_pasta_parser = cmd2.Cmd2ArgumentParser('pasta', add_help=False)
342+
stir_pasta_parser = cmd2.Cmd2ArgumentParser()
343343
stir_pasta_parser.add_argument('--option', '-o')
344344
stir_pasta_parser.add_subparsers(title='style', help='Stir style')
345345

@@ -379,7 +379,7 @@ def __init__(self, dummy):
379379
def do_apple(self, _: cmd2.Statement):
380380
self._cmd.poutput('Apple')
381381

382-
banana_parser = cmd2.Cmd2ArgumentParser(add_help=False)
382+
banana_parser = cmd2.Cmd2ArgumentParser()
383383
banana_parser.add_argument('direction', choices=['discs', 'lengthwise'])
384384

385385
@cmd2.as_subcommand_to('cut', 'banana', banana_parser, help='Cut banana', aliases=['bananer'])
@@ -393,7 +393,7 @@ def __init__(self, dummy):
393393
super(LoadablePastaStir, self).__init__()
394394
self._dummy = dummy # prevents autoload
395395

396-
stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser('vigor', add_help=False)
396+
stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser()
397397
stir_pasta_vigor_parser.add_argument('frequency')
398398

399399
@cmd2.as_subcommand_to('stir pasta', 'vigorously', stir_pasta_vigor_parser)
@@ -413,7 +413,7 @@ def do_arugula(self, _: cmd2.Statement):
413413
def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
414414
return ['quartered', 'diced']
415415

416-
bokchoy_parser = cmd2.Cmd2ArgumentParser(add_help=False)
416+
bokchoy_parser = cmd2.Cmd2ArgumentParser()
417417
bokchoy_parser.add_argument('style', completer_method=complete_style_arg)
418418

419419
@cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser)
@@ -561,7 +561,7 @@ def __init__(self, dummy):
561561
super(BadNestedSubcommands, self).__init__()
562562
self._dummy = dummy # prevents autoload
563563

564-
stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser('vigor', add_help=False)
564+
stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser()
565565
stir_pasta_vigor_parser.add_argument('frequency')
566566

567567
# stir sauce doesn't exist anywhere, this should fail
@@ -607,7 +607,7 @@ def do_cut(self, ns: argparse.Namespace):
607607
self.poutput('This command does nothing without sub-parsers registered')
608608
self.do_help('cut')
609609

610-
banana_parser = cmd2.Cmd2ArgumentParser(add_help=False)
610+
banana_parser = cmd2.Cmd2ArgumentParser()
611611
banana_parser.add_argument('direction', choices=['discs', 'lengthwise'])
612612

613613
@cmd2.as_subcommand_to('cut', 'banana', banana_parser, help='Cut banana', aliases=['bananer'])
@@ -618,7 +618,7 @@ def cut_banana(self, ns: argparse.Namespace):
618618
def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
619619
return ['quartered', 'diced']
620620

621-
bokchoy_parser = cmd2.Cmd2ArgumentParser(add_help=False)
621+
bokchoy_parser = cmd2.Cmd2ArgumentParser()
622622
bokchoy_parser.add_argument('style', completer_method=complete_style_arg)
623623

624624
@cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser)
@@ -861,7 +861,7 @@ def do_cut(self, ns: argparse.Namespace):
861861
"""Cut something"""
862862
pass
863863

864-
banana_parser = cmd2.Cmd2ArgumentParser(add_help=False)
864+
banana_parser = cmd2.Cmd2ArgumentParser()
865865
banana_parser.add_argument('direction', choices=['discs', 'lengthwise'])
866866

867867
@cmd2.as_subcommand_to('cut', 'bad name', banana_parser, help='This should fail')

0 commit comments

Comments
 (0)