diff options
-rw-r--r-- | cmd2/cmd2.py | 32 | ||||
-rw-r--r-- | docs/hooks.rst | 44 | ||||
-rw-r--r-- | tests/test_plugin.py | 58 |
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') |