Skip to content

Commit 6c939fb

Browse files
committed
Will now traverse down CommandSet inheritance tree to find all leaf descendants.
CommandSet now has a check to ensure it is only registered with one cmd2.Cmd instance at a time. Adds function to find command set by type and by command name
1 parent 2674341 commit 6c939fb

File tree

4 files changed

+81
-15
lines changed

4 files changed

+81
-15
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
## 1.3.2 (August 7, 2020)
1+
## 1.3.2 (August 10, 2020)
22
* Bug Fixes
33
* Fixed `prog` value of subcommands added with `as_subcommand_to()` decorator.
44
* Fixed missing settings in subcommand parsers created with `as_subcommand_to()` decorator. These settings
55
include things like description and epilog text.
6+
* Fixed issue with CommandSet auto-discovery only searching direct sub-classes
7+
* Enhancements
8+
* Added functions to fetch registered CommandSets by type and command name
69

710
## 1.3.1 (August 6, 2020)
811
* Bug Fixes

cmd2/cmd2.py

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -406,18 +406,47 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
406406

407407
self._register_subcommands(self)
408408

409+
def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> List[CommandSet]:
410+
"""
411+
Find all CommandSets that match the provided CommandSet type.
412+
By default, locates a CommandSet that is an exact type match but may optionally return all CommandSets that
413+
are sub-classes of the provided type
414+
:param commandset_type: CommandSet sub-class type to search for
415+
:param subclass_match: If True, return all sub-classes of provided type, otherwise only search for exact match
416+
:return: Matching CommandSets
417+
"""
418+
return [cmdset for cmdset in self._installed_command_sets
419+
if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type))]
420+
421+
def find_commandset_for_command(self, command_name: str) -> Optional[CommandSet]:
422+
"""
423+
Finds the CommandSet that registered the command name
424+
:param command_name: command name to search
425+
:return: CommandSet that provided the command
426+
"""
427+
return self._cmd_to_command_sets.get(command_name)
428+
409429
def _autoload_commands(self) -> None:
410430
"""Load modular command definitions."""
411-
# Search for all subclasses of CommandSet, instantiate them if they weren't provided in the constructor
431+
# Search for all subclasses of CommandSet, instantiate them if they weren't already provided in the constructor
412432
all_commandset_defs = CommandSet.__subclasses__()
413433
existing_commandset_types = [type(command_set) for command_set in self._installed_command_sets]
414-
for cmdset_type in all_commandset_defs:
415-
init_sig = inspect.signature(cmdset_type.__init__)
416-
if not (cmdset_type in existing_commandset_types
417-
or len(init_sig.parameters) != 1
418-
or 'self' not in init_sig.parameters):
419-
cmdset = cmdset_type()
420-
self.install_command_set(cmdset)
434+
435+
def load_commandset_by_type(commandset_types: List[Type]) -> None:
436+
for cmdset_type in commandset_types:
437+
# check if the type has sub-classes. We will only auto-load leaf class types.
438+
subclasses = cmdset_type.__subclasses__()
439+
if subclasses:
440+
load_commandset_by_type(subclasses)
441+
else:
442+
init_sig = inspect.signature(cmdset_type.__init__)
443+
if not (cmdset_type in existing_commandset_types
444+
or len(init_sig.parameters) != 1
445+
or 'self' not in init_sig.parameters):
446+
cmdset = cmdset_type()
447+
self.install_command_set(cmdset)
448+
449+
load_commandset_by_type(all_commandset_defs)
421450

422451
def install_command_set(self, cmdset: CommandSet) -> None:
423452
"""
@@ -471,6 +500,7 @@ def install_command_set(self, cmdset: CommandSet) -> None:
471500
if cmdset in self._cmd_to_command_sets.values():
472501
self._cmd_to_command_sets = \
473502
{key: val for key, val in self._cmd_to_command_sets.items() if val is not cmdset}
503+
cmdset.on_unregister(self)
474504
raise
475505

476506
def _install_command_function(self, command: str, command_wrapper: Callable, context=''):

cmd2/command_definition.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Callable, Iterable, Optional, Type
77

88
from .constants import COMMAND_FUNC_PREFIX
9+
from .exceptions import CommandSetRegistrationError
910

1011
# Allows IDEs to resolve types without impacting imports at runtime, breaking circular dependency issues
1112
try: # pragma: no cover
@@ -92,7 +93,10 @@ def on_register(self, cmd):
9293
:param cmd: The cmd2 main application
9394
:type cmd: cmd2.Cmd
9495
"""
95-
self._cmd = cmd
96+
if self._cmd is None:
97+
self._cmd = cmd
98+
else:
99+
raise CommandSetRegistrationError('This CommandSet has already been registered')
96100

97101
def on_unregister(self, cmd):
98102
"""

tests_isolated/test_commandset/test_commandset.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515
from cmd2.exceptions import CommandSetRegistrationError
1616

1717

18+
class CommandSetBase(cmd2.CommandSet):
19+
pass
20+
21+
1822
@cmd2.with_default_category('Fruits')
19-
class CommandSetA(cmd2.CommandSet):
23+
class CommandSetA(CommandSetBase):
2024
def do_apple(self, cmd: cmd2.Cmd, statement: cmd2.Statement):
2125
cmd.poutput('Apple!')
2226

@@ -60,7 +64,7 @@ def do_elderberry(self, cmd: cmd2.Cmd, ns: argparse.Namespace):
6064

6165

6266
@cmd2.with_default_category('Command Set B')
63-
class CommandSetB(cmd2.CommandSet):
67+
class CommandSetB(CommandSetBase):
6468
def __init__(self, arg1):
6569
super().__init__()
6670
self._arg1 = arg1
@@ -95,8 +99,8 @@ def test_autoload_commands(command_sets_app):
9599

96100
def test_custom_construct_commandsets():
97101
# Verifies that a custom initialized CommandSet loads correctly when passed into the constructor
98-
command_set = CommandSetB('foo')
99-
app = WithCommandSets(command_sets=[command_set])
102+
command_set_b = CommandSetB('foo')
103+
app = WithCommandSets(command_sets=[command_set_b])
100104

101105
cmds_cats, cmds_doc, cmds_undoc, help_topics = app._build_command_info()
102106
assert 'Command Set B' in cmds_cats
@@ -107,16 +111,41 @@ def test_custom_construct_commandsets():
107111
assert app.install_command_set(command_set_2)
108112

109113
# Verify that autoload doesn't conflict with a manually loaded CommandSet that could be autoloaded.
110-
app2 = WithCommandSets(command_sets=[CommandSetA()])
114+
command_set_a = CommandSetA()
115+
app2 = WithCommandSets(command_sets=[command_set_a])
116+
117+
with pytest.raises(CommandSetRegistrationError):
118+
app2.install_command_set(command_set_b)
119+
120+
app.uninstall_command_set(command_set_b)
121+
122+
app2.install_command_set(command_set_b)
123+
111124
assert hasattr(app2, 'do_apple')
125+
assert hasattr(app2, 'do_aardvark')
126+
127+
assert app2.find_commandset_for_command('aardvark') is command_set_b
128+
assert app2.find_commandset_for_command('apple') is command_set_a
129+
130+
matches = app2.find_commandsets(CommandSetBase, subclass_match=True)
131+
assert command_set_a in matches
132+
assert command_set_b in matches
133+
assert command_set_2 not in matches
112134

113135

114136
def test_load_commands(command_sets_manual):
115137

116138
# now install a command set and verify the commands are now present
117139
cmd_set = CommandSetA()
140+
141+
assert command_sets_manual.find_commandset_for_command('elderberry') is None
142+
assert not command_sets_manual.find_commandsets(CommandSetA)
143+
118144
command_sets_manual.install_command_set(cmd_set)
119145

146+
assert command_sets_manual.find_commandsets(CommandSetA)[0] is cmd_set
147+
assert command_sets_manual.find_commandset_for_command('elderberry') is cmd_set
148+
120149
cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
121150

122151
assert 'Alone' in cmds_cats

0 commit comments

Comments
 (0)