diff options
-rw-r--r-- | CHANGELOG.md | 5 | ||||
-rw-r--r-- | cmd2/cmd2.py | 28 | ||||
-rw-r--r-- | docs/argument_processing.rst | 27 | ||||
-rw-r--r-- | tests/test_argparse.py | 56 |
4 files changed, 93 insertions, 23 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a6f56821..2fe4e734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,10 +19,13 @@ scroll the actual error message off the screen. * Exceptions occurring in tab completion functions are now printed to stderr before returning control back to readline. This makes debugging a lot easier since readline suppresses these exceptions. + * Added support for custom Namespaces in the argparse decorators. See description of `ns_provider` argument + for more information. * Potentially breaking changes * Replaced `unquote_redirection_tokens()` with `unquote_specific_tokens()`. This was to support the fix that allows terminators in alias and macro values. - * Changed `Statement.pipe_to` to a string instead of a list + * Changed `Statement.pipe_to` to a string instead of a list + * `preserve_quotes` is now a keyword-only argument in the argparse decorators * **Python 3.4 EOL notice** * Python 3.4 reached its [end of life](https://www.python.org/dev/peps/pep-0429/) on March 18, 2019 * This is the last release of `cmd2` which will support Python 3.4 diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c29a1812..f5a2a844 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -191,12 +191,17 @@ def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) -> return arg_decorator -def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve_quotes: bool = False) -> \ +def with_argparser_and_unknown_args(argparser: 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 arguments with the given instance of argparse.ArgumentParser, but also returning unknown args as a list. :param argparser: unique instance of ArgumentParser + :param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an + 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 :return: function that gets passed argparse-parsed args in a Namespace and a list of unknown argument strings A member called __statement__ is added to the Namespace to provide command functions access to the @@ -213,8 +218,13 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve statement, preserve_quotes) + if ns_provider is None: + namespace = None + else: + namespace = ns_provider(cmd2_instance) + try: - args, unknown = argparser.parse_known_args(parsed_arglist) + args, unknown = argparser.parse_known_args(parsed_arglist, namespace) except SystemExit: return else: @@ -241,12 +251,16 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve return arg_decorator -def with_argparser(argparser: argparse.ArgumentParser, +def with_argparser(argparser: argparse.ArgumentParser, *, + ns_provider: Optional[Callable[..., argparse.Namespace]] = None, preserve_quotes: 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. :param argparser: unique instance of ArgumentParser + :param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an + 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 :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 @@ -261,8 +275,14 @@ def with_argparser(argparser: argparse.ArgumentParser, statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name, statement, preserve_quotes) + + if ns_provider is None: + namespace = None + else: + namespace = ns_provider(cmd2_instance) + try: - args = argparser.parse_args(parsed_arglist) + args = argparser.parse_args(parsed_arglist, namespace) except SystemExit: return else: diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst index fc1f2433..4bd917cf 100644 --- a/docs/argument_processing.rst +++ b/docs/argument_processing.rst @@ -247,7 +247,7 @@ argument list instead of a string:: pass -Using the argument parser decorator and also receiving a a list of unknown positional arguments +Using the argument parser decorator and also receiving a list of unknown positional arguments =============================================================================================== If you want all unknown arguments to be passed to your command as a list of strings, then decorate the command method with the ``@with_argparser_and_unknown_args`` decorator. @@ -275,6 +275,31 @@ Here's what it looks like:: ... +Using custom argparse.Namespace with argument parser decorators +=============================================================================================== +In some cases, it may be necessary to write custom ``argparse`` code that is dependent on state data of your +application. To support this ability while still allowing use of the decorators, both ``@with_argparser`` and +``@with_argparser_and_unknown_args`` have an optional argument called ``ns_provider``. + +``ns_provider`` is a Callable that accepts a ``cmd2.Cmd`` object as an argument and returns an ``argparse.Namespace``:: + + Callable[[cmd2.Cmd], argparse.Namespace] + +For example:: + + def settings_ns_provider(self) -> argparse.Namespace: + """Populate an argparse Namespace with current settings""" + ns = argparse.Namespace() + ns.app_settings = self.settings + return ns + +To use this function with the argparse decorators, do the following:: + + @with_argparser(my_parser, ns_provider=settings_ns_provider) + +The Namespace is passed by the decorators to the ``argparse`` parsing functions which gives your custom code access +to the state data it needs for its parsing logic. + Sub-commands ============ Sub-commands are supported for commands using either the ``@with_argparser`` or diff --git a/tests/test_argparse.py b/tests/test_argparse.py index d716c68d..74a5d16f 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -29,6 +29,11 @@ class ArgparseApp(cmd2.Cmd): self.maxrepeats = 3 cmd2.Cmd.__init__(self) + def namespace_provider(self) -> argparse.Namespace: + ns = argparse.Namespace() + ns.custom_stuff = "custom" + return ns + say_parser = argparse.ArgumentParser() say_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') say_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') @@ -56,11 +61,15 @@ class ArgparseApp(cmd2.Cmd): tag_parser.add_argument('tag', help='tag') tag_parser.add_argument('content', nargs='+', help='content to surround with tag') - @cmd2.with_argparser(tag_parser) + @cmd2.with_argparser(tag_parser, preserve_quotes=True) def do_tag(self, args): self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content))) self.stdout.write('\n') + @cmd2.with_argparser(argparse.ArgumentParser(), ns_provider=namespace_provider) + def do_test_argparse_ns(self, args): + self.stdout.write('{}'.format(args.custom_stuff)) + @cmd2.with_argument_list def do_arglist(self, arglist): if isinstance(arglist, list): @@ -93,21 +102,14 @@ class ArgparseApp(cmd2.Cmd): self.stdout.write(' '.join(words)) self.stdout.write('\n') - @cmd2.with_argparser_and_unknown_args(known_parser) - def do_talk(self, args, extra): - words = [] - for word in extra: - if word is None: - word = '' - if args.piglatin: - word = '%s%say' % (word[1:], word[0]) - if args.shout: - word = word.upper() - words.append(word) - repetitions = args.repeat or 1 - for i in range(min(repetitions, self.maxrepeats)): - self.stdout.write(' '.join(words)) - self.stdout.write('\n') + @cmd2.with_argparser_and_unknown_args(argparse.ArgumentParser(), preserve_quotes=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) + def do_test_argparse_with_list_ns(self, args, extra): + self.stdout.write('{}'.format(args.custom_stuff)) + @pytest.fixture def argparse_app(): @@ -123,14 +125,34 @@ def test_argparse_basic_command(argparse_app): out, err = run_cmd(argparse_app, 'say hello') assert out == ['hello'] -def test_argparse_quoted_arguments(argparse_app): +def test_argparse_remove_quotes(argparse_app): out, err = run_cmd(argparse_app, 'say "hello there"') assert out == ['hello there'] +def test_argparse_preserve_quotes(argparse_app): + out, err = run_cmd(argparse_app, 'tag mytag "hello"') + assert out[0] == '<mytag>"hello"</mytag>' + +def test_argparse_custom_namespace(argparse_app): + out, err = run_cmd(argparse_app, 'test_argparse_ns') + assert out[0] == 'custom' + def test_argparse_with_list(argparse_app): out, err = run_cmd(argparse_app, 'speak -s hello world!') assert out == ['HELLO WORLD!'] +def test_argparse_with_list_remove_quotes(argparse_app): + out, err = run_cmd(argparse_app, 'speak -s hello "world!"') + assert out == ['HELLO WORLD!'] + +def test_argparse_with_list_preserve_quotes(argparse_app): + out, err = run_cmd(argparse_app, 'test_argparse_with_list_quotes "hello" person') + assert out[0] == '"hello" person' + +def test_argparse_with_list_custom_namespace(argparse_app): + out, err = run_cmd(argparse_app, 'test_argparse_with_list_ns') + assert out[0] == 'custom' + def test_argparse_with_list_and_empty_doc(argparse_app): out, err = run_cmd(argparse_app, 'speak -s hello world!') assert out == ['HELLO WORLD!'] |