summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2020-08-25 01:46:04 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2020-08-25 10:10:14 -0400
commit9b5a98825a9b00807a40494e8c634c392077ccd2 (patch)
treeb8cfbaca2ec4622a981d96c33c7a964ca621108e
parenta540cfc5373cee2272de6c81be8b6fa8a78c6462 (diff)
downloadcmd2-git-9b5a98825a9b00807a40494e8c634c392077ccd2.tar.gz
Fixed RecursionError when printing an argparse.Namespace caused by custom attribute cmd2 was adding
Added get_statement() function to argparse.Namespace which returns __statement__ attribute
-rw-r--r--CHANGELOG.md6
-rw-r--r--cmd2/cmd2.py4
-rw-r--r--cmd2/constants.py11
-rw-r--r--cmd2/decorators.py14
-rw-r--r--docs/features/argument_processing.rst3
-rwxr-xr-xexamples/decorator_example.py2
-rw-r--r--tests/test_argparse.py23
-rw-r--r--tests/test_plugin.py2
8 files changed, 48 insertions, 17 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 161a058d..f2ba2710 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 1.3.5 (TBD)
+* Bug Fixes
+ * Fixed `RecursionError` when printing an `argparse.Namespace` caused by custom attribute cmd2 was adding
+* Enhancements
+ * Added `get_statement()` function to `argparse.Namespace` which returns `__statement__` attribute
+
## 1.3.4 (August 20, 2020)
* Bug Fixes
* Fixed `AttributeError` when `CommandSet` that uses `as_subcommand_to` decorator is loaded during
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index da576805..bc6691f6 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -663,7 +663,9 @@ class Cmd(cmd.Cmd):
raise CommandSetRegistrationError('Could not find argparser for command "{}" needed by subcommand: {}'
.format(command_name, str(method)))
- subcmd_parser.set_defaults(cmd2_handler=method)
+ # Set the subcommand handler function
+ defaults = {constants.NS_ATTR_SUBCMD_HANDLER: method}
+ subcmd_parser.set_defaults(**defaults)
def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> argparse.ArgumentParser:
if not subcmd_names:
diff --git a/cmd2/constants.py b/cmd2/constants.py
index 9eaa9957..037a7cab 100644
--- a/cmd2/constants.py
+++ b/cmd2/constants.py
@@ -37,10 +37,6 @@ HELP_FUNC_PREFIX = 'help_'
# All command completer functions start with this
COMPLETER_FUNC_PREFIX = 'complete_'
-##############################################################################
-# The following are optional attributes added to do_* command functions
-##############################################################################
-
# The custom help category a command belongs to
CMD_ATTR_HELP_CATEGORY = 'help_category'
@@ -50,9 +46,6 @@ CMD_ATTR_ARGPARSER = 'argparser'
# Whether or not tokens are unquoted before sending to argparse
CMD_ATTR_PRESERVE_QUOTES = 'preserve_quotes'
-# optional attribute
-SUBCMD_HANDLER = 'cmd2_handler'
-
# subcommand attributes for the base command name and the subcommand name
SUBCMD_ATTR_COMMAND = 'parent_command'
SUBCMD_ATTR_NAME = 'subcommand_name'
@@ -60,3 +53,7 @@ SUBCMD_ATTR_ADD_PARSER_KWARGS = 'subcommand_add_parser_kwargs'
# arpparse attribute linking to command set instance
PARSER_ATTR_COMMANDSET = 'command_set'
+
+# custom attributes added to argparse Namespaces
+NS_ATTR_SUBCMD_HANDLER = '__subcmd_handler__'
+NS_ATTR_STATEMENT = '__statement__'
diff --git a/cmd2/decorators.py b/cmd2/decorators.py
index 689f29c5..ccbbd832 100644
--- a/cmd2/decorators.py
+++ b/cmd2/decorators.py
@@ -1,7 +1,6 @@
# coding=utf-8
"""Decorators for ``cmd2`` commands"""
import argparse
-import types
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
from . import constants
@@ -190,6 +189,7 @@ def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *,
of unknown argument strings. A member called ``__statement__`` is added to the
``Namespace`` to provide command functions access to the :class:`cmd2.Statement`
object. This can be useful if the command function needs to know the command line.
+ ``__statement__`` can also be retrieved by calling ``get_statement()`` on the ``Namespace``.
:Example:
@@ -228,6 +228,7 @@ def with_argparser(parser: argparse.ArgumentParser, *,
: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.
+ ``__statement__`` can also be retrieved by calling ``get_statement()`` on the ``Namespace``.
:Example:
@@ -297,12 +298,13 @@ def with_argparser(parser: argparse.ArgumentParser, *,
except SystemExit:
raise Cmd2ArgparseError
else:
- setattr(ns, '__statement__', statement)
+ # Add statement to Namespace and a getter function for it
+ setattr(ns, constants.NS_ATTR_STATEMENT, statement)
+ setattr(ns, 'get_statement', lambda: statement)
- def get_handler(ns_self: argparse.Namespace) -> Optional[Callable]:
- return getattr(ns_self, constants.SUBCMD_HANDLER, None)
-
- setattr(ns, 'get_handler', types.MethodType(get_handler, ns))
+ # Add getter function for subcmd handler, which can be None
+ subcmd_handler = getattr(ns, constants.NS_ATTR_SUBCMD_HANDLER, None)
+ setattr(ns, 'get_handler', lambda: subcmd_handler)
args_list = _arg_swap(args, statement, *new_args)
return func(*args_list, **kwargs)
diff --git a/docs/features/argument_processing.rst b/docs/features/argument_processing.rst
index 9e65742e..16170239 100644
--- a/docs/features/argument_processing.rst
+++ b/docs/features/argument_processing.rst
@@ -14,7 +14,8 @@ handles the following for you:
3. Passes the resulting ``argparse.Namespace`` object to your command function.
The ``Namespace`` includes the ``Statement`` object that was created when
parsing the command line. It is stored in the ``__statement__`` attribute of
- the ``Namespace``.
+ the ``Namespace`` and can also be retrieved by calling ``get_statement()``
+ on the ``Namespace``.
4. Adds the usage message from the argument parser to your command.
diff --git a/examples/decorator_example.py b/examples/decorator_example.py
index 0f5374ce..5b721da6 100755
--- a/examples/decorator_example.py
+++ b/examples/decorator_example.py
@@ -67,7 +67,7 @@ class CmdLineApp(cmd2.Cmd):
def do_tag(self, args: argparse.Namespace):
"""create an html tag"""
# The Namespace always includes the Statement object created when parsing the command line
- statement = args.__statement__
+ statement = args.get_statement()
self.poutput("The command line you ran was: {}".format(statement.command_and_args))
self.poutput("It generated this tag:")
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index 0d46b15a..9806c9b5 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -290,6 +290,24 @@ class SubcommandApp(cmd2.Cmd):
func(self, args)
+ # Add a subcommand using as_subcommand_to decorator
+ has_subcmd_parser = cmd2.Cmd2ArgumentParser(description="Tests as_subcmd_to decorator")
+ has_subcmd_subparsers = has_subcmd_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
+ has_subcmd_subparsers.required = True
+
+ @cmd2.with_argparser(has_subcmd_parser)
+ def do_test_subcmd_decorator(self, args: argparse.Namespace):
+ handler = args.get_handler()
+ handler(args)
+
+ subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="The subcommand")
+
+ @cmd2.as_subcommand_to('test_subcmd_decorator', 'subcmd', subcmd_parser, help='the subcommand')
+ def subcmd_func(self, args: argparse.Namespace):
+ # Make sure printing the Namespace works. The way we originally added get_hander()
+ # to it resulted in a RecursionError when printing.
+ print(args)
+
@pytest.fixture
def subcommand_app():
app = SubcommandApp()
@@ -373,6 +391,11 @@ def test_add_another_subcommand(subcommand_app):
assert new_parser.prog == "base new_sub"
+def test_subcmd_decorator(subcommand_app):
+ out, err = run_cmd(subcommand_app, 'test_subcmd_decorator subcmd')
+ assert out[0].startswith('Namespace(')
+
+
def test_unittest_mock():
from unittest import mock
from cmd2 import CommandSetRegistrationError
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index e49cbbfc..d40a9b61 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -279,7 +279,7 @@ class PluggedApp(Plugin, cmd2.Cmd):
@with_argparser(parser)
def do_argparse_cmd(self, namespace: argparse.Namespace):
"""Repeat back the arguments"""
- self.poutput(namespace.__statement__)
+ self.poutput(namespace.get_statement())
###
#