summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md5
-rw-r--r--cmd2/cmd2.py38
-rw-r--r--tests_isolated/test_commandset/test_commandset.py35
3 files changed, 62 insertions, 16 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd8d29ba..292c9115 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.3.4 (TBD)
+* Bug Fixes
+ * Fixed `AttributeError` when `CommandSet` that uses `as_subcommand_to` decorator is loaded during
+ `cmd2.Cmd.__init__()`.
+
## 1.3.3 (August 13, 2020)
* Breaking changes
* CommandSet command functions (do_, complete_, help_) will no longer have the cmd2 app
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 610ce4a3..adf797bf 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -259,22 +259,6 @@ class Cmd(cmd.Cmd):
multiline_commands=multiline_commands,
shortcuts=shortcuts)
- # Load modular commands
- self._installed_command_sets = [] # type: List[CommandSet]
- self._cmd_to_command_sets = {} # type: Dict[str, CommandSet]
- if command_sets:
- for command_set in command_sets:
- self.register_command_set(command_set)
-
- if auto_load_commands:
- self._autoload_commands()
-
- # Verify commands don't have invalid names (like starting with a shortcut)
- for cur_cmd in self.get_all_commands():
- valid, errmsg = self.statement_parser.is_valid_command(cur_cmd)
- if not valid:
- raise ValueError("Invalid command name {!r}: {}".format(cur_cmd, errmsg))
-
# Stores results from the last command run to enable usage of results in a Python script or interactive console
# Built-in commands don't make use of this. It is purely there for user-defined commands and convenience.
self.last_result = None
@@ -412,6 +396,28 @@ class Cmd(cmd.Cmd):
# If False, then complete() will sort the matches using self.default_sort_key before they are displayed.
self.matches_sorted = False
+ ############################################################################################################
+ # The following code block loads CommandSets, verifies command names, and registers subcommands.
+ # This block should appear after all attributes have been created since the registration code
+ # depends on them and it's possible a module's on_register() method may need to access some.
+ ############################################################################################################
+ # Load modular commands
+ self._installed_command_sets = [] # type: List[CommandSet]
+ self._cmd_to_command_sets = {} # type: Dict[str, CommandSet]
+ if command_sets:
+ for command_set in command_sets:
+ self.register_command_set(command_set)
+
+ if auto_load_commands:
+ self._autoload_commands()
+
+ # Verify commands don't have invalid names (like starting with a shortcut)
+ for cur_cmd in self.get_all_commands():
+ valid, errmsg = self.statement_parser.is_valid_command(cur_cmd)
+ if not valid:
+ raise ValueError("Invalid command name {!r}: {}".format(cur_cmd, errmsg))
+
+ # Add functions decorated to be subcommands
self._register_subcommands(self)
def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> List[CommandSet]:
diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py
index bab5d536..ffeadf75 100644
--- a/tests_isolated/test_commandset/test_commandset.py
+++ b/tests_isolated/test_commandset/test_commandset.py
@@ -62,6 +62,26 @@ class CommandSetA(CommandSetBase):
self._cmd.poutput('Elderberry {}!!'.format(ns.arg1))
self._cmd.last_result = {'arg1': ns.arg1}
+ # Test that CommandSet with as_subcommand_to decorator successfully loads
+ # during `cmd2.Cmd.__init__()`.
+ main_parser = cmd2.Cmd2ArgumentParser(description="Main Command")
+ main_subparsers = main_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
+ main_subparsers.required = True
+
+ @cmd2.with_category('Alone')
+ @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)
+
+ # main -> sub
+ subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="Sub Command")
+
+ @cmd2.as_subcommand_to('main', 'sub', subcmd_parser, help="sub command")
+ def subcmd_func(self, args: argparse.Namespace) -> None:
+ self._cmd.poutput("Subcommand Ran")
+
@cmd2.with_default_category('Command Set B')
class CommandSetB(CommandSetBase):
@@ -87,6 +107,11 @@ def test_autoload_commands(command_sets_app):
assert 'Alone' in cmds_cats
assert 'elderberry' in cmds_cats['Alone']
+ assert 'main' in cmds_cats['Alone']
+
+ # Test subcommand was autoloaded
+ result = command_sets_app.app_cmd('main sub')
+ assert 'Subcommand Ran' in result.stdout
assert 'Also Alone' in cmds_cats
assert 'durian' in cmds_cats['Also Alone']
@@ -150,6 +175,11 @@ def test_load_commands(command_sets_manual):
assert 'Alone' in cmds_cats
assert 'elderberry' in cmds_cats['Alone']
+ assert 'main' in cmds_cats['Alone']
+
+ # Test subcommand was loaded
+ result = command_sets_manual.app_cmd('main sub')
+ assert 'Subcommand Ran' in result.stdout
assert 'Fruits' in cmds_cats
assert 'cranberry' in cmds_cats['Fruits']
@@ -172,6 +202,11 @@ def test_load_commands(command_sets_manual):
assert 'Alone' in cmds_cats
assert 'elderberry' in cmds_cats['Alone']
+ assert 'main' in cmds_cats['Alone']
+
+ # Test subcommand was loaded
+ result = command_sets_manual.app_cmd('main sub')
+ assert 'Subcommand Ran' in result.stdout
assert 'Fruits' in cmds_cats
assert 'cranberry' in cmds_cats['Fruits']