summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md5
-rw-r--r--cmd2/cmd2.py46
-rw-r--r--cmd2/command_definition.py6
-rw-r--r--tests_isolated/test_commandset/test_commandset.py39
4 files changed, 81 insertions, 15 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index de57031d..2ba8c53c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,11 @@
-## 1.3.2 (August 7, 2020)
+## 1.3.2 (August 10, 2020)
* Bug Fixes
* Fixed `prog` value of subcommands added with `as_subcommand_to()` decorator.
* Fixed missing settings in subcommand parsers created with `as_subcommand_to()` decorator. These settings
include things like description and epilog text.
+ * Fixed issue with CommandSet auto-discovery only searching direct sub-classes
+* Enhancements
+ * Added functions to fetch registered CommandSets by type and command name
## 1.3.1 (August 6, 2020)
* Bug Fixes
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 975f4635..39feac68 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -406,18 +406,47 @@ class Cmd(cmd.Cmd):
self._register_subcommands(self)
+ def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> List[CommandSet]:
+ """
+ Find all CommandSets that match the provided CommandSet type.
+ By default, locates a CommandSet that is an exact type match but may optionally return all CommandSets that
+ are sub-classes of the provided type
+ :param commandset_type: CommandSet sub-class type to search for
+ :param subclass_match: If True, return all sub-classes of provided type, otherwise only search for exact match
+ :return: Matching CommandSets
+ """
+ return [cmdset for cmdset in self._installed_command_sets
+ if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type))]
+
+ def find_commandset_for_command(self, command_name: str) -> Optional[CommandSet]:
+ """
+ Finds the CommandSet that registered the command name
+ :param command_name: command name to search
+ :return: CommandSet that provided the command
+ """
+ return self._cmd_to_command_sets.get(command_name)
+
def _autoload_commands(self) -> None:
"""Load modular command definitions."""
- # Search for all subclasses of CommandSet, instantiate them if they weren't provided in the constructor
+ # Search for all subclasses of CommandSet, instantiate them if they weren't already provided in the constructor
all_commandset_defs = CommandSet.__subclasses__()
existing_commandset_types = [type(command_set) for command_set in self._installed_command_sets]
- for cmdset_type in all_commandset_defs:
- init_sig = inspect.signature(cmdset_type.__init__)
- if not (cmdset_type in existing_commandset_types
- or len(init_sig.parameters) != 1
- or 'self' not in init_sig.parameters):
- cmdset = cmdset_type()
- self.install_command_set(cmdset)
+
+ def load_commandset_by_type(commandset_types: List[Type]) -> None:
+ for cmdset_type in commandset_types:
+ # check if the type has sub-classes. We will only auto-load leaf class types.
+ subclasses = cmdset_type.__subclasses__()
+ if subclasses:
+ load_commandset_by_type(subclasses)
+ else:
+ init_sig = inspect.signature(cmdset_type.__init__)
+ if not (cmdset_type in existing_commandset_types
+ or len(init_sig.parameters) != 1
+ or 'self' not in init_sig.parameters):
+ cmdset = cmdset_type()
+ self.install_command_set(cmdset)
+
+ load_commandset_by_type(all_commandset_defs)
def install_command_set(self, cmdset: CommandSet) -> None:
"""
@@ -471,6 +500,7 @@ class Cmd(cmd.Cmd):
if cmdset in self._cmd_to_command_sets.values():
self._cmd_to_command_sets = \
{key: val for key, val in self._cmd_to_command_sets.items() if val is not cmdset}
+ cmdset.on_unregister(self)
raise
def _install_command_function(self, command: str, command_wrapper: Callable, context=''):
diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py
index 1858c80b..22d8915e 100644
--- a/cmd2/command_definition.py
+++ b/cmd2/command_definition.py
@@ -6,6 +6,7 @@ import functools
from typing import Callable, Iterable, Optional, Type
from .constants import COMMAND_FUNC_PREFIX
+from .exceptions import CommandSetRegistrationError
# Allows IDEs to resolve types without impacting imports at runtime, breaking circular dependency issues
try: # pragma: no cover
@@ -92,7 +93,10 @@ class CommandSet(object):
:param cmd: The cmd2 main application
:type cmd: cmd2.Cmd
"""
- self._cmd = cmd
+ if self._cmd is None:
+ self._cmd = cmd
+ else:
+ raise CommandSetRegistrationError('This CommandSet has already been registered')
def on_unregister(self, cmd):
"""
diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py
index 43947071..813f9183 100644
--- a/tests_isolated/test_commandset/test_commandset.py
+++ b/tests_isolated/test_commandset/test_commandset.py
@@ -15,8 +15,12 @@ from .conftest import complete_tester, WithCommandSets
from cmd2.exceptions import CommandSetRegistrationError
+class CommandSetBase(cmd2.CommandSet):
+ pass
+
+
@cmd2.with_default_category('Fruits')
-class CommandSetA(cmd2.CommandSet):
+class CommandSetA(CommandSetBase):
def do_apple(self, cmd: cmd2.Cmd, statement: cmd2.Statement):
cmd.poutput('Apple!')
@@ -60,7 +64,7 @@ class CommandSetA(cmd2.CommandSet):
@cmd2.with_default_category('Command Set B')
-class CommandSetB(cmd2.CommandSet):
+class CommandSetB(CommandSetBase):
def __init__(self, arg1):
super().__init__()
self._arg1 = arg1
@@ -95,8 +99,8 @@ def test_autoload_commands(command_sets_app):
def test_custom_construct_commandsets():
# Verifies that a custom initialized CommandSet loads correctly when passed into the constructor
- command_set = CommandSetB('foo')
- app = WithCommandSets(command_sets=[command_set])
+ command_set_b = CommandSetB('foo')
+ app = WithCommandSets(command_sets=[command_set_b])
cmds_cats, cmds_doc, cmds_undoc, help_topics = app._build_command_info()
assert 'Command Set B' in cmds_cats
@@ -107,16 +111,41 @@ def test_custom_construct_commandsets():
assert app.install_command_set(command_set_2)
# Verify that autoload doesn't conflict with a manually loaded CommandSet that could be autoloaded.
- app2 = WithCommandSets(command_sets=[CommandSetA()])
+ command_set_a = CommandSetA()
+ app2 = WithCommandSets(command_sets=[command_set_a])
+
+ with pytest.raises(CommandSetRegistrationError):
+ app2.install_command_set(command_set_b)
+
+ app.uninstall_command_set(command_set_b)
+
+ app2.install_command_set(command_set_b)
+
assert hasattr(app2, 'do_apple')
+ assert hasattr(app2, 'do_aardvark')
+
+ assert app2.find_commandset_for_command('aardvark') is command_set_b
+ assert app2.find_commandset_for_command('apple') is command_set_a
+
+ matches = app2.find_commandsets(CommandSetBase, subclass_match=True)
+ assert command_set_a in matches
+ assert command_set_b in matches
+ assert command_set_2 not in matches
def test_load_commands(command_sets_manual):
# now install a command set and verify the commands are now present
cmd_set = CommandSetA()
+
+ assert command_sets_manual.find_commandset_for_command('elderberry') is None
+ assert not command_sets_manual.find_commandsets(CommandSetA)
+
command_sets_manual.install_command_set(cmd_set)
+ assert command_sets_manual.find_commandsets(CommandSetA)[0] is cmd_set
+ assert command_sets_manual.find_commandset_for_command('elderberry') is cmd_set
+
cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
assert 'Alone' in cmds_cats