Skip to content

Commit 30d010f

Browse files
committed
Fixed AttributeError when CommandSet that uses as_subcommand_to decorator is loaded during cmd2.Cmd.__init__().
1 parent 5dd2d03 commit 30d010f

File tree

3 files changed

+62
-16
lines changed

3 files changed

+62
-16
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 1.3.4 (TBD)
2+
* Bug Fixes
3+
* Fixed `AttributeError` when `CommandSet` that uses `as_subcommand_to` decorator is loaded during
4+
`cmd2.Cmd.__init__()`.
5+
16
## 1.3.3 (August 13, 2020)
27
* Breaking changes
38
* CommandSet command functions (do_, complete_, help_) will no longer have the cmd2 app

cmd2/cmd2.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -259,22 +259,6 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
259259
multiline_commands=multiline_commands,
260260
shortcuts=shortcuts)
261261

262-
# Load modular commands
263-
self._installed_command_sets = [] # type: List[CommandSet]
264-
self._cmd_to_command_sets = {} # type: Dict[str, CommandSet]
265-
if command_sets:
266-
for command_set in command_sets:
267-
self.register_command_set(command_set)
268-
269-
if auto_load_commands:
270-
self._autoload_commands()
271-
272-
# Verify commands don't have invalid names (like starting with a shortcut)
273-
for cur_cmd in self.get_all_commands():
274-
valid, errmsg = self.statement_parser.is_valid_command(cur_cmd)
275-
if not valid:
276-
raise ValueError("Invalid command name {!r}: {}".format(cur_cmd, errmsg))
277-
278262
# Stores results from the last command run to enable usage of results in a Python script or interactive console
279263
# Built-in commands don't make use of this. It is purely there for user-defined commands and convenience.
280264
self.last_result = None
@@ -412,6 +396,28 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
412396
# If False, then complete() will sort the matches using self.default_sort_key before they are displayed.
413397
self.matches_sorted = False
414398

399+
############################################################################################################
400+
# The following code block loads CommandSets, verifies command names, and registers subcommands.
401+
# This block should appear after all attributes have been created since the registration code
402+
# depends on them and it's possible a module's on_register() method may need to access some.
403+
############################################################################################################
404+
# Load modular commands
405+
self._installed_command_sets = [] # type: List[CommandSet]
406+
self._cmd_to_command_sets = {} # type: Dict[str, CommandSet]
407+
if command_sets:
408+
for command_set in command_sets:
409+
self.register_command_set(command_set)
410+
411+
if auto_load_commands:
412+
self._autoload_commands()
413+
414+
# Verify commands don't have invalid names (like starting with a shortcut)
415+
for cur_cmd in self.get_all_commands():
416+
valid, errmsg = self.statement_parser.is_valid_command(cur_cmd)
417+
if not valid:
418+
raise ValueError("Invalid command name {!r}: {}".format(cur_cmd, errmsg))
419+
420+
# Add functions decorated to be subcommands
415421
self._register_subcommands(self)
416422

417423
def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> List[CommandSet]:

tests_isolated/test_commandset/test_commandset.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,26 @@ def do_elderberry(self, ns: argparse.Namespace):
6262
self._cmd.poutput('Elderberry {}!!'.format(ns.arg1))
6363
self._cmd.last_result = {'arg1': ns.arg1}
6464

65+
# Test that CommandSet with as_subcommand_to decorator successfully loads
66+
# during `cmd2.Cmd.__init__()`.
67+
main_parser = cmd2.Cmd2ArgumentParser(description="Main Command")
68+
main_subparsers = main_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
69+
main_subparsers.required = True
70+
71+
@cmd2.with_category('Alone')
72+
@cmd2.with_argparser(main_parser)
73+
def do_main(self, args: argparse.Namespace) -> None:
74+
# Call handler for whatever subcommand was selected
75+
handler = args.get_handler()
76+
handler(args)
77+
78+
# main -> sub
79+
subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="Sub Command")
80+
81+
@cmd2.as_subcommand_to('main', 'sub', subcmd_parser, help="sub command")
82+
def subcmd_func(self, args: argparse.Namespace) -> None:
83+
self._cmd.poutput("Subcommand Ran")
84+
6585

6686
@cmd2.with_default_category('Command Set B')
6787
class CommandSetB(CommandSetBase):
@@ -87,6 +107,11 @@ def test_autoload_commands(command_sets_app):
87107

88108
assert 'Alone' in cmds_cats
89109
assert 'elderberry' in cmds_cats['Alone']
110+
assert 'main' in cmds_cats['Alone']
111+
112+
# Test subcommand was autoloaded
113+
result = command_sets_app.app_cmd('main sub')
114+
assert 'Subcommand Ran' in result.stdout
90115

91116
assert 'Also Alone' in cmds_cats
92117
assert 'durian' in cmds_cats['Also Alone']
@@ -150,6 +175,11 @@ def test_load_commands(command_sets_manual):
150175

151176
assert 'Alone' in cmds_cats
152177
assert 'elderberry' in cmds_cats['Alone']
178+
assert 'main' in cmds_cats['Alone']
179+
180+
# Test subcommand was loaded
181+
result = command_sets_manual.app_cmd('main sub')
182+
assert 'Subcommand Ran' in result.stdout
153183

154184
assert 'Fruits' in cmds_cats
155185
assert 'cranberry' in cmds_cats['Fruits']
@@ -172,6 +202,11 @@ def test_load_commands(command_sets_manual):
172202

173203
assert 'Alone' in cmds_cats
174204
assert 'elderberry' in cmds_cats['Alone']
205+
assert 'main' in cmds_cats['Alone']
206+
207+
# Test subcommand was loaded
208+
result = command_sets_manual.app_cmd('main sub')
209+
assert 'Subcommand Ran' in result.stdout
175210

176211
assert 'Fruits' in cmds_cats
177212
assert 'cranberry' in cmds_cats['Fruits']

0 commit comments

Comments
 (0)