Skip to content

Added Cmd2AttributeWrapper class #985

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
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
Expand Down
20 changes: 19 additions & 1 deletion cmd2/argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def my_completer_method(self, text, line, begidx, endidx, arg_tokens)
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

Expand Down Expand Up @@ -904,6 +904,24 @@ def _print_message(self, message, file=None):
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

Expand Down
4 changes: 2 additions & 2 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2685,7 +2685,7 @@ def _cmdloop(self) -> None:
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
Expand Down Expand Up @@ -2812,7 +2812,7 @@ def _alias_list(self, args: argparse.Namespace) -> None:
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
Expand Down
36 changes: 22 additions & 14 deletions cmd2/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -298,13 +299,20 @@ def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]:
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)
Expand Down
20 changes: 11 additions & 9 deletions docs/features/argument_processing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docs/features/modular_commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
2 changes: 1 addition & 1 deletion examples/decorator_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def do_speak(self, args: argparse.Namespace):
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:")
Expand Down
2 changes: 1 addition & 1 deletion examples/modular_subcommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def do_unload(self, ns: argparse.Namespace):
@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:
Expand Down
5 changes: 2 additions & 3 deletions tests/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,15 +289,14 @@ def do_base(self, args):
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')
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.cmd2_handler.get()
handler(args)

subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="The subcommand")
Expand All @@ -306,7 +305,7 @@ def do_test_subcmd_decorator(self, args: argparse.Namespace):
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():
Expand Down
10 changes: 10 additions & 0 deletions tests/test_argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def do_skip_postcmd_hooks(self, _):
@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())

###
#
Expand Down
12 changes: 6 additions & 6 deletions tests_isolated/test_commandset/test_commandset.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def do_elderberry(self, ns: argparse.Namespace):
@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
Expand Down Expand Up @@ -309,7 +309,7 @@ def namespace_provider(self) -> argparse.Namespace:
@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)
Expand All @@ -330,7 +330,7 @@ def do_stir(self, ns: argparse.Namespace):
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)
Expand All @@ -345,7 +345,7 @@ def do_stir(self, ns: argparse.Namespace):

@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)
Expand All @@ -360,7 +360,7 @@ def __init__(self, dummy):

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)
Expand Down Expand Up @@ -598,7 +598,7 @@ def __init__(self, *args, **kwargs):
@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)
Expand Down