summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py30
-rw-r--r--docs/hooks.rst33
-rw-r--r--tests/test_plugin.py59
3 files changed, 108 insertions, 14 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 69d6224e..89f14b37 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -3127,8 +3127,36 @@ Script should contain one command per line, just like command would be typed in
def register_precmd_hook(self, func):
"""Register a function to be called before the command function."""
+ import inspect
+ signature = inspect.signature(func)
+ # validate that the callable has the right number of parameters
+ nparam = len(signature.parameters)
+ if nparam < 1:
+ raise TypeError('precommand hooks must have one positional argument')
+ if nparam > 1:
+ raise TypeError('precommand hooks take one positional argument but {} were given'.format(nparam))
+ # validate the parameter has the right annotation
+ paramname = list(signature.parameters.keys())[0]
+ param = signature.parameters[paramname]
+ if param.annotation != plugin.PrecommandData:
+ raise TypeError('argument 1 of {} has incompatible type {}, expected {}'.format(
+ func.__name__,
+ param.annotation,
+ plugin.PrecommandData,
+ ))
+ # validate the return value has the right annotation
+ if signature.return_annotation == signature.empty:
+ raise TypeError('{} does not have a declared return type, expected {}'.format(
+ func.__name__,
+ plugin.PrecommandData,
+ ))
+ if signature.return_annotation != plugin.PrecommandData:
+ raise TypeError('{} has incompatible return type {}, expected {}'.format(
+ func.__name__,
+ signature.return_annotation,
+ plugin.PrecommandData,
+ ))
self._precmd_hooks.append(func)
- # TODO check signature of registered func and throw error if it's wrong
def register_postcmd_hook(self, func):
"""Register a function to be called after the command function."""
diff --git a/docs/hooks.rst b/docs/hooks.rst
index 47909abf..1efb2c3a 100644
--- a/docs/hooks.rst
+++ b/docs/hooks.rst
@@ -166,28 +166,39 @@ If a postparsing hook returns ``True`` as the first value in the tuple:
Precommand Hooks
^^^^^^^^^^^^^^^^^
-A precommand hook is defined in ``cmd.Cmd``. It is not able to request that the
-app terminate, but it is passed the user input and allowed to make changes. If
-your hook needs to be able to exit the application, you should implement it as a
-postparsing hook.
+Precommand hooks can modify the user input, but can not request the application
+terminate. If your hook needs to be able to exit the application, you should
+implement it as a postparsing hook.
Once output is redirected and the timer started, all the hooks registered with
-``register_precmd_hook()`` are called. Here's how you do it::
+``register_precmd_hook()`` are called. Here's how to do it::
class App(cmd2.Cmd):
def __init__(self, *args, *kwargs):
super().__init__(*args, **kwargs)
self.register_precmd_hook(self.myhookmethod)
- def myhookmethod(self, statement):
- return statement
+ def myhookmethod(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
+ # the statement object created from the user input
+ # is available as data.statement
+ return data
+
+``register_precmd_hook()`` checks the method signature of the passed callable,
+and raises a ``TypeError`` if it has the wrong number of parameters. It will
+also raise a ``TypeError`` if the parameters and return value are not annotated
+as ``PrecommandData``.
-You may choose to create a new ``Statement`` with different properties (see
-above) or leave it alone, but you must return a ``Statement`` object.
+You may choose to modify the user input by creating a new ``Statement`` with
+different properties (see above). If you do so, assign your new ``Statement``
+object to ``data.statement``. In any case, you must return a ``PrecommandData``
+object. You don't have to create this object from scratch, you can just return
+the one given to you after you make modifications.
After all registered precommand hooks have been called,
-``self.precmd(statement)`` will be called. This retains full backwards
-compatibility with ``cmd.Cmd``.
+``self.precmd(statement)`` will be called. To retain full backward compatibility
+with ``cmd.Cmd``, this method is passed a ``Statement``, not a
+``PrecommandData`` object.
+
Postcommand Hooks
^^^^^^^^^^^^^^^^^^
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 133284a0..26eb88bb 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -53,6 +53,11 @@ class Plugin:
self.called_postparsing += 1
raise ValueError
+ ###
+ #
+ # precommand hooks, some valid, some invalid
+ #
+ ###
def precmd(self, statement: cmd2.Statement) -> cmd2.Statement:
"Override cmd.Cmd method"
self.called_precmd += 1
@@ -63,16 +68,39 @@ class Plugin:
self.called_precmd += 1
return data
- def precmd_hook_emptystatement(self, statement: cmd2.Statement) -> cmd2.Statement:
+ def precmd_hook_emptystatement(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
"A precommand hook which raises an EmptyStatement exception"
self.called_precmd += 1
raise cmd2.EmptyStatement
- def precmd_hook_exception(self, statement: cmd2.Statement) -> cmd2.Statement:
+ def precmd_hook_exception(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
"A precommand hook which raises an exception"
self.called_precmd += 1
raise ValueError
+ def precmd_hook_not_enough_parameters(self) -> plugin.PrecommandData:
+ "A precommand hook with no parameters"
+ pass
+
+ def precmd_hook_too_many_parameters(self, one: plugin.PrecommandData, two: str) -> plugin.PrecommandData:
+ "A precommand hook with too many parameters"
+ return one
+
+ def precmd_hook_no_parameter_annotation(self, mydat) -> plugin.PrecommandData:
+ "A precommand hook with no type annotation on the parameter"
+ return mydat
+
+ def precmd_hook_wrong_parameter_annotation(self, mydat: str) -> plugin.PrecommandData:
+ "A precommand hook with the incorrect type annotation on the parameter"
+ return mydat
+
+ def precmd_hook_no_return_annotation(self, mydat: plugin.PrecommandData):
+ "A precommand hook with no type annotation on the return value"
+ return mydat
+
+ def precmd_hook_wrong_return_annotation(self, mydat: plugin.PrecommandData) -> cmd2.Statement:
+ return self.statement_parser.parse('hi there')
+
class PluggedApp(Plugin, cmd2.Cmd):
"A sample app with a plugin mixed in"
@@ -270,6 +298,33 @@ def test_postparsing_hook_exception(capsys):
# test precmd hooks
#
#####
+def test_register_precmd_hook_parameter_count():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_precmd_hook(app.precmd_hook_not_enough_parameters)
+ with pytest.raises(TypeError):
+ app.register_precmd_hook(app.precmd_hook_too_many_parameters)
+
+def test_register_precmd_hook_no_parameter_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_precmd_hook(app.precmd_hook_no_parameter_annotation)
+
+def test_register_precmd_hook_wrong_parameter_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_precmd_hook(app.precmd_hook_wrong_parameter_annotation)
+
+def test_register_precmd_hook_no_return_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_precmd_hook(app.precmd_hook_no_return_annotation)
+
+def test_register_precmd_hook_wrong_return_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_precmd_hook(app.precmd_hook_wrong_return_annotation)
+
def test_precmd_hook(capsys):
app = PluggedApp()
app.onecmd_plus_hooks('say hello')