Skip to content

Commit ae7e67c

Browse files
authored
Merge pull request #984 from python-cmd2/recursion_error
Fixed RecursionError when printing an argparse.Namespace
2 parents a540cfc + 9b5a988 commit ae7e67c

File tree

8 files changed

+48
-17
lines changed

8 files changed

+48
-17
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 1.3.5 (TBD)
2+
* Bug Fixes
3+
* Fixed `RecursionError` when printing an `argparse.Namespace` caused by custom attribute cmd2 was adding
4+
* Enhancements
5+
* Added `get_statement()` function to `argparse.Namespace` which returns `__statement__` attribute
6+
17
## 1.3.4 (August 20, 2020)
28
* Bug Fixes
39
* Fixed `AttributeError` when `CommandSet` that uses `as_subcommand_to` decorator is loaded during

cmd2/cmd2.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,9 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
663663
raise CommandSetRegistrationError('Could not find argparser for command "{}" needed by subcommand: {}'
664664
.format(command_name, str(method)))
665665

666-
subcmd_parser.set_defaults(cmd2_handler=method)
666+
# Set the subcommand handler function
667+
defaults = {constants.NS_ATTR_SUBCMD_HANDLER: method}
668+
subcmd_parser.set_defaults(**defaults)
667669

668670
def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> argparse.ArgumentParser:
669671
if not subcmd_names:

cmd2/constants.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,6 @@
3737
# All command completer functions start with this
3838
COMPLETER_FUNC_PREFIX = 'complete_'
3939

40-
##############################################################################
41-
# The following are optional attributes added to do_* command functions
42-
##############################################################################
43-
4440
# The custom help category a command belongs to
4541
CMD_ATTR_HELP_CATEGORY = 'help_category'
4642

@@ -50,13 +46,14 @@
5046
# Whether or not tokens are unquoted before sending to argparse
5147
CMD_ATTR_PRESERVE_QUOTES = 'preserve_quotes'
5248

53-
# optional attribute
54-
SUBCMD_HANDLER = 'cmd2_handler'
55-
5649
# subcommand attributes for the base command name and the subcommand name
5750
SUBCMD_ATTR_COMMAND = 'parent_command'
5851
SUBCMD_ATTR_NAME = 'subcommand_name'
5952
SUBCMD_ATTR_ADD_PARSER_KWARGS = 'subcommand_add_parser_kwargs'
6053

6154
# arpparse attribute linking to command set instance
6255
PARSER_ATTR_COMMANDSET = 'command_set'
56+
57+
# custom attributes added to argparse Namespaces
58+
NS_ATTR_SUBCMD_HANDLER = '__subcmd_handler__'
59+
NS_ATTR_STATEMENT = '__statement__'

cmd2/decorators.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# coding=utf-8
22
"""Decorators for ``cmd2`` commands"""
33
import argparse
4-
import types
54
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
65

76
from . import constants
@@ -190,6 +189,7 @@ def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *,
190189
of unknown argument strings. A member called ``__statement__`` is added to the
191190
``Namespace`` to provide command functions access to the :class:`cmd2.Statement`
192191
object. This can be useful if the command function needs to know the command line.
192+
``__statement__`` can also be retrieved by calling ``get_statement()`` on the ``Namespace``.
193193
194194
:Example:
195195
@@ -228,6 +228,7 @@ def with_argparser(parser: argparse.ArgumentParser, *,
228228
:return: function that gets passed the argparse-parsed args in a Namespace
229229
A member called __statement__ is added to the Namespace to provide command functions access to the
230230
Statement object. This can be useful if the command function needs to know the command line.
231+
``__statement__`` can also be retrieved by calling ``get_statement()`` on the ``Namespace``.
231232
232233
:Example:
233234
@@ -297,12 +298,13 @@ def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]:
297298
except SystemExit:
298299
raise Cmd2ArgparseError
299300
else:
300-
setattr(ns, '__statement__', statement)
301+
# Add statement to Namespace and a getter function for it
302+
setattr(ns, constants.NS_ATTR_STATEMENT, statement)
303+
setattr(ns, 'get_statement', lambda: statement)
301304

302-
def get_handler(ns_self: argparse.Namespace) -> Optional[Callable]:
303-
return getattr(ns_self, constants.SUBCMD_HANDLER, None)
304-
305-
setattr(ns, 'get_handler', types.MethodType(get_handler, ns))
305+
# Add getter function for subcmd handler, which can be None
306+
subcmd_handler = getattr(ns, constants.NS_ATTR_SUBCMD_HANDLER, None)
307+
setattr(ns, 'get_handler', lambda: subcmd_handler)
306308

307309
args_list = _arg_swap(args, statement, *new_args)
308310
return func(*args_list, **kwargs)

docs/features/argument_processing.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ handles the following for you:
1414
3. Passes the resulting ``argparse.Namespace`` object to your command function.
1515
The ``Namespace`` includes the ``Statement`` object that was created when
1616
parsing the command line. It is stored in the ``__statement__`` attribute of
17-
the ``Namespace``.
17+
the ``Namespace`` and can also be retrieved by calling ``get_statement()``
18+
on the ``Namespace``.
1819

1920
4. Adds the usage message from the argument parser to your command.
2021

examples/decorator_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def do_speak(self, args: argparse.Namespace):
6767
def do_tag(self, args: argparse.Namespace):
6868
"""create an html tag"""
6969
# The Namespace always includes the Statement object created when parsing the command line
70-
statement = args.__statement__
70+
statement = args.get_statement()
7171

7272
self.poutput("The command line you ran was: {}".format(statement.command_and_args))
7373
self.poutput("It generated this tag:")

tests/test_argparse.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,24 @@ def do_base(self, args):
290290
func(self, args)
291291

292292

293+
# Add a subcommand using as_subcommand_to decorator
294+
has_subcmd_parser = cmd2.Cmd2ArgumentParser(description="Tests as_subcmd_to decorator")
295+
has_subcmd_subparsers = has_subcmd_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
296+
has_subcmd_subparsers.required = True
297+
298+
@cmd2.with_argparser(has_subcmd_parser)
299+
def do_test_subcmd_decorator(self, args: argparse.Namespace):
300+
handler = args.get_handler()
301+
handler(args)
302+
303+
subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="The subcommand")
304+
305+
@cmd2.as_subcommand_to('test_subcmd_decorator', 'subcmd', subcmd_parser, help='the subcommand')
306+
def subcmd_func(self, args: argparse.Namespace):
307+
# Make sure printing the Namespace works. The way we originally added get_hander()
308+
# to it resulted in a RecursionError when printing.
309+
print(args)
310+
293311
@pytest.fixture
294312
def subcommand_app():
295313
app = SubcommandApp()
@@ -373,6 +391,11 @@ def test_add_another_subcommand(subcommand_app):
373391
assert new_parser.prog == "base new_sub"
374392

375393

394+
def test_subcmd_decorator(subcommand_app):
395+
out, err = run_cmd(subcommand_app, 'test_subcmd_decorator subcmd')
396+
assert out[0].startswith('Namespace(')
397+
398+
376399
def test_unittest_mock():
377400
from unittest import mock
378401
from cmd2 import CommandSetRegistrationError

tests/test_plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def do_skip_postcmd_hooks(self, _):
279279
@with_argparser(parser)
280280
def do_argparse_cmd(self, namespace: argparse.Namespace):
281281
"""Repeat back the arguments"""
282-
self.poutput(namespace.__statement__)
282+
self.poutput(namespace.get_statement())
283283

284284
###
285285
#

0 commit comments

Comments
 (0)