summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkotfu <kotfu@kotfu.net>2018-05-26 16:23:15 -0600
committerkotfu <kotfu@kotfu.net>2018-05-26 16:23:15 -0600
commit4d7d98c0c856cb5ce1ac1dbe214a542f4a83a813 (patch)
tree0c7ae5f309d6a7141118d75beaa4c69efc1c1fde
parentc390a6bc1aed393b7c7d4e493d61636b34bcdc9a (diff)
downloadcmd2-git-4d7d98c0c856cb5ce1ac1dbe214a542f4a83a813.tar.gz
Document and test postparsing hooks
-rw-r--r--docs/hooks.rst123
-rw-r--r--tests/test_plugin.py179
2 files changed, 275 insertions, 27 deletions
diff --git a/docs/hooks.rst b/docs/hooks.rst
index 1402fee5..9e6c8b82 100644
--- a/docs/hooks.rst
+++ b/docs/hooks.rst
@@ -5,14 +5,16 @@ cmd2 Application Lifecycle and Hooks
The typical way of starting a cmd2 application is as follows::
- from cmd2 import Cmd
- class App(Cmd):
+ import cmd2
+ class App(cmd2.Cmd):
# customized attributes and methods here
- app = App()
- app.cmdloop()
+
+ if __name__ == '__main__':
+ app = App()
+ app.cmdloop()
There are several pre-existing methods and attributes which you can tweak to control the overall behavior of your
-application before, during, and after the main loop.
+application before, during, and after the command processing loop.
Application Lifecycle Hook Methods
----------------------------------
@@ -36,8 +38,94 @@ behavior upon entering or during the main loop. A partial list of some of the m
command results
-Command Processing Hooks
-------------------------
+Command Processing Loop
+-----------------------
+
+When you call `.cmdloop()`, the following sequence of events are repeated
+until the application exits:
+
+1. Output the prompt
+2. Accept user input
+3. Call methods registered with `register_preparsing_hook()`
+4. Parse user input into `Statement` object
+5. Call methods registered with `register_postparsing_hook()`
+6. Call `postparsing_precmd()` - for backwards compatibility deprecated
+7. Redirect output, if user asked for it and it's allowed
+8. Start command timer
+9. Call methods registered with `register_precmd_hook()`
+10. Call `precmd()` - for backwards compatibility deprecated
+11. Add statement to history
+12. Call `do_command` method
+13. Call methods registered with `register_postcmd_hook()`
+14. Call `postcmd()` - for backwards compatibility deprecated
+15. Stop timer
+16. Stop redirecting output, if it was redirected
+17. Call methods registered with `register_cmdcompleted_hook()`
+18. Call `postparsing_postcmd()` - for backwards compatibility - deprecated
+
+By registering hook methods, steps 3, 5, 9, 13, and 17 allow you to run code
+during, and control the flow of the command processing loop. Be aware that
+plugins also utilize these hooks, so there may be code running that is not
+part of your application. Methods registered for a hook are called in the
+order they were registered. You can register a function more than once, and
+it will be called each time it was registered.
+
+Preparsing Hooks
+^^^^^^^^^^^^^^^^
+
+Postparsing Hooks
+^^^^^^^^^^^^^^^^^
+
+You can register one or more methods which are called after the user input
+has been parsed, but before output is redirected, the timer is started, and
+before the command is run.
+
+To define and register a postparsing hook, do the following::
+
+ class App(cmd2.Cmd):
+ def __init__(self, *args, *kwargs):
+ super().__init__(*args, **kwargs)
+ self.register_postparsing_hook(self.myhookmethod)
+
+ def myhookmethod(self, statement):
+ return False, statement
+
+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 and exit the application, and the second element is a potentially
+modified `Statement` object.
+
+To modify the user input, you create and return a new `Statement` object::
+
+ def myhookmethod(self, statement):
+ if not '|' in statement.raw:
+ newinput = statement.raw + ' | less'
+ statement = self.statement_parser.parse(newinput)
+ return False, statement
+
+There are several other mechanisms for controlling the flow of command
+processing. If you raise an `cmd2.EmptyStatement` exception, no further
+postparsing hooks will be run, nor will the command be run. No error will
+be displayed for the user either.
+
+If you raise any other exception, no further postprocessing hooks will be run,
+nor will the command be executed. The exception message will be displayed for
+the user.
+
+Precommand Hooks
+^^^^^^^^^^^^^^^^^
+
+Postcommand Hooks
+^^^^^^^^^^^^^^^^^^
+
+Command Completed Hooks
+^^^^^^^^^^^^^^^^^^^^^^^
+
+
+Deprecated Command Processing Hooks
+-----------------------------------
Inside the main loop, every time the user hits <Enter> the line is processed by the ``onecmd_plus_hooks`` method.
@@ -58,23 +146,4 @@ the various hook methods, presented in chronological order starting with the one
.. automethod:: cmd2.cmd2.Cmd.postcmd
-.. automethod:: cmd2.cmd2.Cmd.postparsing_postcmd
-
-Registering hooks
------------------
-
-As an alternative to overriding one of the hook methods, you can register any number of functions
-to be called when the hook is processed. These registered functions are called before any overridden
-method.
-
-This method of registering and calling multiple hooks allows plugins to tap into the hook mechanism
-without interfering with each other or with your code.
-
-register_preloop_hook
-register_postloop_hook
-
-register_preparsing_hook
-register_postparsing_hook
-register_precmd_hook
-register_postcmd_hook
-register_cmdcompleted_hook
+.. automethod:: cmd2.cmd2.Cmd.postparsing_postcmd \ No newline at end of file
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
new file mode 100644
index 00000000..c82e6bcd
--- /dev/null
+++ b/tests/test_plugin.py
@@ -0,0 +1,179 @@
+# coding=utf-8
+"""
+Test plugin infrastructure and hooks.
+
+Copyright 2018 Jared Crapo <jared@kotfu.net>
+Released under MIT license, see LICENSE file
+"""
+
+from typing import Tuple
+
+import pytest
+
+import cmd2
+
+from .conftest import StdOut
+
+class Plugin:
+ "A mixin class for testing hook registration and calling"
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.reset_counters()
+
+ def reset_counters(self):
+ self.called_pph = 0
+
+ def pph(self, statement: cmd2.Statement) -> Tuple[bool, cmd2.Statement]:
+ self.called_pph += 1
+ return False, statement
+
+ def pph_stop(self, statement: cmd2.Statement) -> Tuple[bool, cmd2.Statement]:
+ self.called_pph += 1
+ return True, statement
+
+ def pph_emptystatement(self, statement: cmd2.Statement) -> Tuple[bool, cmd2.Statement]:
+ self.called_pph += 1
+ raise cmd2.EmptyStatement
+
+ def pph_exception(self, statement: cmd2.Statement) -> Tuple[bool, cmd2.Statement]:
+ self.called_pph += 1
+ raise ValueError
+
+class PluggedApp(Plugin, cmd2.Cmd):
+ "A sample app with a plugin mixed in"
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def do_say(self, statement):
+ """Repeat back the arguments"""
+ self.poutput(statement)
+
+###
+#
+# test hooks
+#
+###
+def test_postparsing_hook(capsys):
+ app = PluggedApp()
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ assert not app.called_pph
+
+ app.reset_counters()
+ app.register_postparsing_hook(app.pph)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ assert app.called_pph == 1
+
+ # register the function again, so it should be called
+ # twice
+ app.reset_counters()
+ app.register_postparsing_hook(app.pph)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ assert app.called_pph == 2
+
+def test_postparsing_hook_stop_first(capsys):
+ app = PluggedApp()
+ app.register_postparsing_hook(app.pph_stop)
+ stop = app.onecmd_plus_hooks('say hello')
+ assert app.called_pph == 1
+ assert stop
+
+ # register another function but it shouldn't be called
+ app.reset_counters()
+ app.register_postparsing_hook(app.pph)
+ stop = app.onecmd_plus_hooks('say hello')
+ assert app.called_pph == 1
+ assert stop
+
+def test_postparsing_hook_stop_second(capsys):
+ app = PluggedApp()
+ app.register_postparsing_hook(app.pph)
+ stop = app.onecmd_plus_hooks('say hello')
+ assert app.called_pph == 1
+ assert not stop
+
+ # register another function and make sure it gets called
+ app.reset_counters()
+ app.register_postparsing_hook(app.pph_stop)
+ stop = app.onecmd_plus_hooks('say hello')
+ assert app.called_pph == 2
+ assert stop
+
+ # register a third function which shouldn't be called
+ app.reset_counters()
+ app.register_postparsing_hook(app.pph)
+ stop = app.onecmd_plus_hooks('say hello')
+ assert app.called_pph == 2
+ assert stop
+
+def test_postparsing_hook_emptystatement_first(capsys):
+ app = PluggedApp()
+ app.register_postparsing_hook(app.pph_emptystatement)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not out
+ assert not err
+ assert app.called_pph == 1
+
+ # register another function but it shouldn't be called
+ app.reset_counters()
+ app.register_postparsing_hook(app.pph)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not out
+ assert not err
+ assert app.called_pph == 1
+
+def test_postparsing_hook_emptystatement_second(capsys):
+ app = PluggedApp()
+ app.register_postparsing_hook(app.pph)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ assert app.called_pph == 1
+
+ # register another function and make sure it gets called
+ app.reset_counters()
+ app.register_postparsing_hook(app.pph_emptystatement)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not out
+ assert not err
+ assert app.called_pph == 2
+
+ # register a third function which shouldn't be called
+ app.reset_counters()
+ app.register_postparsing_hook(app.pph)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not out
+ assert not err
+ assert app.called_pph == 2
+ assert not stop
+
+def test_postparsing_hook_exception(capsys):
+ app = PluggedApp()
+ app.register_postparsing_hook(app.pph_exception)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not out
+ assert err
+ assert app.called_pph == 1
+
+ # register another function, but it shouldn't be called
+ app.reset_counters()
+ app.register_postparsing_hook(app.pph)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not out
+ assert err
+ assert app.called_pph == 1