From 2049496815bc0e46db2371f9173c0cef7496e706 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 7 Aug 2020 14:53:56 -0400 Subject: [PATCH 1/6] Fixed prog value of subcommands added with as_subcommand_to() decorator. Fixed missing settings in subcommand parsers created with as_subcommand_to() decorator. --- cmd2/decorators.py | 39 ++++++++++++++----- .../test_commandset/test_commandset.py | 4 +- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 7fee32955..9981ca944 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -326,7 +326,7 @@ def as_subcommand_to(command: str, subcommand: str, parser: argparse.ArgumentParser, *, - help_text: Optional[str] = None, + help: Optional[str] = None, aliases: Iterable[str] = None) -> Callable[[argparse.Namespace], Optional[bool]]: """ Tag this method as a subcommand to an existing argparse decorated command. @@ -334,28 +334,49 @@ def as_subcommand_to(command: str, :param command: Command Name. Space-delimited subcommands may optionally be specified :param subcommand: Subcommand name :param parser: argparse Parser for this subcommand - :param help_text: Help message for this subcommand - :param aliases: Alternative names for this subcommand + :param help: Help message for this subcommand which displays in the list of subcommands of the command we are adding to. + This is passed as the help argument to ArgumentParser.add_subparser(). + :param aliases: Alternative names for this subcommand. This is passed as the alias argument to + ArgumentParser.add_subparser(). :return: Wrapper function that can receive an argparse.Namespace """ def arg_decorator(func: Callable): - _set_parser_prog(parser, subcommand) + _set_parser_prog(parser, command + ' ' + subcommand) # If the description has not been set, then use the method docstring if one exists if parser.description is None and func.__doc__: parser.description = func.__doc__ - parser.set_defaults(func=func) - # Set some custom attributes for this command setattr(func, constants.SUBCMD_ATTR_COMMAND, command) setattr(func, constants.CMD_ATTR_ARGPARSER, parser) setattr(func, constants.SUBCMD_ATTR_NAME, subcommand) - parser_args = {} - if help_text is not None: - parser_args['help'] = help_text + + # Dictionary of arguments which will be passed to ArgumentParser.add_subparser() + parser_args = dict() + + # parser is set as the parent to the one being created by ArgumentParser.add_parser(). + # argparse only copies actions (arguments) from a parent and not the following settings. + # To retain these settings, we will copy them from parser and pass them as ArgumentParser + # constructor arguments to add_parser(). + parser_args['prog'] = parser.prog + parser_args['usage'] = parser.usage + parser_args['description'] = parser.description + parser_args['epilog'] = parser.epilog + parser_args['formatter_class'] = parser.formatter_class + parser_args['prefix_chars'] = parser.prefix_chars + parser_args['fromfile_prefix_chars'] = parser.fromfile_prefix_chars + parser_args['argument_default'] = parser.argument_default + parser_args['conflict_handler'] = parser.conflict_handler + parser_args['add_help'] = parser.add_help + parser_args['allow_abbrev'] = parser.allow_abbrev + + # Add remaining arguments specific to add_parser() + if help is not None: + parser_args['help'] = help if aliases is not None: parser_args['aliases'] = aliases[:] + setattr(func, constants.SUBCMD_ATTR_PARSER_ARGS, parser_args) return func diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py index 506b309d2..43947071a 100644 --- a/tests_isolated/test_commandset/test_commandset.py +++ b/tests_isolated/test_commandset/test_commandset.py @@ -330,7 +330,7 @@ def do_apple(self, cmd: cmd2.Cmd, _: cmd2.Statement): banana_parser = cmd2.Cmd2ArgumentParser(add_help=False) banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) - @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help_text='Cut banana', aliases=['bananer']) + @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help='Cut banana', aliases=['bananer']) def cut_banana(self, cmd: cmd2.Cmd, ns: argparse.Namespace): """Cut banana""" cmd.poutput('cutting banana: ' + ns.direction) @@ -545,7 +545,7 @@ def do_cut(self, ns: argparse.Namespace): banana_parser = cmd2.Cmd2ArgumentParser(add_help=False) banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) - @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help_text='Cut banana', aliases=['bananer']) + @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help='Cut banana', aliases=['bananer']) def cut_banana(self, ns: argparse.Namespace): """Cut banana""" self.poutput('cutting banana: ' + ns.direction) From 3ada4bd53925ca20fdeb16fa971814637dc42e4d Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 7 Aug 2020 15:26:40 -0400 Subject: [PATCH 2/6] Moved code which copies subparser's settings to _register_subcommands(). Changed alias and macro commands to use as_subcommand_to() decorator. Updated CommandSet subcommand example to use help and description text. --- cmd2/cmd2.py | 330 +++++++++++++++++--------------- cmd2/constants.py | 2 +- cmd2/decorators.py | 28 +-- examples/modular_subcommands.py | 12 +- 4 files changed, 188 insertions(+), 184 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 855431d04..27edd19c3 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -47,7 +47,7 @@ from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer from .command_definition import CommandSet, _partial_passthru from .constants import COMMAND_FUNC_PREFIX, COMPLETER_FUNC_PREFIX, HELP_FUNC_PREFIX -from .decorators import with_argparser +from .decorators import with_argparser, as_subcommand_to from .exceptions import ( CommandSetRegistrationError, Cmd2ShlexError, @@ -572,6 +572,7 @@ def check_parser_uninstallable(parser): raise CommandSetRegistrationError( 'Cannot uninstall CommandSet when another CommandSet depends on it') check_parser_uninstallable(subparser) + break if command_parser is not None: check_parser_uninstallable(command_parser) @@ -598,7 +599,6 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: subcommand_name = getattr(method, constants.SUBCMD_ATTR_NAME) full_command_name = getattr(method, constants.SUBCMD_ATTR_COMMAND) # type: str subcmd_parser = getattr(method, constants.CMD_ATTR_ARGPARSER) - parser_args = getattr(method, constants.SUBCMD_ATTR_PARSER_ARGS, {}) command_tokens = full_command_name.split() command_name = command_tokens[0] @@ -633,14 +633,37 @@ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> for choice_name, choice in sub_action.choices.items(): if choice_name == cur_subcmd: return find_subcommand(choice, subcmd_names) + break raise CommandSetRegistrationError('Could not find sub-command "{}"'.format(full_command_name)) target_parser = find_subcommand(command_parser, subcommand_names) for action in target_parser._actions: if isinstance(action, argparse._SubParsersAction): - attached_parser = action.add_parser(subcommand_name, parents=[subcmd_parser], **parser_args) + # Get the kwargs for add_parser() + add_parser_kwargs = getattr(method, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, {}) + + # Set subcmd_parser as the parent to the parser we're creating to get its arguments + add_parser_kwargs['parents'] = [subcmd_parser] + + # argparse only copies actions from a parent and not the following settings. + # To retain these settings, we will copy them from subcmd_parser and pass them + # as ArgumentParser constructor arguments to add_parser(). + add_parser_kwargs['prog'] = subcmd_parser.prog + add_parser_kwargs['usage'] = subcmd_parser.usage + add_parser_kwargs['description'] = subcmd_parser.description + add_parser_kwargs['epilog'] = subcmd_parser.epilog + add_parser_kwargs['formatter_class'] = subcmd_parser.formatter_class + add_parser_kwargs['prefix_chars'] = subcmd_parser.prefix_chars + 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 + + attached_parser = action.add_parser(subcommand_name, **add_parser_kwargs) setattr(attached_parser, constants.PARSER_ATTR_COMMANDSET, cmdset) + break def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: """ @@ -686,6 +709,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: for action in command_parser._actions: if isinstance(action, argparse._SubParsersAction): action.remove_parser(subcommand_name) + break def add_settable(self, settable: Settable) -> None: """ @@ -2599,8 +2623,52 @@ def _cmdloop(self) -> None: if saved_readline_settings is not None: self._restore_readline(saved_readline_settings) - # ----- Alias subcommand functions ----- + # Top-level parser for alias + alias_description = ("Manage aliases\n" + "\n" + "An alias is a command that enables replacement of a word by another string.") + alias_epilog = ("See also:\n" + " macro") + alias_parser = DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog) + alias_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') + alias_subparsers.required = True + + # Preserve quotes since we are passing strings to other commands + @with_argparser(alias_parser, preserve_quotes=True) + def do_alias(self, args: argparse.Namespace) -> None: + """Manage aliases""" + # Call handler for whatever subcommand was selected + handler = args.get_handler() + handler(args) + + ############################################ + # Add subcommands to alias + ############################################ + + # alias -> create + alias_create_description = "Create or overwrite an alias" + alias_create_epilog = ("Notes:\n" + " If you want to use redirection, pipes, or terminators in the value of the\n" + " alias, then quote them.\n" + "\n" + " Since aliases are resolved during parsing, tab completion will function as\n" + " it would for the actual command the alias resolves to.\n" + "\n" + "Examples:\n" + " alias create ls !ls -lF\n" + " alias create show_log !cat \"log file.txt\"\n" + " alias create save_results print_results \">\" out.txt\n") + + alias_create_parser = DEFAULT_ARGUMENT_PARSER('create', add_help=False, 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) + alias_create_parser.add_argument('command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', + completer_method=path_complete) + + @as_subcommand_to('alias', 'create', alias_create_parser, help=alias_create_description.lower()) def _alias_create(self, args: argparse.Namespace) -> None: """Create or overwrite an alias""" @@ -2633,6 +2701,16 @@ def _alias_create(self, args: argparse.Namespace) -> None: self.aliases[args.name] = value self.poutput("Alias '{}' {}".format(args.name, result)) + # alias -> delete + alias_delete_help = "delete aliases" + alias_delete_description = "Delete specified aliases or all aliases if --all is used" + + alias_delete_parser = DEFAULT_ARGUMENT_PARSER('delete', add_help=False, 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") + + @as_subcommand_to('alias', 'delete', alias_delete_parser, help=alias_delete_help) def _alias_delete(self, args: argparse.Namespace) -> None: """Delete aliases""" if args.all: @@ -2648,6 +2726,18 @@ def _alias_delete(self, args: argparse.Namespace) -> None: else: self.perror("Alias '{}' does not exist".format(cur_name)) + # alias -> list + alias_list_help = "list aliases" + alias_list_description = ("List specified aliases in a reusable form that can be saved to a startup\n" + "script to preserve aliases across sessions\n" + "\n" + "Without arguments, all aliases will be listed.") + + alias_list_parser = DEFAULT_ARGUMENT_PARSER('list', add_help=False, 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') + + @as_subcommand_to('alias', 'list', alias_list_parser, help=alias_delete_help) def _alias_list(self, args: argparse.Namespace) -> None: """List some or all aliases""" if args.names: @@ -2660,76 +2750,76 @@ def _alias_list(self, args: argparse.Namespace) -> None: for cur_alias in sorted(self.aliases, key=self.default_sort_key): self.poutput("alias create {} {}".format(cur_alias, self.aliases[cur_alias])) - # Top-level parser for alias - alias_description = ("Manage aliases\n" + # Top-level parser for macro + macro_description = ("Manage macros\n" "\n" - "An alias is a command that enables replacement of a word by another string.") - alias_epilog = ("See also:\n" - " macro") - alias_parser = DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog) + "A macro is similar to an alias, but it can contain argument placeholders.") + macro_epilog = ("See also:\n" + " alias") + macro_parser = DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog) + macro_subparsers = macro_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') + macro_subparsers.required = True - # Add subcommands to alias - alias_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') - alias_subparsers.required = True + # Preserve quotes since we are passing strings to other commands + @with_argparser(macro_parser, preserve_quotes=True) + def do_macro(self, args: argparse.Namespace) -> None: + """Manage macros""" + # Call handler for whatever subcommand was selected + handler = args.get_handler() + handler(args) - # alias -> create - alias_create_description = "Create or overwrite an alias" + ############################################ + # Add subcommands to macro + ############################################ - alias_create_epilog = ("Notes:\n" + # macro -> create + macro_create_help = "create or overwrite a macro" + macro_create_description = "Create or overwrite a macro" + + macro_create_epilog = ("A macro is similar to an alias, but it can contain argument placeholders.\n" + "Arguments are expressed when creating a macro using {#} notation where {1}\n" + "means the first argument.\n" + "\n" + "The following creates a macro called my_macro that expects two arguments:\n" + "\n" + " macro create my_macro make_dinner --meat {1} --veggie {2}\n" + "\n" + "When the macro is called, the provided arguments are resolved and the\n" + "assembled command is run. For example:\n" + "\n" + " my_macro beef broccoli ---> make_dinner --meat beef --veggie broccoli\n" + "\n" + "Notes:\n" + " To use the literal string {1} in your command, escape it this way: {{1}}.\n" + "\n" + " Extra arguments passed to a macro are appended to resolved command.\n" + "\n" + " An argument number can be repeated in a macro. In the following example the\n" + " first argument will populate both {1} instances.\n" + "\n" + " macro create ft file_taxes -p {1} -q {2} -r {1}\n" + "\n" + " To quote an argument in the resolved command, quote it during creation.\n" + "\n" + " macro create backup !cp \"{1}\" \"{1}.orig\"\n" + "\n" " If you want to use redirection, pipes, or terminators in the value of the\n" - " alias, then quote them.\n" + " macro, then quote them.\n" "\n" - " Since aliases are resolved during parsing, tab completion will function as\n" - " it would for the actual command the alias resolves to.\n" + " macro create show_results print_results -type {1} \"|\" less\n" "\n" - "Examples:\n" - " alias create ls !ls -lF\n" - " alias create show_log !cat \"log file.txt\"\n" - " alias create save_results print_results \">\" out.txt\n") + " Because macros do not resolve until after hitting Enter, tab completion\n" + " will only complete paths while typing a macro.") - alias_create_parser = alias_subparsers.add_parser('create', help=alias_create_description.lower(), - 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', + macro_create_parser = DEFAULT_ARGUMENT_PARSER('create', add_help=False, 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) - alias_create_parser.add_argument('command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', - completer_method=path_complete) - alias_create_parser.set_defaults(func=_alias_create) - - # alias -> delete - alias_delete_help = "delete aliases" - alias_delete_description = "Delete specified aliases or all aliases if --all is used" - alias_delete_parser = alias_subparsers.add_parser('delete', help=alias_delete_help, - 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") - alias_delete_parser.set_defaults(func=_alias_delete) - - # alias -> list - alias_list_help = "list aliases" - alias_list_description = ("List specified aliases in a reusable form that can be saved to a startup\n" - "script to preserve aliases across sessions\n" - "\n" - "Without arguments, all aliases will be listed.") - - alias_list_parser = alias_subparsers.add_parser('list', help=alias_list_help, - 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') - alias_list_parser.set_defaults(func=_alias_list) - - # Preserve quotes since we are passing strings to other commands - @with_argparser(alias_parser, preserve_quotes=True) - def do_alias(self, args: argparse.Namespace) -> None: - """Manage aliases""" - # Call whatever subcommand function was selected - func = getattr(args, 'func') - func(self, args) - - # ----- Macro subcommand functions ----- + macro_create_parser.add_argument('command_args', nargs=argparse.REMAINDER, + help='arguments to pass to command', completer_method=path_complete) + @as_subcommand_to('macro', 'create', macro_create_parser, help=macro_create_help) def _macro_create(self, args: argparse.Namespace) -> None: """Create or overwrite a macro""" @@ -2809,6 +2899,15 @@ def _macro_create(self, args: argparse.Namespace) -> None: self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, arg_list=arg_list) self.poutput("Macro '{}' {}".format(args.name, result)) + # 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('delete', add_help=False, 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") + + @as_subcommand_to('macro', 'delete', macro_delete_parser, help=macro_delete_help) def _macro_delete(self, args: argparse.Namespace) -> None: """Delete macros""" if args.all: @@ -2824,6 +2923,18 @@ def _macro_delete(self, args: argparse.Namespace) -> None: else: self.perror("Macro '{}' does not exist".format(cur_name)) + # macro -> list + macro_list_help = "list macros" + macro_list_description = ("List specified macros in a reusable form that can be saved to a startup script\n" + "to preserve macros across sessions\n" + "\n" + "Without arguments, all macros will be listed.") + + macro_list_parser = DEFAULT_ARGUMENT_PARSER('list', add_help=False, 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') + + @as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help) def _macro_list(self, args: argparse.Namespace) -> None: """List some or all macros""" if args.names: @@ -2836,97 +2947,6 @@ def _macro_list(self, args: argparse.Namespace) -> None: for cur_macro in sorted(self.macros, key=self.default_sort_key): self.poutput("macro create {} {}".format(cur_macro, self.macros[cur_macro].value)) - # Top-level parser for macro - macro_description = ("Manage macros\n" - "\n" - "A macro is similar to an alias, but it can contain argument placeholders.") - macro_epilog = ("See also:\n" - " alias") - macro_parser = DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog) - - # Add subcommands to macro - macro_subparsers = macro_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') - macro_subparsers.required = True - - # macro -> create - macro_create_help = "create or overwrite a macro" - macro_create_description = "Create or overwrite a macro" - - macro_create_epilog = ("A macro is similar to an alias, but it can contain argument placeholders.\n" - "Arguments are expressed when creating a macro using {#} notation where {1}\n" - "means the first argument.\n" - "\n" - "The following creates a macro called my_macro that expects two arguments:\n" - "\n" - " macro create my_macro make_dinner --meat {1} --veggie {2}\n" - "\n" - "When the macro is called, the provided arguments are resolved and the\n" - "assembled command is run. For example:\n" - "\n" - " my_macro beef broccoli ---> make_dinner --meat beef --veggie broccoli\n" - "\n" - "Notes:\n" - " To use the literal string {1} in your command, escape it this way: {{1}}.\n" - "\n" - " Extra arguments passed to a macro are appended to resolved command.\n" - "\n" - " An argument number can be repeated in a macro. In the following example the\n" - " first argument will populate both {1} instances.\n" - "\n" - " macro create ft file_taxes -p {1} -q {2} -r {1}\n" - "\n" - " To quote an argument in the resolved command, quote it during creation.\n" - "\n" - " macro create backup !cp \"{1}\" \"{1}.orig\"\n" - "\n" - " If you want to use redirection, pipes, or terminators in the value of the\n" - " macro, then quote them.\n" - "\n" - " macro create show_results print_results -type {1} \"|\" less\n" - "\n" - " Because macros do not resolve until after hitting Enter, tab completion\n" - " will only complete paths while typing a macro.") - - macro_create_parser = macro_subparsers.add_parser('create', help=macro_create_help, - 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) - macro_create_parser.add_argument('command_args', nargs=argparse.REMAINDER, - help='arguments to pass to command', completer_method=path_complete) - macro_create_parser.set_defaults(func=_macro_create) - - # macro -> delete - macro_delete_help = "delete macros" - macro_delete_description = "Delete specified macros or all macros if --all is used" - macro_delete_parser = macro_subparsers.add_parser('delete', help=macro_delete_help, - 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") - macro_delete_parser.set_defaults(func=_macro_delete) - - # macro -> list - macro_list_help = "list macros" - macro_list_description = ("List specified macros in a reusable form that can be saved to a startup script\n" - "to preserve macros across sessions\n" - "\n" - "Without arguments, all macros will be listed.") - - macro_list_parser = macro_subparsers.add_parser('list', help=macro_list_help, 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') - macro_list_parser.set_defaults(func=_macro_list) - - # Preserve quotes since we are passing strings to other commands - @with_argparser(macro_parser, preserve_quotes=True) - def do_macro(self, args: argparse.Namespace) -> None: - """Manage macros""" - # Call whatever subcommand function was selected - func = getattr(args, 'func') - func(self, args) - def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: """Completes the command argument of help""" diff --git a/cmd2/constants.py b/cmd2/constants.py index aa2ccb6ab..9eaa9957c 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -56,7 +56,7 @@ # subcommand attributes for the base command name and the subcommand name SUBCMD_ATTR_COMMAND = 'parent_command' SUBCMD_ATTR_NAME = 'subcommand_name' -SUBCMD_ATTR_PARSER_ARGS = 'subcommand_parser_args' +SUBCMD_ATTR_ADD_PARSER_KWARGS = 'subcommand_add_parser_kwargs' # arpparse attribute linking to command set instance PARSER_ATTR_COMMANDSET = 'command_set' diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 9981ca944..0dda3485b 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -352,32 +352,14 @@ def arg_decorator(func: Callable): setattr(func, constants.CMD_ATTR_ARGPARSER, parser) setattr(func, constants.SUBCMD_ATTR_NAME, subcommand) - # Dictionary of arguments which will be passed to ArgumentParser.add_subparser() - parser_args = dict() - - # parser is set as the parent to the one being created by ArgumentParser.add_parser(). - # argparse only copies actions (arguments) from a parent and not the following settings. - # To retain these settings, we will copy them from parser and pass them as ArgumentParser - # constructor arguments to add_parser(). - parser_args['prog'] = parser.prog - parser_args['usage'] = parser.usage - parser_args['description'] = parser.description - parser_args['epilog'] = parser.epilog - parser_args['formatter_class'] = parser.formatter_class - parser_args['prefix_chars'] = parser.prefix_chars - parser_args['fromfile_prefix_chars'] = parser.fromfile_prefix_chars - parser_args['argument_default'] = parser.argument_default - parser_args['conflict_handler'] = parser.conflict_handler - parser_args['add_help'] = parser.add_help - parser_args['allow_abbrev'] = parser.allow_abbrev - - # Add remaining arguments specific to add_parser() + # Keyword arguments for ArgumentParser.add_subparser() + add_parser_kwargs = dict() if help is not None: - parser_args['help'] = help + add_parser_kwargs['help'] = help if aliases is not None: - parser_args['aliases'] = aliases[:] + add_parser_kwargs['aliases'] = aliases[:] - setattr(func, constants.SUBCMD_ATTR_PARSER_ARGS, parser_args) + setattr(func, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, add_parser_kwargs) return func diff --git a/examples/modular_subcommands.py b/examples/modular_subcommands.py index bf4a08aef..49620dfbf 100644 --- a/examples/modular_subcommands.py +++ b/examples/modular_subcommands.py @@ -23,10 +23,11 @@ def __init__(self): def do_apple(self, cmd: cmd2.Cmd, _: cmd2.Statement): cmd.poutput('Apple') - banana_parser = cmd2.Cmd2ArgumentParser(add_help=False) + banana_description = "Cut a banana" + banana_parser = cmd2.Cmd2ArgumentParser(add_help=False, description=banana_description) banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) - @cmd2.as_subcommand_to('cut', 'banana', banana_parser) + @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help=banana_description.lower()) def cut_banana(self, cmd: cmd2.Cmd, ns: argparse.Namespace): """Cut banana""" cmd.poutput('cutting banana: ' + ns.direction) @@ -40,10 +41,11 @@ def __init__(self): def do_arugula(self, cmd: cmd2.Cmd, _: cmd2.Statement): cmd.poutput('Arugula') - bokchoy_parser = cmd2.Cmd2ArgumentParser(add_help=False) + bokchoy_description = "Cut some bokchoy" + bokchoy_parser = cmd2.Cmd2ArgumentParser(add_help=False, description=bokchoy_description) bokchoy_parser.add_argument('style', choices=['quartered', 'diced']) - @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser) + @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser, help=bokchoy_description.lower()) def cut_bokchoy(self, cmd: cmd2.Cmd, _: cmd2.Statement): cmd.poutput('Bok Choy') @@ -95,9 +97,9 @@ def do_unload(self, ns: argparse.Namespace): @with_argparser(cut_parser) def do_cut(self, ns: argparse.Namespace): + # Call handler for whatever subcommand was selected handler = ns.get_handler() if handler is not None: - # Call whatever subcommand function was selected handler(ns) else: # No subcommand was provided, so call help From 2bb7f349661d903b7d0b491529dc8ecff9183012 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 7 Aug 2020 15:40:34 -0400 Subject: [PATCH 3/6] Updated change log --- CHANGELOG.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb532a19..de57031d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ +## 1.3.2 (August 7, 2020) +* Bug Fixes + * Fixed `prog` value of subcommands added with `as_subcommand_to()` decorator. + * Fixed missing settings in subcommand parsers created with `as_subcommand_to()` decorator. These settings + include things like description and epilog text. + ## 1.3.1 (August 6, 2020) * Bug Fixes * Fixed issue determining whether an argparse completer function required a reference to a containing - CommandSet. Also resolves issues determining the correct CommandSet instance when calling the argparse - argument completer function. Manifested as a TypeError when using `cmd2.Cmd.path_complete` as a completer - for an argparse-based command defined in a CommandSet + CommandSet. Also resolves issues determining the correct CommandSet instance when calling the argparse + argument completer function. Manifested as a TypeError when using `cmd2.Cmd.path_complete` as a completer + for an argparse-based command defined in a CommandSet ## 1.3.0 (August 4, 2020) * Enhancements From 091a88efc63a60211a0d1addb73c35bf47e7cd69 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 7 Aug 2020 16:11:09 -0400 Subject: [PATCH 4/6] Removed unnecessary prog value from subcommand parsers --- cmd2/cmd2.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 27edd19c3..82e520b7f 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2660,7 +2660,7 @@ def do_alias(self, args: argparse.Namespace) -> None: " alias create show_log !cat \"log file.txt\"\n" " alias create save_results print_results \">\" out.txt\n") - alias_create_parser = DEFAULT_ARGUMENT_PARSER('create', add_help=False, description=alias_create_description, + alias_create_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, 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', @@ -2705,7 +2705,7 @@ def _alias_create(self, args: argparse.Namespace) -> None: alias_delete_help = "delete aliases" alias_delete_description = "Delete specified aliases or all aliases if --all is used" - alias_delete_parser = DEFAULT_ARGUMENT_PARSER('delete', add_help=False, description=alias_delete_description) + alias_delete_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, 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") @@ -2733,7 +2733,7 @@ def _alias_delete(self, args: argparse.Namespace) -> None: "\n" "Without arguments, all aliases will be listed.") - alias_list_parser = DEFAULT_ARGUMENT_PARSER('list', add_help=False, description=alias_list_description) + alias_list_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, 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') @@ -2811,7 +2811,7 @@ def do_macro(self, args: argparse.Namespace) -> None: " 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('create', add_help=False, description=macro_create_description, + macro_create_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, 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', @@ -2902,7 +2902,7 @@ def _macro_create(self, args: argparse.Namespace) -> None: # 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('delete', add_help=False, description=macro_delete_description) + macro_delete_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, 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") @@ -2930,7 +2930,7 @@ def _macro_delete(self, args: argparse.Namespace) -> None: "\n" "Without arguments, all macros will be listed.") - macro_list_parser = DEFAULT_ARGUMENT_PARSER('list', add_help=False, description=macro_list_description) + macro_list_parser = DEFAULT_ARGUMENT_PARSER(add_help=False, 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') From dc94af322089e2fc2c1fb1be22e363e96061f2e8 Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Fri, 7 Aug 2020 16:45:01 -0400 Subject: [PATCH 5/6] Minor housekeeping. Updated CODEOWNERS with recent additions --- CODEOWNERS | 16 ++++++++++++++-- examples/modular_subcommands.py | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 01be734cf..442e08d95 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -16,14 +16,19 @@ # cmd2 code cmd2/__init__.py @tleonhardt @kotfu cmd2/ansi.py @kmvanbrunt @tleonhardt -cmd2/argparse_*.py @kmvanbrunt +cmd2/argparse_*.py @kmvanbrunt @anselor cmd2/clipboard.py @tleonhardt cmd2/cmd2.py @tleonhardt @kmvanbrunt @kotfu +cmd2/command_definition.py @anselor cmd2/constants.py @kotfu +cmd2/decorators.py @kotfu @kmvanbrunt @anselor +cmd2/exceptions.py @kmvanbrunt @anselor +cmd2/history.py @kotfu @tleonhardt cmd2/parsing.py @kotfu @kmvanbrunt cmd2/plugin.py @kotfu -cmd2/pyscript_bridge.py @kmvanbrunt +cmd2/py_bridge.py @kmvanbrunt cmd2/rl_utils.py @kmvanbrunt +cmd2/table_creator.py @kmvanbrunt cmd2/transcript.py @kotfu cmd2/utils.py @tleonhardt @kotfu @kmvanbrunt @@ -34,6 +39,11 @@ docs/* @tleonhardt @kotfu examples/async_printing.py @kmvanbrunt examples/environment.py @kotfu examples/tab_*.py @kmvanbrunt +examples/modular_*.py @anselor +examples/modular_commands/* @anselor + +plugins/template/* @kotfu +plugins/ext_test/* @anselor # Unit Tests tests/pyscript/* @kmvanbrunt @@ -47,6 +57,8 @@ tests/test_pars*.py @kotfu tests/test_run_pyscript.py @kmvanbrunt tests/test_transcript.py @kotfu +tests_isolated/test_commandset/* @anselor + # Top-level project stuff CONTRIBUTING.md @tleonhardt @kotfu setup.py @tleonhardt @kotfu diff --git a/examples/modular_subcommands.py b/examples/modular_subcommands.py index 49620dfbf..945fd54da 100644 --- a/examples/modular_subcommands.py +++ b/examples/modular_subcommands.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # coding=utf-8 -"""A simple example demonstracting modular subcommand loading through CommandSets +"""A simple example demonstrating modular subcommand loading through CommandSets In this example, there are loadable CommandSets defined. Each CommandSet has 1 subcommand defined that will be attached to the 'cut' command. From a04fdb50b372b3e1dda5200cc56150b2079ea596 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 7 Aug 2020 16:59:44 -0400 Subject: [PATCH 6/6] Moved a comment to better section off code --- cmd2/cmd2.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 82e520b7f..975f46359 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2623,6 +2623,10 @@ def _cmdloop(self) -> None: if saved_readline_settings is not None: self._restore_readline(saved_readline_settings) + ############################################################# + # Parsers and functions for alias command and subcommands + ############################################################# + # Top-level parser for alias alias_description = ("Manage aliases\n" "\n" @@ -2641,10 +2645,6 @@ def do_alias(self, args: argparse.Namespace) -> None: handler = args.get_handler() handler(args) - ############################################ - # Add subcommands to alias - ############################################ - # alias -> create alias_create_description = "Create or overwrite an alias" @@ -2750,6 +2750,10 @@ def _alias_list(self, args: argparse.Namespace) -> None: for cur_alias in sorted(self.aliases, key=self.default_sort_key): self.poutput("alias create {} {}".format(cur_alias, self.aliases[cur_alias])) + ############################################################# + # Parsers and functions for macro command and subcommands + ############################################################# + # Top-level parser for macro macro_description = ("Manage macros\n" "\n" @@ -2768,10 +2772,6 @@ def do_macro(self, args: argparse.Namespace) -> None: handler = args.get_handler() handler(args) - ############################################ - # Add subcommands to macro - ############################################ - # macro -> create macro_create_help = "create or overwrite a macro" macro_create_description = "Create or overwrite a macro"