diff options
author | kotfu <kotfu@kotfu.net> | 2018-05-26 16:23:15 -0600 |
---|---|---|
committer | kotfu <kotfu@kotfu.net> | 2018-05-26 16:23:15 -0600 |
commit | 4d7d98c0c856cb5ce1ac1dbe214a542f4a83a813 (patch) | |
tree | 0c7ae5f309d6a7141118d75beaa4c69efc1c1fde | |
parent | c390a6bc1aed393b7c7d4e493d61636b34bcdc9a (diff) | |
download | cmd2-git-4d7d98c0c856cb5ce1ac1dbe214a542f4a83a813.tar.gz |
Document and test postparsing hooks
-rw-r--r-- | docs/hooks.rst | 123 | ||||
-rw-r--r-- | tests/test_plugin.py | 179 |
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 |