summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2020-09-03 22:52:50 -0400
committerGitHub <noreply@github.com>2020-09-03 22:52:50 -0400
commite7bf6c193e7adb55986af730eb5bbd9facb182f7 (patch)
tree96e4fa1ac5cb57696aef2a0b454e0265dc5825c3
parent36b0b75265942fe375545beb7d2d8a2c5f6f63c4 (diff)
parent85ad31905780b19d2c16965110eda7577c057708 (diff)
downloadcmd2-git-e7bf6c193e7adb55986af730eb5bbd9facb182f7.tar.gz
Merge pull request #990 from python-cmd2/on_registered
Added callbacks to CommandSet
-rw-r--r--CHANGELOG.md10
-rw-r--r--cmd2/cmd2.py7
-rw-r--r--cmd2/command_definition.py28
-rw-r--r--docs/features/modular_commands.rst38
-rw-r--r--examples/modular_commands/commandset_complex.py4
-rw-r--r--examples/modular_commands_dynamic.py2
-rw-r--r--examples/modular_subcommands.py4
-rwxr-xr-xtests/test_cmd2.py8
-rw-r--r--tests_isolated/test_commandset/test_commandset.py41
9 files changed, 111 insertions, 31 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c65bb8a..fe9c1ca6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,13 @@
+## 1.3.9 (September 03, 2020)
+* Breaking Changes
+ * `CommandSet.on_unregister()` is now called as first step in unregistering a `CommandSet` and not
+ the last. `CommandSet.on_unregistered()` is now the last step.
+* Enhancements
+ * Added `CommandSet.on_registered()`. This is called by `cmd2.Cmd` after a `CommandSet` is registered
+ and all its commands have been added to the CLI.
+ * Added `CommandSet.on_unregistered()`. This is called by `cmd2.Cmd` after a `CommandSet` is unregistered
+ and all its commands have been removed from the CLI.
+
## 1.3.8 (August 28, 2020)
* Bug Fixes
* Fixed issue where subcommand added with `@as_subcommand_to` decorator did not display help
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index d768085a..f3a2d88d 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -503,7 +503,9 @@ class Cmd(cmd.Cmd):
self._installed_command_sets.append(cmdset)
self._register_subcommands(cmdset)
+ cmdset.on_registered()
except Exception:
+ cmdset.on_unregister()
for attrib in installed_attributes:
delattr(self, attrib)
if cmdset in self._installed_command_sets:
@@ -511,7 +513,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()
+ cmdset.on_unregistered()
raise
def _install_command_function(self, command: str, command_wrapper: Callable, context=''):
@@ -559,6 +561,7 @@ class Cmd(cmd.Cmd):
"""
if cmdset in self._installed_command_sets:
self._check_uninstallable(cmdset)
+ cmdset.on_unregister()
self._unregister_subcommands(cmdset)
methods = inspect.getmembers(
@@ -584,7 +587,7 @@ class Cmd(cmd.Cmd):
if hasattr(self, HELP_FUNC_PREFIX + cmd_name):
delattr(self, HELP_FUNC_PREFIX + cmd_name)
- cmdset.on_unregister()
+ cmdset.on_unregistered()
self._installed_command_sets.remove(cmdset)
def _check_uninstallable(self, cmdset: CommandSet):
diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py
index 27a044bc..64adaada 100644
--- a/cmd2/command_definition.py
+++ b/cmd2/command_definition.py
@@ -53,10 +53,11 @@ class CommandSet(object):
def __init__(self):
self._cmd = None # type: Optional[cmd2.Cmd]
- def on_register(self, cmd):
+ def on_register(self, cmd) -> None:
"""
- Called by cmd2.Cmd when a CommandSet is registered. Subclasses can override this
- to perform an initialization requiring access to the Cmd object.
+ Called by cmd2.Cmd as the first step to registering a CommandSet. The commands defined in this class have
+ not be added to the CLI object at this point. Subclasses can override this to perform any initialization
+ requiring access to the Cmd object (e.g. configure commands and their parsers based on CLI state data).
:param cmd: The cmd2 main application
:type cmd: cmd2.Cmd
@@ -66,11 +67,24 @@ class CommandSet(object):
else:
raise CommandSetRegistrationError('This CommandSet has already been registered')
- def on_unregister(self):
+ def on_registered(self) -> None:
"""
- Called by ``cmd2.Cmd`` when a CommandSet is unregistered and removed.
+ Called by cmd2.Cmd after a CommandSet is registered and all its commands have been added to the CLI.
+ Subclasses can override this to perform custom steps related to the newly added commands (e.g. setting
+ them to a disabled state).
+ """
+ pass
- :param cmd:
- :type cmd: cmd2.Cmd
+ def on_unregister(self) -> None:
+ """
+ Called by ``cmd2.Cmd`` as the first step to unregistering a CommandSet. Subclasses can override this to
+ perform any cleanup steps which require their commands being registered in the CLI.
+ """
+ pass
+
+ def on_unregistered(self) -> None:
+ """
+ Called by ``cmd2.Cmd`` after a CommandSet has been unregistered and all its commands removed from the CLI.
+ Subclasses can override this to perform remaining cleanup steps.
"""
self._cmd = None
diff --git a/docs/features/modular_commands.rst b/docs/features/modular_commands.rst
index 43779872..611660ff 100644
--- a/docs/features/modular_commands.rst
+++ b/docs/features/modular_commands.rst
@@ -4,7 +4,7 @@ Modular Commands
Overview
--------
-Cmd2 also enables developers to modularize their command definitions into Command Sets. Command sets represent
+Cmd2 also enables developers to modularize their command definitions into ``CommandSet`` objects. CommandSets represent
a logical grouping of commands within an cmd2 application. By default, all CommandSets will be discovered and loaded
automatically when the cmd2.Cmd class is instantiated with this mixin. This also enables the developer to
dynamically add/remove commands from the cmd2 application. This could be useful for loadable plugins that
@@ -21,10 +21,14 @@ Features
* Dynamically Loadable/Unloadable Commands - Command functions and CommandSets can both be loaded and unloaded
dynamically during application execution. This can enable features such as dynamically loaded modules that
add additional commands.
+* Events handlers - Four event handlers are provided in ``CommandSet`` class for custom initialization
+ and cleanup steps. See :ref:`features/modular_commands:Event Handlers`.
* Subcommand Injection - Subcommands can be defined separately from the base command. This allows for a more
action-centric instead of object-centric command system while still organizing your code and handlers around the
objects being managed.
+See API documentation for :attr:`cmd2.command_definition.CommandSet`
+
See the examples for more details: https://github.com/python-cmd2/cmd2/tree/master/plugins/command_sets/examples
@@ -171,7 +175,7 @@ You may need to disable command auto-loading if you need dynamically load comman
self._fruits = LoadableFruits()
self._vegetables = LoadableVegetables()
- load_parser = cmd2.Cmd2ArgumentParser('load')
+ load_parser = cmd2.Cmd2ArgumentParser()
load_parser.add_argument('cmds', choices=['fruits', 'vegetables'])
@with_argparser(load_parser)
@@ -207,6 +211,30 @@ You may need to disable command auto-loading if you need dynamically load comman
app.cmdloop()
+Event Handlers
+--------------
+The following functions are called at different points in the ``CommandSet`` life cycle.
+
+``on_register(self, cmd) -> None`` - Called by cmd2.Cmd as the first step to
+registering a CommandSet. The commands defined in this class have not be
+added to the CLI object at this point. Subclasses can override this to
+perform any initialization requiring access to the Cmd object
+(e.g. configure commands and their parsers based on CLI state data).
+
+``on_registered(self) -> None`` - Called by cmd2.Cmd after a CommandSet is
+registered and all its commands have been added to the CLI. Subclasses can
+override this to perform custom steps related to the newly added commands
+(e.g. setting them to a disabled state).
+
+``on_unregister(self) -> None`` - Called by ``cmd2.Cmd`` as the first step to
+unregistering a CommandSet. Subclasses can override this to perform any cleanup
+steps which require their commands being registered in the CLI.
+
+``on_unregistered(self) -> None`` - Called by ``cmd2.Cmd`` after a CommandSet
+has been unregistered and all its commands removed from the CLI. Subclasses can
+override this to perform remaining cleanup steps.
+
+
Injecting Subcommands
----------------------
@@ -281,7 +309,7 @@ command and each CommandSet
self._fruits = LoadableFruits()
self._vegetables = LoadableVegetables()
- load_parser = cmd2.Cmd2ArgumentParser('load')
+ load_parser = cmd2.Cmd2ArgumentParser()
load_parser.add_argument('cmds', choices=['fruits', 'vegetables'])
@with_argparser(load_parser)
@@ -311,8 +339,8 @@ command and each CommandSet
self.unregister_command_set(self._vegetables)
self.poutput('Vegetables unloaded')
- cut_parser = cmd2.Cmd2ArgumentParser('cut')
- cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut', unloadable=True)
+ cut_parser = cmd2.Cmd2ArgumentParser()
+ cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut')
@with_argparser(cut_parser)
def do_cut(self, ns: argparse.Namespace):
diff --git a/examples/modular_commands/commandset_complex.py b/examples/modular_commands/commandset_complex.py
index 7c6b1300..579c0677 100644
--- a/examples/modular_commands/commandset_complex.py
+++ b/examples/modular_commands/commandset_complex.py
@@ -20,7 +20,7 @@ class CommandSetA(cmd2.CommandSet):
"""Banana Command"""
self._cmd.poutput('Banana!!')
- cranberry_parser = cmd2.Cmd2ArgumentParser('cranberry')
+ cranberry_parser = cmd2.Cmd2ArgumentParser()
cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce'])
@cmd2.with_argparser(cranberry_parser, with_unknown_args=True)
@@ -44,7 +44,7 @@ class CommandSetA(cmd2.CommandSet):
def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
return utils.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting'])
- elderberry_parser = cmd2.Cmd2ArgumentParser('elderberry')
+ elderberry_parser = cmd2.Cmd2ArgumentParser()
elderberry_parser.add_argument('arg1')
@cmd2.with_category('Alone')
diff --git a/examples/modular_commands_dynamic.py b/examples/modular_commands_dynamic.py
index eb6283a7..b2be5dd1 100644
--- a/examples/modular_commands_dynamic.py
+++ b/examples/modular_commands_dynamic.py
@@ -50,7 +50,7 @@ class ExampleApp(cmd2.Cmd):
self._fruits = LoadableFruits()
self._vegetables = LoadableVegetables()
- load_parser = cmd2.Cmd2ArgumentParser('load')
+ load_parser = cmd2.Cmd2ArgumentParser()
load_parser.add_argument('cmds', choices=['fruits', 'vegetables'])
@with_argparser(load_parser)
diff --git a/examples/modular_subcommands.py b/examples/modular_subcommands.py
index 94959349..4445fde2 100644
--- a/examples/modular_subcommands.py
+++ b/examples/modular_subcommands.py
@@ -62,7 +62,7 @@ class ExampleApp(cmd2.Cmd):
self._fruits = LoadableFruits()
self._vegetables = LoadableVegetables()
- load_parser = cmd2.Cmd2ArgumentParser('load')
+ load_parser = cmd2.Cmd2ArgumentParser()
load_parser.add_argument('cmds', choices=['fruits', 'vegetables'])
@with_argparser(load_parser)
@@ -92,7 +92,7 @@ class ExampleApp(cmd2.Cmd):
self.unregister_command_set(self._vegetables)
self.poutput('Vegetables unloaded')
- cut_parser = cmd2.Cmd2ArgumentParser('cut')
+ cut_parser = cmd2.Cmd2ArgumentParser()
cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut')
@with_argparser(cut_parser)
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 3b240e4e..be6f52d1 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -2247,7 +2247,7 @@ def test_disable_and_enable_category(disable_commands_app):
# Make sure neither function completes
text = ''
- line = 'has_helper_funcs'
+ line = 'has_helper_funcs {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
@@ -2255,7 +2255,7 @@ def test_disable_and_enable_category(disable_commands_app):
assert first_match is None
text = ''
- line = 'has_no_helper_funcs'
+ line = 'has_no_helper_funcs {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
@@ -2291,7 +2291,7 @@ def test_disable_and_enable_category(disable_commands_app):
# has_helper_funcs should complete now
text = ''
- line = 'has_helper_funcs'
+ line = 'has_helper_funcs {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
@@ -2300,7 +2300,7 @@ def test_disable_and_enable_category(disable_commands_app):
# has_no_helper_funcs had no completer originally, so there should be no results
text = ''
- line = 'has_no_helper_funcs'
+ line = 'has_no_helper_funcs {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py
index 5b670601..1685accf 100644
--- a/tests_isolated/test_commandset/test_commandset.py
+++ b/tests_isolated/test_commandset/test_commandset.py
@@ -21,6 +21,22 @@ class CommandSetBase(cmd2.CommandSet):
@cmd2.with_default_category('Fruits')
class CommandSetA(CommandSetBase):
+ def on_register(self, cmd) -> None:
+ super().on_register(cmd)
+ print("in on_register now")
+
+ def on_registered(self) -> None:
+ super().on_registered()
+ print("in on_registered now")
+
+ def on_unregister(self) -> None:
+ super().on_unregister()
+ print("in on_unregister now")
+
+ def on_unregistered(self) -> None:
+ super().on_unregistered()
+ print("in on_unregistered now")
+
def do_apple(self, statement: cmd2.Statement):
self._cmd.poutput('Apple!')
@@ -28,7 +44,7 @@ class CommandSetA(CommandSetBase):
"""Banana Command"""
self._cmd.poutput('Banana!!')
- cranberry_parser = cmd2.Cmd2ArgumentParser('cranberry')
+ cranberry_parser = cmd2.Cmd2ArgumentParser()
cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce'])
@cmd2.with_argparser(cranberry_parser, with_unknown_args=True)
@@ -53,7 +69,7 @@ class CommandSetA(CommandSetBase):
def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
return utils.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting'])
- elderberry_parser = cmd2.Cmd2ArgumentParser('elderberry')
+ elderberry_parser = cmd2.Cmd2ArgumentParser()
elderberry_parser.add_argument('arg1')
@cmd2.with_category('Alone')
@@ -158,7 +174,7 @@ def test_custom_construct_commandsets():
assert command_set_2 not in matches
-def test_load_commands(command_sets_manual):
+def test_load_commands(command_sets_manual, capsys):
# now install a command set and verify the commands are now present
cmd_set = CommandSetA()
@@ -171,6 +187,11 @@ def test_load_commands(command_sets_manual):
assert command_sets_manual.find_commandsets(CommandSetA)[0] is cmd_set
assert command_sets_manual.find_commandset_for_command('elderberry') is cmd_set
+ # Make sure registration callbacks ran
+ out, err = capsys.readouterr()
+ assert "in on_register now" in out
+ assert "in on_registered now" in out
+
cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
assert 'Alone' in cmds_cats
@@ -192,6 +213,11 @@ def test_load_commands(command_sets_manual):
assert 'Alone' not in cmds_cats
assert 'Fruits' not in cmds_cats
+ # Make sure unregistration callbacks ran
+ out, err = capsys.readouterr()
+ assert "in on_unregister now" in out
+ assert "in on_unregistered now" in out
+
# uninstall a second time and verify no errors happen
command_sets_manual.unregister_command_set(cmd_set)
@@ -298,7 +324,7 @@ class LoadableBase(cmd2.CommandSet):
self._dummy = dummy # prevents autoload
self._cut_called = False
- cut_parser = cmd2.Cmd2ArgumentParser('cut')
+ cut_parser = cmd2.Cmd2ArgumentParser()
cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut')
def namespace_provider(self) -> argparse.Namespace:
@@ -319,8 +345,7 @@ class LoadableBase(cmd2.CommandSet):
self._cmd.pwarning('This command does nothing without sub-parsers registered')
self._cmd.do_help('cut')
-
- stir_parser = cmd2.Cmd2ArgumentParser('stir')
+ stir_parser = cmd2.Cmd2ArgumentParser()
stir_subparsers = stir_parser.add_subparsers(title='item', help='what to stir')
@cmd2.with_argparser(stir_parser, ns_provider=namespace_provider)
@@ -592,7 +617,7 @@ class AppWithSubCommands(cmd2.Cmd):
def __init__(self, *args, **kwargs):
super(AppWithSubCommands, self).__init__(*args, **kwargs)
- cut_parser = cmd2.Cmd2ArgumentParser('cut')
+ cut_parser = cmd2.Cmd2ArgumentParser()
cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut')
@cmd2.with_argparser(cut_parser)
@@ -853,7 +878,7 @@ def test_bad_subcommand():
def __init__(self, *args, **kwargs):
super(BadSubcommandApp, self).__init__(*args, **kwargs)
- cut_parser = cmd2.Cmd2ArgumentParser('cut')
+ cut_parser = cmd2.Cmd2ArgumentParser()
cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut')
@cmd2.with_argparser(cut_parser)