diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-08-19 21:07:44 -0400 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-08-19 21:38:22 -0400 |
commit | 30d010f62196ff082bf243a4c460517cb70c70f2 (patch) | |
tree | f51eee6bde1a2f1a7f3a264a6e67a3bd1743d469 | |
parent | 5dd2d03ef35a3d33ff53d82c8039d68e263246ee (diff) | |
download | cmd2-git-30d010f62196ff082bf243a4c460517cb70c70f2.tar.gz |
Fixed AttributeError when CommandSet that uses as_subcommand_to decorator is loaded during cmd2.Cmd.__init__().
-rw-r--r-- | CHANGELOG.md | 5 | ||||
-rw-r--r-- | cmd2/cmd2.py | 38 | ||||
-rw-r--r-- | tests_isolated/test_commandset/test_commandset.py | 35 |
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'] |