diff --git a/CHANGELOG.md b/CHANGELOG.md index d3dfb1f8e..b358e4d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ ## 1.3.0 (August 4, 2020) -* Enchancements +* Enhancements * Added CommandSet - Enables defining a separate loadable module of commands to register/unregister - with your cmd2 application. + with your cmd2 application. +* Other + * Marked with_argparser_and_unknown_args pending deprecation and consolidated implementation into + with_argparser ## 1.2.1 (July 14, 2020) * Bug Fixes diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 5947020f5..7fee32955 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -172,7 +172,10 @@ 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 + """ + Deprecated decorator. Use `with_argparser(parser, with_unknown_args=True)` instead. + + 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. @@ -194,77 +197,23 @@ def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *, >>> parser.add_argument('-r', '--repeat', type=int, help='output [n] times') >>> >>> class MyApp(cmd2.Cmd): - >>> @cmd2.with_argparser_and_unknown_args(parser) + >>> @cmd2.with_argparser(parser, with_unknown_args=True) >>> def do_argprint(self, args, unknown): >>> "Print the options and argument list this options command was called with." >>> self.poutput('args: {!r}'.format(args)) >>> self.poutput('unknowns: {}'.format(unknown)) """ - import functools - - def arg_decorator(func: Callable): - @functools.wraps(func) - def cmd_wrapper(*args: Tuple[Any, ...], **kwargs: Dict[str, Any]) -> Optional[bool]: - """ - Command function wrapper which translates command line into argparse Namespace and calls actual - command function - - :param args: All positional arguments to this function. We're expecting there to be: - cmd2_app, statement: Union[Statement, str] - contiguously somewhere in the list - :param kwargs: any keyword arguments being passed to command function - :return: return value of command function - :raises: Cmd2ArgparseError if argparse has error parsing command line - """ - cmd2_app, statement = _parse_positionals(args) - statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, - statement, - preserve_quotes) - - if ns_provider is None: - namespace = None - else: - namespace = ns_provider(cmd2_app) - - try: - ns, unknown = parser.parse_known_args(parsed_arglist, namespace) - except SystemExit: - raise Cmd2ArgparseError - else: - setattr(ns, '__statement__', statement) + import warnings + warnings.warn('This decorator will be deprecated. Use `with_argparser(parser, with_unknown_args=True)`.', + PendingDeprecationWarning, stacklevel=2) - def get_handler(self: argparse.Namespace) -> Optional[Callable]: - return getattr(self, constants.SUBCMD_HANDLER, None) - - setattr(ns, 'get_handler', types.MethodType(get_handler, ns)) - - args_list = _arg_swap(args, statement, ns, unknown) - return func(*args_list, **kwargs) - - # 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(constants.COMMAND_FUNC_PREFIX):] - _set_parser_prog(parser, command_name) - - # 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__ - - # Set the command's help text as argparser.description (which can be None) - cmd_wrapper.__doc__ = parser.description - - # Set some custom attributes for this command - setattr(cmd_wrapper, constants.CMD_ATTR_ARGPARSER, parser) - setattr(cmd_wrapper, constants.CMD_ATTR_PRESERVE_QUOTES, preserve_quotes) - - return cmd_wrapper - - # noinspection PyTypeChecker - return arg_decorator + return with_argparser(parser, ns_provider=ns_provider, preserve_quotes=preserve_quotes, with_unknown_args=True) def with_argparser(parser: argparse.ArgumentParser, *, ns_provider: Optional[Callable[..., argparse.Namespace]] = None, - preserve_quotes: bool = False) -> Callable[[argparse.Namespace], Optional[bool]]: + preserve_quotes: bool = False, + with_unknown_args: 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. @@ -273,6 +222,7 @@ def with_argparser(parser: argparse.ArgumentParser, *, argparse.Namespace. This is useful if the Namespace needs to be prepopulated with state data that affects parsing. :param preserve_quotes: if True, then arguments passed to argparse maintain their quotes + :param with_unknown_args: if true, then capture unknown args :return: function that gets passed the argparse-parsed args in a Namespace A member called __statement__ is added to the Namespace to provide command functions access to the Statement object. This can be useful if the command function needs to know the command line. @@ -290,6 +240,21 @@ def with_argparser(parser: argparse.ArgumentParser, *, >>> def do_argprint(self, args): >>> "Print the options and argument list this options command was called with." >>> self.poutput('args: {!r}'.format(args)) + + :Example with unknown args: + + >>> parser = argparse.ArgumentParser() + >>> parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') + >>> parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + >>> parser.add_argument('-r', '--repeat', type=int, help='output [n] times') + >>> + >>> class MyApp(cmd2.Cmd): + >>> @cmd2.with_argparser(parser, with_unknown_args=True) + >>> def do_argprint(self, args, unknown): + >>> "Print the options and argument list this options command was called with." + >>> self.poutput('args: {!r}'.format(args)) + >>> self.poutput('unknowns: {}'.format(unknown)) + """ import functools @@ -318,7 +283,11 @@ def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]: namespace = ns_provider(cmd2_app) try: - ns = parser.parse_args(parsed_arglist, namespace) + if with_unknown_args: + new_args = parser.parse_known_args(parsed_arglist, namespace) + else: + new_args = (parser.parse_args(parsed_arglist, namespace), ) + ns = new_args[0] except SystemExit: raise Cmd2ArgparseError else: @@ -329,7 +298,7 @@ def get_handler(self: argparse.Namespace) -> Optional[Callable]: setattr(ns, 'get_handler', types.MethodType(get_handler, ns)) - args_list = _arg_swap(args, statement, ns) + args_list = _arg_swap(args, statement, *new_args) return func(*args_list, **kwargs) # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command diff --git a/docs/features/argument_processing.rst b/docs/features/argument_processing.rst index f98a686a0..9e65742eb 100644 --- a/docs/features/argument_processing.rst +++ b/docs/features/argument_processing.rst @@ -272,7 +272,7 @@ Here's what it looks like:: dir_parser = argparse.ArgumentParser() dir_parser.add_argument('-l', '--long', action='store_true', help="display in long format with one item per line") - @with_argparser_and_unknown_args(dir_parser) + @with_argparser(dir_parser, with_unknown_args=True) def do_dir(self, args, unknown): """List contents of current directory.""" # No arguments for this command diff --git a/examples/arg_print.py b/examples/arg_print.py index 3f7f3815a..dbf740ff6 100755 --- a/examples/arg_print.py +++ b/examples/arg_print.py @@ -56,7 +56,7 @@ def do_oprint(self, args): pprint_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') pprint_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') - @cmd2.with_argparser_and_unknown_args(pprint_parser) + @cmd2.with_argparser(pprint_parser, with_unknown_args=True) def do_pprint(self, args, unknown): """Print the options and argument list this options command was called with.""" self.poutput('oprint was called with the following\n\toptions: {!r}\n\targuments: {}'.format(args, unknown)) diff --git a/examples/modular_commands/commandset_complex.py b/examples/modular_commands/commandset_complex.py index 5a031bd06..ec5a9e130 100644 --- a/examples/modular_commands/commandset_complex.py +++ b/examples/modular_commands/commandset_complex.py @@ -23,7 +23,7 @@ def do_banana(self, cmd: cmd2.Cmd, statement: cmd2.Statement): cranberry_parser = cmd2.Cmd2ArgumentParser('cranberry') cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce']) - @cmd2.with_argparser_and_unknown_args(cranberry_parser) + @cmd2.with_argparser(cranberry_parser, with_unknown_args=True) def do_cranberry(self, cmd: cmd2.Cmd, ns: argparse.Namespace, unknown: List[str]): cmd.poutput('Cranberry {}!!'.format(ns.arg1)) if unknown and len(unknown): diff --git a/examples/python_scripting.py b/examples/python_scripting.py index 69cfb6724..6e4295d43 100755 --- a/examples/python_scripting.py +++ b/examples/python_scripting.py @@ -96,7 +96,7 @@ def complete_cd(self, text, line, begidx, endidx): dir_parser = argparse.ArgumentParser() dir_parser.add_argument('-l', '--long', action='store_true', help="display in long format with one item per line") - @cmd2.with_argparser_and_unknown_args(dir_parser) + @cmd2.with_argparser(dir_parser, with_unknown_args=True) def do_dir(self, args, unknown): """List contents of current directory.""" # No arguments for this command diff --git a/isolated_tests/test_commandset/test_commandset.py b/isolated_tests/test_commandset/test_commandset.py index 90f0448cd..83ae4646a 100644 --- a/isolated_tests/test_commandset/test_commandset.py +++ b/isolated_tests/test_commandset/test_commandset.py @@ -28,7 +28,7 @@ def do_banana(self, cmd: cmd2.Cmd, statement: cmd2.Statement): cranberry_parser = cmd2.Cmd2ArgumentParser('cranberry') cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce']) - @cmd2.with_argparser_and_unknown_args(cranberry_parser) + @cmd2.with_argparser(cranberry_parser, with_unknown_args=True) def do_cranberry(self, cmd: cmd2.Cmd, ns: argparse.Namespace, unknown: List[str]): cmd.poutput('Cranberry {}!!'.format(ns.arg1)) if unknown and len(unknown): diff --git a/tests/test_argparse.py b/tests/test_argparse.py index daf434978..1334f9e3a 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -92,7 +92,7 @@ def do_preservelist(self, arglist): known_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') known_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') known_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') - @cmd2.with_argparser_and_unknown_args(known_parser) + @cmd2.with_argparser(known_parser, with_unknown_args=True) def do_speak(self, args, extra, *, keyword_arg: Optional[str] = None): """Repeat what you tell me to.""" words = [] @@ -112,11 +112,11 @@ def do_speak(self, args, extra, *, keyword_arg: Optional[str] = None): if keyword_arg is not None: print(keyword_arg) - @cmd2.with_argparser_and_unknown_args(argparse.ArgumentParser(), preserve_quotes=True) + @cmd2.with_argparser(argparse.ArgumentParser(), preserve_quotes=True, with_unknown_args=True) def do_test_argparse_with_list_quotes(self, args, extra): self.stdout.write('{}'.format(' '.join(extra))) - @cmd2.with_argparser_and_unknown_args(argparse.ArgumentParser(), ns_provider=namespace_provider) + @cmd2.with_argparser(argparse.ArgumentParser(), ns_provider=namespace_provider, with_unknown_args=True) def do_test_argparse_with_list_ns(self, args, extra): self.stdout.write('{}'.format(args.custom_stuff)) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index bc0e0a94f..8688e124f 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1224,7 +1224,7 @@ def test_select_ctrl_c(outsim_app, monkeypatch, capsys): class HelpNoDocstringApp(cmd2.Cmd): greet_parser = argparse.ArgumentParser() greet_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE") - @cmd2.with_argparser_and_unknown_args(greet_parser) + @cmd2.with_argparser(greet_parser, with_unknown_args=True) def do_greet(self, opts, arg): arg = ''.join(arg) if opts.shout: @@ -1268,7 +1268,7 @@ def __init__(self, *args, **kwargs): orate_parser = argparse.ArgumentParser() orate_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE") - @cmd2.with_argparser_and_unknown_args(orate_parser) + @cmd2.with_argparser(orate_parser, with_unknown_args=True) def do_orate(self, opts, arg): arg = ''.join(arg) if opts.shout: diff --git a/tests/test_completion.py b/tests/test_completion.py index a380d43af..48a055d0a 100755 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -1132,7 +1132,7 @@ def base_sport(self, args): parser_sport = base_subparsers.add_parser('sport', help='sport help') sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs) - @cmd2.with_argparser_and_unknown_args(base_parser) + @cmd2.with_argparser(base_parser, with_unknown_args=True) def do_base(self, args): """Base command help""" func = getattr(args, 'func', None) diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 69389b7f2..55d60e181 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -41,7 +41,7 @@ def __init__(self, *args, **kwargs): speak_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE") speak_parser.add_argument('-r', '--repeat', type=int, help="output [n] times") - @cmd2.with_argparser_and_unknown_args(speak_parser) + @cmd2.with_argparser(speak_parser, with_unknown_args=True) def do_speak(self, opts, arg): """Repeats what you tell me to.""" arg = ' '.join(arg) @@ -61,7 +61,8 @@ def do_speak(self, opts, arg): mumble_parser = argparse.ArgumentParser() mumble_parser.add_argument('-r', '--repeat', type=int, help="output [n] times") - @cmd2.with_argparser_and_unknown_args(mumble_parser) + + @cmd2.with_argparser(mumble_parser, with_unknown_args=True) def do_mumble(self, opts, arg): """Mumbles what you tell me to.""" repetitions = opts.repeat or 1