summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py32
-rw-r--r--docs/hooks.rst44
-rw-r--r--tests/test_plugin.py58
3 files changed, 103 insertions, 31 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 6e8db039..c2e79b09 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -1691,14 +1691,19 @@ class Cmd(cmd.Cmd):
try:
statement = self._complete_statement(line)
# call the postparsing hooks
+ params = plugin.PostparsingData(False, statement)
for func in self._postparsing_hooks:
- (stop, statement) = func(statement)
- if stop:
+ params = func(params)
+ if params.stop:
break
- if not stop:
- (stop, statement) = self.postparsing_precmd(statement)
+ # postparsing_precmd is deprecated
+ if not params.stop:
+ (params.stop, params.statement) = self.postparsing_precmd(params.statement)
+ # unpack the data object
+ statement = params.statement
+ stop = params.stop
if stop:
- # we need to not run the command, but
+ # we should not run the command, but
# we need to run the finalization hooks
raise EmptyStatement
@@ -3162,10 +3167,25 @@ Script should contain one command per line, just like command would be typed in
self._validate_prepostloop_callable(func)
self._postloop_hooks.append(func)
+ @classmethod
+ def _validate_postparsing_callable(cls, func):
+ """Check parameter and return values for postparsing hooks"""
+ cls._validate_callable_param_count(func, 1)
+ signature = inspect.signature(func)
+ _, param = list(signature.parameters.items())[0]
+ if param.annotation != plugin.PostparsingData:
+ raise TypeError("{} must have one parameter declared with type 'cmd2.plugin.PostparsingData'".format(
+ func.__name__
+ ))
+ if signature.return_annotation != plugin.PostparsingData:
+ raise TypeError("{} must declare return a return type of 'cmd2.plugin.PostparsingData'".format(
+ func.__name__
+ ))
+
def register_postparsing_hook(self, func):
"""Register a function to be called after parsing user input but before running the command"""
+ self._validate_postparsing_callable(func)
self._postparsing_hooks.append(func)
- # TODO check signature of registered func and throw error if it's wrong
def register_precmd_hook(self, func):
"""Register a function to be called before the command function."""
diff --git a/docs/hooks.rst b/docs/hooks.rst
index 05bb7078..4c15e90c 100644
--- a/docs/hooks.rst
+++ b/docs/hooks.rst
@@ -73,7 +73,7 @@ the application exits:
2. Accept user input
3. Parse user input into `Statement` object
4. Call methods registered with `register_postparsing_hook()`
-5. Call `postparsing_precmd()` - for backwards compatibility deprecated
+5. Call `postparsing_precmd()` - for backwards compatibility with prior releases of cmd2, now deprecated
6. Redirect output, if user asked for it and it's allowed
7. Start timer
8. Call methods registered with `register_precmd_hook()`
@@ -141,18 +141,24 @@ also raise a ``TypeError`` if the passed parameter and return value are not anno
as ``PostparsingData``.
-The hook method will be passed one parameter, a ``Statement`` object containing
-the parsed user input. There are many useful attributes in the ``Statement``
-object, including ``.raw`` which contains exactly what the user typed. The hook
-method must return a tuple: the first element indicates whether to fatally fail
-this command prior to execution and exit the application, and the second element
-is a potentially modified ``Statement`` object.
+The hook method will be passed one parameter, a ``PostparsingData`` object
+which we will refer to as ``params``. ``params`` contains two attributes.
+``params.statement`` is a ``Statement`` object which describes the parsed
+user input. There are many useful attributes in the ``Statement``
+object, including ``.raw`` which contains exactly what the user typed.
+``params.stop`` is set to ``False`` by default.
-To modify the user input, you create and return a new ``Statement`` object.
-Don't try and directly modify the contents of a ``Statement`` object, there be
-dragons. Instead, use the various attributes in a ``Statement`` object to
-construct a new string, and then parse that string to create a new ``Statement``
-object.
+The hook method must return a ``PostparsingData`` object, and it is very
+convenient to just return the object passed into the hook method. The hook
+method may modify the attributes of the object to influece the behavior of
+the application. If ``params.stop`` is set to true, a fatal failure is
+triggered prior to execution of the command, and the application exits.
+
+To modify the user input, you create a new ``Statement`` object and return it in
+``params.statement``. Don't try and directly modify the contents of a
+``Statement`` object, there be dragons. Instead, use the various attributes in a
+``Statement`` object to construct a new string, and then parse that string to
+create a new ``Statement`` object.
``cmd2.Cmd()`` uses an instance of ``cmd2.StatementParser`` to parse user input.
This instance has been configured with the proper command terminators, multiline
@@ -160,14 +166,14 @@ commands, and other parsing related settings. This instance is available as the
``self.statement_parser`` attribute. Here's a simple example which shows the
proper technique::
- def myhookmethod(self, statement):
- stop = False
- if not '|' in statement.raw:
- newinput = statement.raw + ' | less'
- statement = self.statement_parser.parse(newinput)
- return stop, statement
+ def myhookmethod(self, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
+ if not '|' in params.statement.raw:
+ newinput = params.statement.raw + ' | less'
+ params.statement = self.statement_parser.parse(newinput)
+ return params
-If a postparsing hook returns ``True`` as the first value in the tuple:
+If a postparsing hook returns a ``PostparsingData`` object with the ``stop``
+attribute set to ``True``:
- no more hooks of any kind (except command finalization hooks) will be called
- the command will not be executed
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index d4d6166e..39f35cb0 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -52,26 +52,47 @@ class Plugin:
# Postparsing hooks
#
###
- def postparse_hook(self, statement: cmd2.Statement) -> Tuple[bool, cmd2.Statement]:
+ def postparse_hook(self, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
"A postparsing hook"
self.called_postparsing += 1
- return False, statement
+ return params
- def postparse_hook_stop(self, statement: cmd2.Statement) -> Tuple[bool, cmd2.Statement]:
+ def postparse_hook_stop(self, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
"A postparsing hook with requests application exit"
self.called_postparsing += 1
- return True, statement
+ params.stop = True
+ return params
- def postparse_hook_emptystatement(self, statement: cmd2.Statement) -> Tuple[bool, cmd2.Statement]:
+ def postparse_hook_emptystatement(self, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
"A postparsing hook with raises an EmptyStatement exception"
self.called_postparsing += 1
raise cmd2.EmptyStatement
- def postparse_hook_exception(self, statement: cmd2.Statement) -> Tuple[bool, cmd2.Statement]:
+ def postparse_hook_exception(self, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
"A postparsing hook which raises an exception"
self.called_postparsing += 1
raise ValueError
+ def postparse_hook_too_many_parameters(self, param1, param2) -> cmd2.plugin.PostparsingData:
+ "A postparsing hook with too many parameters"
+ pass
+
+ def postparse_hook_undeclared_parameter_type(self, param) -> cmd2.plugin.PostparsingData:
+ "A postparsing hook with an undeclared parameter type"
+ pass
+
+ def postparse_hook_wrong_parameter_type(self, params: str) -> cmd2.plugin.PostparsingData:
+ "A postparsing hook with the wrong parameter type"
+ pass
+
+ def postparse_hook_undeclared_return_type(self, params: cmd2.plugin.PostparsingData):
+ "A postparsing hook with an undeclared return type"
+ pass
+
+ def postparse_hook_wrong_return_type(self, params: cmd2.plugin.PostparsingData) -> str:
+ "A postparsing hook with the wrong return type"
+ pass
+
###
#
# precommand hooks, some valid, some invalid
@@ -202,6 +223,31 @@ def test_postloop_hooks(capsys):
# test postparsing hooks
#
###
+def test_postparsing_hook_too_many_parameters():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postparsing_hook(app.postparse_hook_too_many_parameters)
+
+def test_postparsing_hook_undeclared_parameter_type():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postparsing_hook(app.postparse_hook_undeclared_parameter_type)
+
+def test_postparsing_hook_wrong_parameter_type():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postparsing_hook(app.postparse_hook_wrong_parameter_type)
+
+def test_postparsing_hook_undeclared_return_type():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postparsing_hook(app.postparse_hook_undeclared_return_type)
+
+def test_postparsing_hook_wrong_return_type():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postparsing_hook(app.postparse_hook_wrong_return_type)
+
def test_postparsing_hook(capsys):
app = PluggedApp()
app.onecmd_plus_hooks('say hello')