summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2020-08-27 13:19:23 -0400
committerGitHub <noreply@github.com>2020-08-27 13:19:23 -0400
commitf4943a904efba05c13eb930b4a4535c19429c72c (patch)
treecbaff855e2d73f1d86ae1230f32770e31c62ad6b
parent97c348c599d8fa963553593e5c19fb100b85e313 (diff)
parent478ea83336a4d1659ad06ef365db3dc5e051e46d (diff)
downloadcmd2-git-f4943a904efba05c13eb930b4a4535c19429c72c.tar.gz
Merge pull request #985 from python-cmd2/dynamic_value
Added Cmd2AttributeWrapper class
-rw-r--r--CHANGELOG.md8
-rw-r--r--cmd2/__init__.py2
-rw-r--r--cmd2/argparse_custom.py20
-rw-r--r--cmd2/cmd2.py4
-rw-r--r--cmd2/decorators.py36
-rw-r--r--docs/features/argument_processing.rst20
-rw-r--r--docs/features/modular_commands.rst2
-rw-r--r--docs/testing.rst2
-rwxr-xr-xexamples/decorator_example.py2
-rw-r--r--examples/modular_subcommands.py2
-rw-r--r--tests/test_argparse.py5
-rw-r--r--tests/test_argparse_custom.py10
-rw-r--r--tests/test_plugin.py2
-rw-r--r--tests_isolated/test_commandset/test_commandset.py12
14 files changed, 86 insertions, 41 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e57c1e4b..5efc8b69 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 1.3.6 (August 26, 2020)
+* Breaking changes
+ * The functions cmd2 adds to Namespaces (`get_statement()` and `get_handler()`) are now
+ `Cmd2AttributeWrapper` objects named `cmd2_statement` and `cmd2_handler`. This makes it
+ easy to filter out which attributes in an `argparse.Namespace` were added by `cmd2`.
+* Deprecations
+ * ``Namespace.__statement__`` will be removed in `cmd2` 2.0.0. Use `Namespace.get_statement()` going forward.
+
## 1.3.5 (August 25, 2020)
* Bug Fixes
* Fixed `RecursionError` when printing an `argparse.Namespace` caused by custom attribute cmd2 was adding
diff --git a/cmd2/__init__.py b/cmd2/__init__.py
index 9f0bb176..81e80efe 100644
--- a/cmd2/__init__.py
+++ b/cmd2/__init__.py
@@ -16,7 +16,7 @@ except importlib_metadata.PackageNotFoundError: # pragma: no cover
pass
from .ansi import style, fg, bg
-from .argparse_custom import Cmd2ArgumentParser, CompletionItem, set_default_argument_parser
+from .argparse_custom import Cmd2ArgumentParser, Cmd2AttributeWrapper, CompletionItem, set_default_argument_parser
# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER
import argparse
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
index 12c18644..45abe6b2 100644
--- a/cmd2/argparse_custom.py
+++ b/cmd2/argparse_custom.py
@@ -221,7 +221,7 @@ import re
import sys
# noinspection PyUnresolvedReferences,PyProtectedMember
from argparse import ONE_OR_MORE, ZERO_OR_MORE, ArgumentError, _
-from typing import Callable, Optional, Tuple, Type, Union
+from typing import Any, Callable, Optional, Tuple, Type, Union
from . import ansi, constants
@@ -904,6 +904,24 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
ansi.style_aware_write(file, message)
+class Cmd2AttributeWrapper:
+ """
+ Wraps a cmd2-specific attribute added to an argparse Namespace.
+ This makes it easy to know which attributes in a Namespace are
+ arguments from a parser and which were added by cmd2.
+ """
+ def __init__(self, attribute: Any):
+ self.__attribute = attribute
+
+ def get(self) -> Any:
+ """Get the value of the attribute"""
+ return self.__attribute
+
+ def set(self, new_val: Any) -> None:
+ """Set the value of the attribute"""
+ self.__attribute = new_val
+
+
# The default ArgumentParser class for a cmd2 app
DEFAULT_ARGUMENT_PARSER = Cmd2ArgumentParser
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index bc6691f6..103508c5 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -2685,7 +2685,7 @@ class Cmd(cmd.Cmd):
def do_alias(self, args: argparse.Namespace) -> None:
"""Manage aliases"""
# Call handler for whatever subcommand was selected
- handler = args.get_handler()
+ handler = args.cmd2_handler.get()
handler(args)
# alias -> create
@@ -2812,7 +2812,7 @@ class Cmd(cmd.Cmd):
def do_macro(self, args: argparse.Namespace) -> None:
"""Manage macros"""
# Call handler for whatever subcommand was selected
- handler = args.get_handler()
+ handler = args.cmd2_handler.get()
handler(args)
# macro -> create
diff --git a/cmd2/decorators.py b/cmd2/decorators.py
index ccbbd832..c2689102 100644
--- a/cmd2/decorators.py
+++ b/cmd2/decorators.py
@@ -4,6 +4,7 @@ import argparse
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
from . import constants
+from .argparse_custom import Cmd2AttributeWrapper
from .exceptions import Cmd2ArgparseError
from .parsing import Statement
@@ -186,10 +187,10 @@ def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *,
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 :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``.
+ of unknown argument strings. A :class:`cmd2.argparse_custom.Cmd2AttributeWrapper` called
+ ``cmd2_statement`` is included in the ``Namespace`` to provide access to the :class:`cmd2.Statement`
+ object. that was created when parsing the command line. This can be useful if the command function
+ needs to know the command line.
:Example:
@@ -223,12 +224,12 @@ def with_argparser(parser: argparse.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
+ :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.
- ``__statement__`` can also be retrieved by calling ``get_statement()`` on the ``Namespace``.
+ :return: function that gets passed argparse-parsed args in a ``Namespace``
+ A :class:`cmd2.argparse_custom.Cmd2AttributeWrapper` called ``cmd2_statement`` is included
+ in the ``Namespace`` to provide access to the :class:`cmd2.Statement` object that was created when
+ parsing the command line. This can be useful if the command function needs to know the command line.
:Example:
@@ -298,13 +299,20 @@ def with_argparser(parser: argparse.ArgumentParser, *,
except SystemExit:
raise Cmd2ArgparseError
else:
- # Add statement to Namespace and a getter function for it
+ # Add statement to Namespace as __statement__ (this is deprecated and will be removed in 2.0)
setattr(ns, constants.NS_ATTR_STATEMENT, statement)
- setattr(ns, 'get_statement', lambda: statement)
- # 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)
+ # Add wrapped statement to Namespace as cmd2_statement
+ setattr(ns, 'cmd2_statement', Cmd2AttributeWrapper(statement))
+
+ # Add wrapped subcmd handler (which can be None) to Namespace as cmd2_handler
+ handler = getattr(ns, constants.NS_ATTR_SUBCMD_HANDLER, None)
+ setattr(ns, 'cmd2_handler', Cmd2AttributeWrapper(handler))
+
+ # Remove the subcmd handler attribute from the Namespace
+ # since cmd2_handler is how a developer accesses it.
+ if hasattr(ns, constants.NS_ATTR_SUBCMD_HANDLER):
+ delattr(ns, constants.NS_ATTR_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 7ce8c815..06f48f82 100644
--- a/docs/features/argument_processing.rst
+++ b/docs/features/argument_processing.rst
@@ -13,9 +13,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`` and can also be retrieved by calling ``get_statement()``
- on the ``Namespace``.
+ parsing the command line. It can be retrieved by calling
+ ``cmd2_statement.get()`` on the ``Namespace``.
4. Adds the usage message from the argument parser to your command.
@@ -391,10 +390,13 @@ Reserved Argument Names
Namespaces. To avoid naming collisions, do not use any of the names for your
argparse arguments.
+- ``cmd2_statement`` - ``cmd2.Cmd2AttributeWrapper`` object containing
+ ``cmd2.Statement`` object that was created when parsing the command line.
- ``__statement__`` - ``cmd2.Statement`` object that was created when parsing
- the command line.
-- ``get_statement()`` - convenience function which returns the ``Statement``
-- ``__subcmd_handler__`` - points to subcommand handler function. This is added
- when using the ``@cmd2.as_subcommand_to`` decorator.
-- ``get_handler()`` - convenience function which returns the subcommand handler
- or ``None`` if one was not set
+ the command line. (This is deprecated and will be removed in 2.0.0.) Use
+ ``cmd2_statement`` instead.
+
+- ``__subcmd_handler__`` - used by cmd2 to identify the handler for a
+ subcommand created with ``@cmd2.as_subcommand_to`` decorator.
+- ``cmd2_handler`` - ``cmd2.Cmd2AttributeWrapper`` object containing
+ a subcommand handler function or ``None`` if one was not set.
diff --git a/docs/features/modular_commands.rst b/docs/features/modular_commands.rst
index dddd996e..4abeda2d 100644
--- a/docs/features/modular_commands.rst
+++ b/docs/features/modular_commands.rst
@@ -316,7 +316,7 @@ command and each CommandSet
@with_argparser(cut_parser)
def do_cut(self, ns: argparse.Namespace):
- handler = ns.get_handler()
+ handler = ns.cmd2_handler.get()
if handler is not None:
# Call whatever subcommand function was selected
handler(ns)
diff --git a/docs/testing.rst b/docs/testing.rst
index 811e1137..43838c99 100644
--- a/docs/testing.rst
+++ b/docs/testing.rst
@@ -38,7 +38,7 @@ this, you should always mock with `Autospeccing <python_mock_autospeccing_>`_ or
enabled.
Example of spec=True
-====================
+~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
def test_mocked_methods():
diff --git a/examples/decorator_example.py b/examples/decorator_example.py
index 5b721da6..1b6d7570 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.get_statement()
+ statement = args.cmd2_statement.get()
self.poutput("The command line you ran was: {}".format(statement.command_and_args))
self.poutput("It generated this tag:")
diff --git a/examples/modular_subcommands.py b/examples/modular_subcommands.py
index 44d4edd8..0b1f4ed3 100644
--- a/examples/modular_subcommands.py
+++ b/examples/modular_subcommands.py
@@ -98,7 +98,7 @@ class ExampleApp(cmd2.Cmd):
@with_argparser(cut_parser)
def do_cut(self, ns: argparse.Namespace):
# Call handler for whatever subcommand was selected
- handler = ns.get_handler()
+ handler = ns.cmd2_handler.get()
if handler is not None:
handler(ns)
else:
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index 9806c9b5..20d05bed 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -289,7 +289,6 @@ class SubcommandApp(cmd2.Cmd):
func = getattr(args, 'func')
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')
@@ -297,7 +296,7 @@ class SubcommandApp(cmd2.Cmd):
@cmd2.with_argparser(has_subcmd_parser)
def do_test_subcmd_decorator(self, args: argparse.Namespace):
- handler = args.get_handler()
+ handler = args.cmd2_handler.get()
handler(args)
subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="The subcommand")
@@ -306,7 +305,7 @@ class SubcommandApp(cmd2.Cmd):
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)
+ self.poutput(args)
@pytest.fixture
def subcommand_app():
diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py
index 3ce90118..e2b3bb97 100644
--- a/tests/test_argparse_custom.py
+++ b/tests/test_argparse_custom.py
@@ -267,3 +267,13 @@ def test_apcustom_metavar_tuple():
parser = Cmd2ArgumentParser()
parser.add_argument('--aflag', nargs=2, metavar=('foo', 'bar'), help='This is a test')
assert '[--aflag foo bar]' in parser.format_help()
+
+
+def test_cmd2_attribute_wrapper():
+ initial_val = 5
+ wrapper = cmd2.Cmd2AttributeWrapper(initial_val)
+ assert wrapper.get() == initial_val
+
+ new_val = 22
+ wrapper.set(new_val)
+ assert wrapper.get() == new_val
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index d40a9b61..279f2f79 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.get_statement())
+ self.poutput(namespace.cmd2_statement.get())
###
#
diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py
index 6856881a..939aa5b4 100644
--- a/tests_isolated/test_commandset/test_commandset.py
+++ b/tests_isolated/test_commandset/test_commandset.py
@@ -72,7 +72,7 @@ class CommandSetA(CommandSetBase):
@cmd2.with_argparser(main_parser)
def do_main(self, args: argparse.Namespace) -> None:
# Call handler for whatever subcommand was selected
- handler = args.get_handler()
+ handler = args.cmd2_handler.get()
handler(args)
# main -> sub
@@ -309,7 +309,7 @@ class LoadableBase(cmd2.CommandSet):
@cmd2.with_argparser(cut_parser)
def do_cut(self, ns: argparse.Namespace):
"""Cut something"""
- handler = ns.get_handler()
+ handler = ns.cmd2_handler.get()
if handler is not None:
# Call whatever subcommand function was selected
handler(ns)
@@ -330,7 +330,7 @@ class LoadableBase(cmd2.CommandSet):
self._cmd.poutput('Need to cut before stirring')
return
- handler = ns.get_handler()
+ handler = ns.cmd2_handler.get()
if handler is not None:
# Call whatever subcommand function was selected
handler(ns)
@@ -345,7 +345,7 @@ class LoadableBase(cmd2.CommandSet):
@cmd2.as_subcommand_to('stir', 'pasta', stir_pasta_parser)
def stir_pasta(self, ns: argparse.Namespace):
- handler = ns.get_handler()
+ handler = ns.cmd2_handler.get()
if handler is not None:
# Call whatever subcommand function was selected
handler(ns)
@@ -360,7 +360,7 @@ class LoadableBadBase(cmd2.CommandSet):
def do_cut(self, ns: argparse.Namespace):
"""Cut something"""
- handler = ns.get_handler()
+ handler = ns.cmd2_handler.get()
if handler is not None:
# Call whatever subcommand function was selected
handler(ns)
@@ -598,7 +598,7 @@ class AppWithSubCommands(cmd2.Cmd):
@cmd2.with_argparser(cut_parser)
def do_cut(self, ns: argparse.Namespace):
"""Cut something"""
- handler = ns.get_handler()
+ handler = ns.cmd2_handler.get()
if handler is not None:
# Call whatever subcommand function was selected
handler(ns)