Repository: python-cmd2/cmd2 (python)

Issue:
Pass a custom Namespace to args
Kind of a follow up on https://github.com/python-cmd2/cmd2/issues/498

I want to pass a custom Namespace when parsing arguments while keeping the autocompletion:

```
    @with_argparser(parser)
    def do_load_pcap(self, cmdline):
          myNamespace = Namespace()
          myNamespace.toto = self.data
          args = parser.parse_args(shlex.split(cmdline), myNamespace)
          
```
I modified 
https://github.com/python-cmd2/cmd2/blob/b6c39365e0331ab64aad074565fe73fb074a3a7f/cmd2/cmd2.py#L270-L271
so that it doesn't parse stuff.
```
def with_argparser_test(argparser: argparse.ArgumentParser,
                   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 preserve_quotes: if True, then arguments passed to argparse maintain their quotes
    :return: function that gets passed the argparse-parsed args
    """
    import functools

    # noinspection PyProtectedMember
    def arg_decorator(func: Callable[[Any], Optional[bool]]):
        @functools.wraps(func)
        def cmd_wrapper(instance, cmdline):
            # lexed_arglist = parse_quoted_string(cmdline, preserve_quotes)
            # try:
            #     args = argparser.parse_args(lexed_arglist)
            # except SystemExit:
            #     return
            # else:
            return func(instance, cmdline)

        # argparser defaults the program name to sys.argv[0]
        # we want it to be the name of our command
        # argparser.prog = func.__name__[len(COMMAND_FUNC_PREFIX):]

        # If the description has not been set, then use the method docstring if one exists
        if argparser.description is None and func.__doc__:
            argparser.description = func.__doc__

        # Set the command's help text as argparser.description (which can be None)
        # cmd_wrapper.__doc__ = argparser.description

        # Mark this function as having an argparse ArgumentParser
        setattr(cmd_wrapper, 'argparser', argparser)

        return cmd_wrapper

    return arg_decorator

```
which can be used like
```
    @with_argparser_test(parser)
    def do_load_pcap(self, args):
        """
        Load the file as the current one
        """
        args = shlex.split(args)
        parser = self.do_load_pcap.argparser
        args = parser.parse_args(args)
        
        self.poutput("Loading %s" % args.input_file)

```

I wonder if cmd2 could propose some better interface. This is just an example, that adds a parameter "dont_parse" to the with_argparser decorator. If it's true, cmd2 could pass the parser along to the member function (instead of `parser = self.do_load_pcap.argparser` which requires some knowledge about cmd2 implementation)
```
@with_argparser(dont_quote, dont_parse=True)
def func(self, parser, args):
      parser.parse_args(args)
```

Pull request:
Ns provider
Added support for custom Namespaces in the argparse decorators
Fixes #596 

Patch:
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


Test patch:
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!']