summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py199
-rw-r--r--cmd2/parsing.py108
-rw-r--r--cmd2/plugin.py23
-rw-r--r--docs/hooks.rst317
-rw-r--r--tests/test_parsing.py95
-rw-r--r--tests/test_plugin.py799
6 files changed, 1459 insertions, 82 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 42e00c39..8db7cef3 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -34,6 +34,7 @@ import cmd
import collections
from colorama import Fore
import glob
+import inspect
import os
import platform
import re
@@ -43,6 +44,7 @@ from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Union
from . import constants
from . import utils
+from . import plugin
from .argparse_completer import AutoCompleter, ACArgumentParser
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .parsing import StatementParser, Statement
@@ -381,6 +383,10 @@ class Cmd(cmd.Cmd):
import atexit
atexit.register(readline.write_history_file, persistent_history_file)
+ # initialize plugin system
+ # needs to be done before we call __init__(0)
+ self._initialize_plugin_system()
+
# Call super class constructor
super().__init__(completekey=completekey, stdin=stdin, stdout=stdout)
@@ -1680,12 +1686,35 @@ class Cmd(cmd.Cmd):
:return: True if cmdloop() should exit, False otherwise
"""
import datetime
+
stop = False
try:
statement = self._complete_statement(line)
- (stop, statement) = self.postparsing_precmd(statement)
+ except EmptyStatement:
+ return self._run_cmdfinalization_hooks(stop, None)
+ except ValueError as ex:
+ # If shlex.split failed on syntax, let user know whats going on
+ self.perror("Invalid syntax: {}".format(ex), traceback_war=False)
+ return stop
+
+ # now that we have a statement, run it with all the hooks
+ try:
+ # call the postparsing hooks
+ data = plugin.PostparsingData(False, statement)
+ for func in self._postparsing_hooks:
+ data = func(data)
+ if data.stop:
+ break
+ # postparsing_precmd is deprecated
+ if not data.stop:
+ (data.stop, data.statement) = self.postparsing_precmd(data.statement)
+ # unpack the data object
+ statement = data.statement
+ stop = data.stop
if stop:
- return self.postparsing_postcmd(stop)
+ # we should not run the command, but
+ # we need to run the finalization hooks
+ raise EmptyStatement
try:
if self.allow_redirection:
@@ -1693,23 +1722,53 @@ class Cmd(cmd.Cmd):
timestart = datetime.datetime.now()
if self._in_py:
self._last_result = None
+
+ # precommand hooks
+ data = plugin.PrecommandData(statement)
+ for func in self._precmd_hooks:
+ data = func(data)
+ statement = data.statement
+ # call precmd() for compatibility with cmd.Cmd
statement = self.precmd(statement)
+
+ # go run the command function
stop = self.onecmd(statement)
+
+ # postcommand hooks
+ data = plugin.PostcommandData(stop, statement)
+ for func in self._postcmd_hooks:
+ data = func(data)
+ # retrieve the final value of stop, ignoring any statement modification from the hooks
+ stop = data.stop
+ # call postcmd() for compatibility with cmd.Cmd
stop = self.postcmd(stop, statement)
+
if self.timing:
self.pfeedback('Elapsed: %s' % str(datetime.datetime.now() - timestart))
finally:
if self.allow_redirection and self.redirecting:
self._restore_output(statement)
except EmptyStatement:
+ # don't do anything, but do allow command finalization hooks to run
pass
- except ValueError as ex:
- # If shlex.split failed on syntax, let user know whats going on
- self.perror("Invalid syntax: {}".format(ex), traceback_war=False)
except Exception as ex:
self.perror(ex)
finally:
+ return self._run_cmdfinalization_hooks(stop, statement)
+
+ def _run_cmdfinalization_hooks(self, stop: bool, statement: Statement) -> bool:
+ """Run the command finalization hooks"""
+ try:
+ data = plugin.CommandFinalizationData(stop, statement)
+ for func in self._cmdfinalization_hooks:
+ data = func(data)
+ # retrieve the final value of stop, ignoring any
+ # modifications to the statement
+ stop = data.stop
+ # postparsing_postcmd is deprecated
return self.postparsing_postcmd(stop)
+ except Exception as ex:
+ self.perror(ex)
def runcmds_plus_hooks(self, cmds: List[str]) -> bool:
"""Convenience method to run multiple commands by onecmd_plus_hooks.
@@ -3063,6 +3122,8 @@ Script should contain one command per line, just like command would be typed in
self.cmdqueue.extend(callargs)
# Always run the preloop first
+ for func in self._preloop_hooks:
+ func()
self.preloop()
# If transcript-based regression testing was requested, then do that instead of the main loop
@@ -3081,8 +3142,136 @@ Script should contain one command per line, just like command would be typed in
self._cmdloop()
# Run the postloop() no matter what
+ for func in self._postloop_hooks:
+ func()
self.postloop()
+ ###
+ #
+ # plugin related functions
+ #
+ ###
+ def _initialize_plugin_system(self):
+ """Initialize the plugin system"""
+ self._preloop_hooks = []
+ self._postloop_hooks = []
+ self._postparsing_hooks = []
+ self._precmd_hooks = []
+ self._postcmd_hooks = []
+ self._cmdfinalization_hooks = []
+
+ @classmethod
+ def _validate_callable_param_count(cls, func, count):
+ """Ensure a function has the given number of parameters."""
+ signature = inspect.signature(func)
+ # validate that the callable has the right number of parameters
+ nparam = len(signature.parameters)
+ if nparam != count:
+ raise TypeError('{} has {} positional arguments, expected {}'.format(
+ func.__name__,
+ nparam,
+ count,
+ ))
+
+ @classmethod
+ def _validate_prepostloop_callable(cls, func):
+ """Check parameter and return types for preloop and postloop hooks."""
+ cls._validate_callable_param_count(func, 0)
+ # make sure there is no return notation
+ signature = inspect.signature(func)
+ if signature.return_annotation != None:
+ raise TypeError("{} must declare return a return type of 'None'".format(
+ func.__name__,
+ ))
+
+ def register_preloop_hook(self, func):
+ """Register a function to be called at the beginning of the command loop."""
+ self._validate_prepostloop_callable(func)
+ self._preloop_hooks.append(func)
+
+ def register_postloop_hook(self, func):
+ """Register a function to be called at the end of the command loop."""
+ self._validate_prepostloop_callable(func)
+ self._postloop_hooks.append(func)
+
+ @classmethod
+ def _validate_postparsing_callable(cls, func):
+ """Check parameter and return types 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)
+
+ @classmethod
+ def _validate_prepostcmd_hook(cls, func, data_type):
+ """Check parameter and return types for pre and post command hooks."""
+ signature = inspect.signature(func)
+ # validate that the callable has the right number of parameters
+ cls._validate_callable_param_count(func, 1)
+ # validate the parameter has the right annotation
+ paramname = list(signature.parameters.keys())[0]
+ param = signature.parameters[paramname]
+ if param.annotation != data_type:
+ raise TypeError('argument 1 of {} has incompatible type {}, expected {}'.format(
+ func.__name__,
+ param.annotation,
+ data_type,
+ ))
+ # 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__,
+ data_type,
+ ))
+ if signature.return_annotation != data_type:
+ raise TypeError('{} has incompatible return type {}, expected {}'.format(
+ func.__name__,
+ signature.return_annotation,
+ data_type,
+ ))
+
+ def register_precmd_hook(self, func):
+ """Register a hook to be called before the command function."""
+ self._validate_prepostcmd_hook(func, plugin.PrecommandData)
+ self._precmd_hooks.append(func)
+
+ def register_postcmd_hook(self, func):
+ """Register a hook to be called after the command function."""
+ self._validate_prepostcmd_hook(func, plugin.PostcommandData)
+ self._postcmd_hooks.append(func)
+
+ @classmethod
+ def _validate_cmdfinalization_callable(cls, func):
+ """Check parameter and return types for command finalization hooks."""
+ cls._validate_callable_param_count(func, 1)
+ signature = inspect.signature(func)
+ _, param = list(signature.parameters.items())[0]
+ if param.annotation != plugin.CommandFinalizationData:
+ raise TypeError("{} must have one parameter declared with type 'cmd2.plugin.CommandFinalizationData'".format(
+ func.__name__
+ ))
+ if signature.return_annotation != plugin.CommandFinalizationData:
+ raise TypeError("{} must declare return a return type of 'cmd2.plugin.CommandFinalizationData'".format(
+ func.__name__
+ ))
+ pass
+
+ def register_cmdfinalization_hook(self, func):
+ """Register a hook to be called after a command is completed, whether it completes successfully or not."""
+ self._validate_cmdfinalization_callable(func)
+ self._cmdfinalization_hooks.append(func)
class History(list):
""" A list of HistoryItems that knows how to respond to user requests. """
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index 8d59aedb..b220f1c4 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -5,7 +5,7 @@
import os
import re
import shlex
-from typing import List, Tuple
+from typing import List, Tuple, Dict
from . import constants
from . import utils
@@ -21,6 +21,10 @@ class Statement(str):
need a place to capture the additional output of the command parsing, so we add
our own attributes to this subclass.
+ Instances of this class should not be created by anything other than the
+ `StatementParser.parse()` method, nor should any of the attributes be modified
+ once the object is created.
+
The string portion of the class contains the arguments, but not the command, nor
the output redirection clauses.
@@ -54,18 +58,39 @@ class Statement(str):
:type output_to: str or None
"""
- def __init__(self, obj):
- super().__init__()
- self.raw = str(obj)
- self.command = None
- self.multiline_command = None
- self.args = None
- self.argv = None
- self.terminator = None
- self.suffix = None
- self.pipe_to = None
- self.output = None
- self.output_to = None
+ def __new__(cls,
+ obj: object,
+ *,
+ raw: str = None,
+ command: str = None,
+ args: str = None,
+ argv: List[str] = None,
+ multiline_command: str = None,
+ terminator: str = None,
+ suffix: str = None,
+ pipe_to: str = None,
+ output: str = None,
+ output_to:str = None
+ ):
+ """Create a new instance of Statement
+
+ We must override __new__ because we are subclassing `str` which is
+ immutable.
+ """
+ stmt = str.__new__(cls, obj)
+ object.__setattr__(stmt, "raw", raw)
+ object.__setattr__(stmt, "command", command)
+ object.__setattr__(stmt, "args", args)
+ if argv is None:
+ argv = []
+ object.__setattr__(stmt, "argv", argv)
+ object.__setattr__(stmt, "multiline_command", multiline_command)
+ object.__setattr__(stmt, "terminator", terminator)
+ object.__setattr__(stmt, "suffix", suffix)
+ object.__setattr__(stmt, "pipe_to", pipe_to)
+ object.__setattr__(stmt, "output", output)
+ object.__setattr__(stmt, "output_to", output_to)
+ return stmt
@property
def command_and_args(self):
@@ -82,6 +107,13 @@ class Statement(str):
rtn = None
return rtn
+ def __setattr__(self, name, value):
+ """Statement instances should feel immutable; raise ValueError"""
+ raise ValueError
+
+ def __delattr__(self, name):
+ """Statement instances should feel immutable; raise ValueError"""
+ raise ValueError
class StatementParser:
"""Parse raw text into command components.
@@ -90,11 +122,11 @@ class StatementParser:
"""
def __init__(
self,
- allow_redirection=True,
- terminators=None,
- multiline_commands=None,
- aliases=None,
- shortcuts=None,
+ allow_redirection: bool = True,
+ terminators: List[str] = None,
+ multiline_commands: List[str] = None,
+ aliases: Dict[str, str] = None,
+ shortcuts: Dict[str, str] = None,
):
self.allow_redirection = allow_redirection
if terminators is None:
@@ -228,7 +260,7 @@ class StatementParser:
tokens = self._split_on_punctuation(list(lexer))
return tokens
- def parse(self, rawinput: str) -> Statement:
+ def parse(self, line: str) -> Statement:
"""Tokenize the input and parse it into a Statement object, stripping
comments, expanding aliases and shortcuts, and extracting output
redirection directives.
@@ -240,7 +272,7 @@ class StatementParser:
# we have to do this before we tokenize because tokenizing
# destroys all unquoted whitespace in the input
terminator = None
- if rawinput[-1:] == LINE_FEED:
+ if line[-1:] == LINE_FEED:
terminator = LINE_FEED
command = None
@@ -248,7 +280,7 @@ class StatementParser:
argv = None
# lex the input into a list of tokens
- tokens = self.tokenize(rawinput)
+ tokens = self.tokenize(line)
# of the valid terminators, find the first one to occur in the input
terminator_pos = len(tokens) + 1
@@ -360,19 +392,18 @@ class StatementParser:
# build the statement
# string representation of args must be an empty string instead of
# None for compatibility with standard library cmd
- statement = Statement('' if args is None else args)
- statement.raw = rawinput
- statement.command = command
- # if there are no args we will use None since we don't have to worry
- # about compatibility with standard library cmd
- statement.args = args
- statement.argv = list(map(lambda x: utils.strip_quotes(x), argv))
- statement.terminator = terminator
- statement.output = output
- statement.output_to = output_to
- statement.pipe_to = pipe_to
- statement.suffix = suffix
- statement.multiline_command = multiline_command
+ statement = Statement('' if args is None else args,
+ raw=line,
+ command=command,
+ args=args,
+ argv=list(map(lambda x: utils.strip_quotes(x), argv)),
+ multiline_command=multiline_command,
+ terminator=terminator,
+ suffix=suffix,
+ pipe_to=pipe_to,
+ output=output,
+ output_to=output_to,
+ )
return statement
def parse_command_only(self, rawinput: str) -> Statement:
@@ -422,10 +453,11 @@ class StatementParser:
# build the statement
# string representation of args must be an empty string instead of
# None for compatibility with standard library cmd
- statement = Statement('' if args is None else args)
- statement.raw = rawinput
- statement.command = command
- statement.args = args
+ statement = Statement('' if args is None else args,
+ raw=rawinput,
+ command=command,
+ args=args,
+ )
return statement
def _expand(self, line: str) -> str:
diff --git a/cmd2/plugin.py b/cmd2/plugin.py
new file mode 100644
index 00000000..5c68dfb9
--- /dev/null
+++ b/cmd2/plugin.py
@@ -0,0 +1,23 @@
+#
+# coding=utf-8
+"""Classes for the cmd2 plugin system"""
+import attr
+
+@attr.s
+class PostparsingData():
+ stop = attr.ib()
+ statement = attr.ib()
+
+@attr.s
+class PrecommandData():
+ statement = attr.ib()
+
+@attr.s
+class PostcommandData():
+ stop = attr.ib()
+ statement = attr.ib()
+
+@attr.s
+class CommandFinalizationData():
+ stop = attr.ib()
+ statement = attr.ib()
diff --git a/docs/hooks.rst b/docs/hooks.rst
index 1e96e963..0ec5d7e9 100644
--- a/docs/hooks.rst
+++ b/docs/hooks.rst
@@ -1,43 +1,314 @@
-.. cmd2 documentation for application and command lifecycle and the hooks which are available
+.. cmd2 documentation for application and command lifecycle and the available hooks
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()
-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.
+ if __name__ == '__main__':
+ app = App()
+ app.cmdloop()
-Application Lifecycle Hook Methods
-----------------------------------
-The ``preloop`` and ``postloop`` methods run before and after the main loop, respectively.
+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
+command processing loop.
-.. automethod:: cmd2.cmd2.Cmd.preloop
+Application Lifecycle Hooks
+---------------------------
+
+You can register methods to be called at the beginning of the command loop::
+
+ class App(cmd2.Cmd):
+ def __init__(self, *args, *kwargs):
+ super().__init__(*args, **kwargs)
+ self.register_preloop_hook(self.myhookmethod)
+
+ def myhookmethod(self):
+ self.poutput("before the loop begins")
+
+And also after the command loop has finished::
+
+ class App(cmd2.Cmd):
+ def __init__(self, *args, *kwargs):
+ super().__init__(*args, **kwargs)
+ self.register_postloop_hook(self.myhookmethod)
+
+ def myhookmethod(self):
+ self.poutput("before the loop begins")
+
+Preloop and postloop hook methods are not passed any parameters and any return
+value is ignored.
-.. automethod:: cmd2.cmd2.Cmd.postloop
Application Lifecycle Attributes
--------------------------------
-There are numerous attributes (member variables of the ``cmd2.Cmd``) which have a significant effect on the application
-behavior upon entering or during the main loop. A partial list of some of the more important ones is presented here:
+There are numerous attributes (member variables of the ``cmd2.Cmd``) which have
+a significant effect on the application behavior upon entering or during the
+main loop. A partial list of some of the more important ones is presented here:
+
+- **intro**: *str* - if provided this serves as the intro banner printed once
+ at start of application, after ``preloop`` runs
+- **allow_cli_args**: *bool* - if True (default), then searches for -t or
+ --test at command line to invoke transcript testing mode instead of a normal
+ main loop and also processes any commands provided as arguments on the
+ command line just prior to entering the main loop
+- **echo**: *bool* - if True, then the command line entered is echoed to the
+ screen (most useful when running scripts)
+- **prompt**: *str* - sets the prompt which is displayed, can be dynamically
+ changed based on application state and/or command results
+
+
+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. Parse user input into `Statement` object
+4. Call methods registered with `register_postparsing_hook()`
+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()`
+9. Call `precmd()` - for backwards compatibility with ``cmd.Cmd``
+10. Add statement to history
+11. Call `do_command` method
+12. Call methods registered with `register_postcmd_hook()`
+13. Call `postcmd(stop, statement)` - for backwards compatibility with ``cmd.Cmd``
+14. Stop timer and display the elapsed time
+15. Stop redirecting output if it was redirected
+16. Call methods registered with `register_cmdfinalization_hook()`
+17. Call `postparsing_postcmd()` - for backwards compatibility - deprecated
+
+By registering hook methods, steps 4, 8, 12, and 16 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.
+
+Postparsing, precomamnd, and postcommand hook methods share some common ways to
+influence the command processing loop.
+
+If a hook raises a ``cmd2.EmptyStatement`` exception:
+- no more hooks (except command finalization hooks) of any kind will be called
+- if the command has not yet been executed, it will not be executed
+- no error message will be displayed to the user
+
+If a hook raises any other exception:
+- no more hooks (except command finalization hooks) of any kind will be called
+- if the command has not yet been executed, it will not be executed
+- the exception message will be displayed for the user.
+
+Specific types of hook methods have additional options as described below.
+
+Postparsing Hooks
+^^^^^^^^^^^^^^^^^
+
+Postparsing hooks are called after the user input has been parsed but before
+execution of the comamnd. These hooks can be used to:
+
+- modify the user input
+- run code before every command executes
+- cancel execution of the current command
+- exit the application
+
+When postparsing hooks are called, output has not been redirected, nor has the
+timer for command execution been started.
+
+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, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
+ # the statement object created from the user input
+ # is available as params.statement
+ return params
+
+``register_postparsing_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 passed parameter and return value are not annotated
+as ``PostparsingData``.
+
+
+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.
+
+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
+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, 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 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
+- no error message will be displayed to the user
+- the application will exit
-- **intro**: *str* - if provided this serves as the intro banner printed once at start of application, after ``preloop`` runs
-- **allow_cli_args**: *bool* - if True (default), then searches for -t or --test at command line to invoke transcript testing mode instead of a normal main loop
- and also processes any commands provided as arguments on the command line just prior to entering the main loop
-- **echo**: *bool* - if True, then the command line entered is echoed to the screen (most useful when running scripts)
-- **prompt**: *str* - sets the prompt which is displayed, can be dynamically changed based on application state and/or
- command results
+Precommand Hooks
+^^^^^^^^^^^^^^^^^
+
+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 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, data: cmd2.plugin.PrecommandData) -> cmd2.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 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``.
+
+The precommand hook must return a ``PrecommandData`` object. You don't have to
+create this object from scratch, you can just return the one passed into the hook.
+
+After all registered precommand hooks have been called,
+``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
+^^^^^^^^^^^^^^^^^^
+
+Once the command method has returned (i.e. the ``do_command(self, statement)
+method`` has been called and returns, all postcommand hooks are called. If
+output was redirected by the user, it is still redirected, and the command timer
+is still running.
+
+Here's how to define and register a postcommand hook::
+
+ class App(cmd2.Cmd):
+ def __init__(self, *args, *kwargs):
+ super().__init__(*args, **kwargs)
+ self.register_postcmd_hook(self.myhookmethod)
+
+ def myhookmethod(self, data: cmd2.plugin.PostcommandData) -> cmd2.plugin.PostcommandData:
+ return stop
+
+Your hook will be passed a ``PostcommandData`` object, which has a ``statement``
+attribute that describes the command which was executed. If your postcommand
+hook method gets called, you are guaranteed that the command method was called,
+and that it didn't raise an exception.
+
+If any postcommand hook raises an exception, the exception will be displayed to the user, and no further postcommand hook methods
+will be called. Command finalization hooks, if any, will be called.
+
+After all registered precommand hooks have been called,
+``self.postcmd(statement)`` will be called to retain full backward compatibility
+with ``cmd.Cmd``.
+
+If any postcommand hook (registered or ``self.postcmd()``) returns ``True``,
+subsequent postcommand hooks will still be called, as will the command
+finalization hooks, but once those hooks have all been called, the application
+will terminate.
+
+Any postcommand hook can change the value of the ``stop`` parameter before
+returning it, and the modified value will be passed to the next postcommand
+hook. The value returned by the final postcommand hook will be passed to the
+command finalization hooks, which may further modify the value. If your hook
+blindly returns ``False``, a prior hook's requst to exit the application will
+not be honored. It's best to return the value you were passed unless you have a
+compelling reason to do otherwise.
+
+
+Command Finalization Hooks
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Command finalization hooks are called even if one of the other types of hooks or
+the command method raise an exception. Here's how to create and register a
+command finalization hook::
+
+ class App(cmd2.Cmd):
+ def __init__(self, *args, *kwargs):
+ super().__init__(*args, **kwargs)
+ self.register_cmdfinalization_hook(self.myhookmethod)
+
+ def myhookmethod(self, stop, statement):
+ return stop
+
+Command Finalization hooks must check whether the statement object is ``None``. There are certain circumstances where these hooks may be called before the statement has been parsed, so you can't always rely on having a statement.
+
+If any prior postparsing or precommand hook has requested the application to
+terminate, the value of the ``stop`` parameter passed to the first command
+finalization hook will be ``True``. Any command finalization hook can change the
+value of the ``stop`` parameter before returning it, and the modified value will
+be passed to the next command finalization hook. The value returned by the final
+command finalization hook will determine whether the application terminates or
+not.
+
+This approach to command finalization hooks can be powerful, but it can also
+cause problems. If your hook blindly returns ``False``, a prior hook's requst to
+exit the application will not be honored. It's best to return the value you were
+passed unless you have a compelling reason to do otherwise.
+
+If any command finalization hook raises an exception, no more command
+finalization hooks will be called. If the last hook to return a value returned
+``True``, then the exception will be rendered, and the application will
+terminate.
+
+Deprecated Application Lifecycle Hook Methods
+---------------------------------------------
+
+The ``preloop`` and ``postloop`` methods run before and after the main loop, respectively.
+
+.. automethod:: cmd2.cmd2.Cmd.preloop
+
+.. automethod:: cmd2.cmd2.Cmd.postloop
-Command Processing 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.
@@ -56,4 +327,4 @@ the various hook methods, presented in chronological order starting with the one
.. automethod:: cmd2.cmd2.Cmd.postcmd
-.. automethod:: cmd2.cmd2.Cmd.postparsing_postcmd
+.. automethod:: cmd2.cmd2.Cmd.postparsing_postcmd \ No newline at end of file
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index 59f9a610..0865b6f6 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -30,6 +30,7 @@ def test_parse_empty_string(parser):
statement = parser.parse('')
assert not statement.command
assert not statement.args
+ assert statement == ''
assert statement.raw == ''
@pytest.mark.parametrize('line,tokens', [
@@ -71,7 +72,8 @@ def test_command_and_args(parser, tokens, command, args):
def test_parse_single_word(parser, line):
statement = parser.parse(line)
assert statement.command == line
- assert not statement.args
+ assert statement.args is None
+ assert statement == ''
assert statement.argv == [utils.strip_quotes(line)]
@pytest.mark.parametrize('line,terminator', [
@@ -83,8 +85,10 @@ def test_parse_single_word(parser, line):
def test_parse_word_plus_terminator(parser, line, terminator):
statement = parser.parse(line)
assert statement.command == 'termbare'
- assert statement.terminator == terminator
+ assert statement.args is None
+ assert statement == ''
assert statement.argv == ['termbare']
+ assert statement.terminator == terminator
@pytest.mark.parametrize('line,terminator', [
('termbare; suffx', ';'),
@@ -95,15 +99,18 @@ def test_parse_word_plus_terminator(parser, line, terminator):
def test_parse_suffix_after_terminator(parser, line, terminator):
statement = parser.parse(line)
assert statement.command == 'termbare'
+ assert statement.args is None
+ assert statement == ''
+ assert statement.argv == ['termbare']
assert statement.terminator == terminator
assert statement.suffix == 'suffx'
- assert statement.argv == ['termbare']
def test_parse_command_with_args(parser):
line = 'command with args'
statement = parser.parse(line)
assert statement.command == 'command'
assert statement.args == 'with args'
+ assert statement == statement.args
assert statement.argv == ['command', 'with', 'args']
def test_parse_command_with_quoted_args(parser):
@@ -111,6 +118,7 @@ def test_parse_command_with_quoted_args(parser):
statement = parser.parse(line)
assert statement.command == 'command'
assert statement.args == 'with "quoted args" and "some not"'
+ assert statement == statement.args
assert statement.argv == ['command', 'with', 'quoted args', 'and', 'some not']
def test_parse_command_with_args_terminator_and_suffix(parser):
@@ -118,22 +126,25 @@ def test_parse_command_with_args_terminator_and_suffix(parser):
statement = parser.parse(line)
assert statement.command == 'command'
assert statement.args == "with args and terminator"
+ assert statement.argv == ['command', 'with', 'args', 'and', 'terminator']
+ assert statement == statement.args
assert statement.terminator == ';'
assert statement.suffix == 'and suffix'
- assert statement.argv == ['command', 'with', 'args', 'and', 'terminator']
def test_parse_hashcomment(parser):
statement = parser.parse('hi # this is all a comment')
assert statement.command == 'hi'
- assert not statement.args
+ assert statement.args is None
+ assert statement == ''
assert statement.argv == ['hi']
def test_parse_c_comment(parser):
statement = parser.parse('hi /* this is | all a comment */')
assert statement.command == 'hi'
- assert not statement.args
- assert not statement.pipe_to
+ assert statement.args is None
assert statement.argv == ['hi']
+ assert statement == ''
+ assert not statement.pipe_to
def test_parse_c_comment_empty(parser):
statement = parser.parse('/* this is | all a comment */')
@@ -141,13 +152,15 @@ def test_parse_c_comment_empty(parser):
assert not statement.args
assert not statement.pipe_to
assert not statement.argv
+ assert statement == ''
def test_parse_what_if_quoted_strings_seem_to_start_comments(parser):
statement = parser.parse('what if "quoted strings /* seem to " start comments?')
assert statement.command == 'what'
assert statement.args == 'if "quoted strings /* seem to " start comments?'
- assert not statement.pipe_to
assert statement.argv == ['what', 'if', 'quoted strings /* seem to ', 'start', 'comments?']
+ assert statement == statement.args
+ assert not statement.pipe_to
@pytest.mark.parametrize('line',[
'simple | piped',
@@ -156,7 +169,8 @@ def test_parse_what_if_quoted_strings_seem_to_start_comments(parser):
def test_parse_simple_pipe(parser, line):
statement = parser.parse(line)
assert statement.command == 'simple'
- assert not statement.args
+ assert statement.args is None
+ assert statement == ''
assert statement.argv == ['simple']
assert statement.pipe_to == ['piped']
@@ -165,6 +179,7 @@ def test_parse_double_pipe_is_not_a_pipe(parser):
statement = parser.parse(line)
assert statement.command == 'double-pipe'
assert statement.args == '|| is not a pipe'
+ assert statement == statement.args
assert statement.argv == ['double-pipe', '||', 'is', 'not', 'a', 'pipe']
assert not statement.pipe_to
@@ -174,6 +189,7 @@ def test_parse_complex_pipe(parser):
assert statement.command == 'command'
assert statement.args == "with args, terminator"
assert statement.argv == ['command', 'with', 'args,', 'terminator']
+ assert statement == statement.args
assert statement.terminator == '&'
assert statement.suffix == 'sufx'
assert statement.pipe_to == ['piped']
@@ -187,7 +203,8 @@ def test_parse_complex_pipe(parser):
def test_parse_redirect(parser,line, output):
statement = parser.parse(line)
assert statement.command == 'help'
- assert not statement.args
+ assert statement.args is None
+ assert statement == ''
assert statement.output == output
assert statement.output_to == 'out.txt'
@@ -196,6 +213,7 @@ def test_parse_redirect_with_args(parser):
statement = parser.parse(line)
assert statement.command == 'output'
assert statement.args == 'into'
+ assert statement == statement.args
assert statement.argv == ['output', 'into']
assert statement.output == '>'
assert statement.output_to == 'afile.txt'
@@ -205,6 +223,7 @@ def test_parse_redirect_with_dash_in_path(parser):
statement = parser.parse(line)
assert statement.command == 'output'
assert statement.args == 'into'
+ assert statement == statement.args
assert statement.argv == ['output', 'into']
assert statement.output == '>'
assert statement.output_to == 'python-cmd2/afile.txt'
@@ -214,6 +233,7 @@ def test_parse_redirect_append(parser):
statement = parser.parse(line)
assert statement.command == 'output'
assert statement.args == 'appended to'
+ assert statement == statement.args
assert statement.argv == ['output', 'appended', 'to']
assert statement.output == '>>'
assert statement.output_to == '/tmp/afile.txt'
@@ -223,6 +243,7 @@ def test_parse_pipe_and_redirect(parser):
statement = parser.parse(line)
assert statement.command == 'output'
assert statement.args == 'into'
+ assert statement == statement.args
assert statement.argv == ['output', 'into']
assert statement.terminator == ';'
assert statement.suffix == 'sufx'
@@ -235,6 +256,7 @@ def test_parse_output_to_paste_buffer(parser):
statement = parser.parse(line)
assert statement.command == 'output'
assert statement.args == 'to paste buffer'
+ assert statement == statement.args
assert statement.argv == ['output', 'to', 'paste', 'buffer']
assert statement.output == '>>'
@@ -246,6 +268,7 @@ def test_parse_redirect_inside_terminator(parser):
statement = parser.parse(line)
assert statement.command == 'has'
assert statement.args == '> inside'
+ assert statement == statement.args
assert statement.argv == ['has', '>', 'inside']
assert statement.terminator == ';'
@@ -263,6 +286,7 @@ def test_parse_multiple_terminators(parser, line, terminator):
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
assert statement.args == 'with | inside'
+ assert statement == statement.args
assert statement.argv == ['multiline', 'with', '|', 'inside']
assert statement.terminator == terminator
@@ -272,6 +296,7 @@ def test_parse_unfinished_multiliine_command(parser):
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
assert statement.args == 'has > inside an unfinished command'
+ assert statement == statement.args
assert statement.argv == ['multiline', 'has', '>', 'inside', 'an', 'unfinished', 'command']
assert not statement.terminator
@@ -286,6 +311,7 @@ def test_parse_multiline_command_ignores_redirectors_within_it(parser, line, ter
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
assert statement.args == 'has > inside'
+ assert statement == statement.args
assert statement.argv == ['multiline', 'has', '>', 'inside']
assert statement.terminator == terminator
@@ -297,6 +323,7 @@ def test_parse_multiline_with_incomplete_comment(parser):
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
assert statement.args == 'command'
+ assert statement == statement.args
assert statement.argv == ['multiline', 'command']
assert not statement.terminator
@@ -306,6 +333,7 @@ def test_parse_multiline_with_complete_comment(parser):
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
assert statement.args == 'command is done'
+ assert statement == statement.args
assert statement.argv == ['multiline', 'command', 'is', 'done']
assert statement.terminator == ';'
@@ -315,6 +343,7 @@ def test_parse_multiline_termninated_by_empty_line(parser):
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
assert statement.args == 'command ends'
+ assert statement == statement.args
assert statement.argv == ['multiline', 'command', 'ends']
assert statement.terminator == '\n'
@@ -324,6 +353,7 @@ def test_parse_multiline_ignores_terminators_in_comments(parser):
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
assert statement.args == 'command "with term; ends" now'
+ assert statement == statement.args
assert statement.argv == ['multiline', 'command', 'with term; ends', 'now']
assert statement.terminator == '\n'
@@ -332,6 +362,7 @@ def test_parse_command_with_unicode_args(parser):
statement = parser.parse(line)
assert statement.command == 'drink'
assert statement.args == 'café'
+ assert statement == statement.args
assert statement.argv == ['drink', 'café']
def test_parse_unicode_command(parser):
@@ -339,6 +370,7 @@ def test_parse_unicode_command(parser):
statement = parser.parse(line)
assert statement.command == 'café'
assert statement.args == 'au lait'
+ assert statement == statement.args
assert statement.argv == ['café', 'au', 'lait']
def test_parse_redirect_to_unicode_filename(parser):
@@ -346,6 +378,7 @@ def test_parse_redirect_to_unicode_filename(parser):
statement = parser.parse(line)
assert statement.command == 'dir'
assert statement.args == 'home'
+ assert statement == statement.args
assert statement.argv == ['dir', 'home']
assert statement.output == '>'
assert statement.output_to == 'café'
@@ -375,6 +408,10 @@ def test_parse_alias_and_shortcut_expansion(parser, line, command, args):
statement = parser.parse(line)
assert statement.command == command
assert statement.args == args
+ if statement.args is None:
+ assert statement == ''
+ else:
+ assert statement == statement.args
def test_parse_alias_on_multiline_command(parser):
line = 'anothermultiline has > inside an unfinished command'
@@ -382,6 +419,7 @@ def test_parse_alias_on_multiline_command(parser):
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
assert statement.args == 'has > inside an unfinished command'
+ assert statement == statement.args
assert not statement.terminator
@pytest.mark.parametrize('line,output', [
@@ -393,7 +431,8 @@ def test_parse_alias_on_multiline_command(parser):
def test_parse_alias_redirection(parser, line, output):
statement = parser.parse(line)
assert statement.command == 'help'
- assert not statement.args
+ assert statement.args is None
+ assert statement == ''
assert statement.output == output
assert statement.output_to == 'out.txt'
@@ -404,7 +443,8 @@ def test_parse_alias_redirection(parser, line, output):
def test_parse_alias_pipe(parser, line):
statement = parser.parse(line)
assert statement.command == 'help'
- assert not statement.args
+ assert statement.args is None
+ assert statement == ''
assert statement.pipe_to == ['less']
@pytest.mark.parametrize('line', [
@@ -418,7 +458,8 @@ def test_parse_alias_pipe(parser, line):
def test_parse_alias_terminator_no_whitespace(parser, line):
statement = parser.parse(line)
assert statement.command == 'help'
- assert not statement.args
+ assert statement.args is None
+ assert statement == ''
assert statement.terminator == ';'
def test_parse_command_only_command_and_args(parser):
@@ -426,6 +467,7 @@ def test_parse_command_only_command_and_args(parser):
statement = parser.parse_command_only(line)
assert statement.command == 'help'
assert statement.args == 'history'
+ assert statement == statement.args
assert statement.command_and_args == line
def test_parse_command_only_emptyline(parser):
@@ -434,9 +476,9 @@ def test_parse_command_only_emptyline(parser):
# statement is a subclass of str(), the value of the str
# should be '', to retain backwards compatibility with
# the cmd in the standard library
- assert statement == ''
assert statement.command is None
assert statement.args is None
+ assert statement == ''
assert not statement.argv
assert statement.command_and_args == None
@@ -445,6 +487,7 @@ def test_parse_command_only_strips_line(parser):
statement = parser.parse_command_only(line)
assert statement.command == 'help'
assert statement.args == 'history'
+ assert statement == statement.args
assert statement.command_and_args == line.strip()
def test_parse_command_only_expands_alias(parser):
@@ -452,12 +495,14 @@ def test_parse_command_only_expands_alias(parser):
statement = parser.parse_command_only(line)
assert statement.command == 'pyscript'
assert statement.args == 'foobar.py'
+ assert statement == statement.args
def test_parse_command_only_expands_shortcuts(parser):
line = '!cat foobar.txt'
statement = parser.parse_command_only(line)
assert statement.command == 'shell'
assert statement.args == 'cat foobar.txt'
+ assert statement == statement.args
assert statement.command_and_args == 'shell cat foobar.txt'
def test_parse_command_only_quoted_args(parser):
@@ -465,6 +510,7 @@ def test_parse_command_only_quoted_args(parser):
statement = parser.parse_command_only(line)
assert statement.command == 'shell'
assert statement.args == 'ls -al "/tmp/directory with spaces/doit.sh"'
+ assert statement == statement.args
assert statement.command_and_args == line.replace('l', 'shell ls -al')
@pytest.mark.parametrize('line', [
@@ -495,5 +541,22 @@ def test_parse_command_only_specialchars(parser, line):
])
def test_parse_command_only_none(parser, line):
statement = parser.parse_command_only(line)
- assert statement.command == None
- assert statement.args == None
+ assert statement.command is None
+ assert statement.args is None
+ assert statement == ''
+
+def test_statement_initialization(parser):
+ string = 'alias'
+ statement = cmd2.Statement(string)
+ assert string == statement
+ assert statement.raw is None
+ assert statement.command is None
+ assert statement.args is None
+ assert isinstance(statement.argv, list)
+ assert not statement.argv
+ assert statement.multiline_command is None
+ assert statement.terminator is None
+ assert statement.suffix is None
+ assert statement.pipe_to is None
+ assert statement.output is None
+ assert statement.output_to is None
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
new file mode 100644
index 00000000..fd8609c3
--- /dev/null
+++ b/tests/test_plugin.py
@@ -0,0 +1,799 @@
+# 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 cmd2 import plugin
+
+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_postparsing = 0
+ self.called_precmd = 0
+ self.called_postcmd = 0
+ self.called_cmdfinalization = 0
+
+ ###
+ #
+ # preloop and postloop hooks
+ # which share the same signature and are thus interchangable
+ #
+ ###
+ def prepost_hook_one(self) -> None:
+ "Method used for preloop or postloop hooks"
+ self.poutput("one")
+
+ def prepost_hook_two(self) -> None:
+ "Another method used for preloop or postloop hooks"
+ self.poutput("two")
+
+ def prepost_hook_too_many_parameters(self, param) -> None:
+ "A preloop or postloop hook with too many parameters"
+ pass
+
+ def prepost_hook_with_wrong_return_annotation(self) -> bool:
+ "A preloop or postloop hook with incorrect return type"
+ pass
+
+ ###
+ #
+ # Postparsing hooks
+ #
+ ###
+ def postparse_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
+ "A postparsing hook"
+ self.called_postparsing += 1
+ return data
+
+ def postparse_hook_stop(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
+ "A postparsing hook with requests application exit"
+ self.called_postparsing += 1
+ data.stop = True
+ return data
+
+ def postparse_hook_emptystatement(self, data: 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, data: 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, data1, data2) -> cmd2.plugin.PostparsingData:
+ "A postparsing hook with too many parameters"
+ pass
+
+ def postparse_hook_undeclared_parameter_annotation(self, data) -> cmd2.plugin.PostparsingData:
+ "A postparsing hook with an undeclared parameter type"
+ pass
+
+ def postparse_hook_wrong_parameter_annotation(self, data: str) -> cmd2.plugin.PostparsingData:
+ "A postparsing hook with the wrong parameter type"
+ pass
+
+ def postparse_hook_undeclared_return_annotation(self, data: cmd2.plugin.PostparsingData):
+ "A postparsing hook with an undeclared return type"
+ pass
+
+ def postparse_hook_wrong_return_annotation(self, data: cmd2.plugin.PostparsingData) -> str:
+ "A postparsing hook with the wrong return type"
+ pass
+
+ ###
+ #
+ # precommand hooks, some valid, some invalid
+ #
+ ###
+ def precmd(self, statement: cmd2.Statement) -> cmd2.Statement:
+ "Override cmd.Cmd method"
+ self.called_precmd += 1
+ return statement
+
+ def precmd_hook(self, data: plugin.PrecommandData) -> plugin.PrecommandData:
+ "A precommand hook"
+ self.called_precmd += 1
+ return data
+
+ 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, 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, data) -> plugin.PrecommandData:
+ "A precommand hook with no type annotation on the parameter"
+ return data
+
+ def precmd_hook_wrong_parameter_annotation(self, data: str) -> plugin.PrecommandData:
+ "A precommand hook with the incorrect type annotation on the parameter"
+ return data
+
+ def precmd_hook_no_return_annotation(self, data: plugin.PrecommandData):
+ "A precommand hook with no type annotation on the return value"
+ return data
+
+ def precmd_hook_wrong_return_annotation(self, data: plugin.PrecommandData) -> cmd2.Statement:
+ return self.statement_parser.parse('hi there')
+
+ ###
+ #
+ # postcommand hooks, some valid, some invalid
+ #
+ ###
+ def postcmd(self, stop: bool, statement: cmd2.Statement) -> bool:
+ "Override cmd.Cmd method"
+ self.called_postcmd += 1
+ return stop
+
+ def postcmd_hook(self, data: plugin.PostcommandData) -> plugin.PostcommandData:
+ "A postcommand hook"
+ self.called_postcmd += 1
+ return data
+
+ def postcmd_hook_exception(self, data: plugin.PostcommandData) -> plugin.PostcommandData:
+ "A postcommand hook with raises an exception"
+ self.called_postcmd += 1
+ raise ZeroDivisionError
+
+ def postcmd_hook_not_enough_parameters(self) -> plugin.PostcommandData:
+ "A precommand hook with no parameters"
+ pass
+
+ def postcmd_hook_too_many_parameters(self, one: plugin.PostcommandData, two: str) -> plugin.PostcommandData:
+ "A precommand hook with too many parameters"
+ return one
+
+ def postcmd_hook_no_parameter_annotation(self, data) -> plugin.PostcommandData:
+ "A precommand hook with no type annotation on the parameter"
+ return data
+
+ def postcmd_hook_wrong_parameter_annotation(self, data: str) -> plugin.PostcommandData:
+ "A precommand hook with the incorrect type annotation on the parameter"
+ return data
+
+ def postcmd_hook_no_return_annotation(self, data: plugin.PostcommandData):
+ "A precommand hook with no type annotation on the return value"
+ return data
+
+ def postcmd_hook_wrong_return_annotation(self, data: plugin.PostcommandData) -> cmd2.Statement:
+ return self.statement_parser.parse('hi there')
+
+ ###
+ #
+ # command finalization hooks, some valid, some invalid
+ #
+ ###
+ def cmdfinalization_hook(self, data: plugin.CommandFinalizationData) -> plugin.CommandFinalizationData:
+ """A command finalization hook."""
+ self.called_cmdfinalization += 1
+ return data
+
+ def cmdfinalization_hook_stop(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData:
+ "A postparsing hook which requests application exit"
+ self.called_cmdfinalization += 1
+ data.stop = True
+ return data
+
+ def cmdfinalization_hook_exception(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData:
+ "A postparsing hook which raises an exception"
+ self.called_cmdfinalization += 1
+ raise ValueError
+
+ def cmdfinalization_hook_not_enough_parameters(self) -> plugin.CommandFinalizationData:
+ """A command finalization hook with no parameters."""
+ pass
+
+ def cmdfinalization_hook_too_many_parameters(self, one: plugin.CommandFinalizationData, two: str) -> plugin.CommandFinalizationData:
+ """A command finalization hook with too many parameters."""
+ return one
+
+ def cmdfinalization_hook_no_parameter_annotation(self, data) -> plugin.CommandFinalizationData:
+ """A command finalization hook with no type annotation on the parameter."""
+ return data
+
+ def cmdfinalization_hook_wrong_parameter_annotation(self, data: str) -> plugin.CommandFinalizationData:
+ """A command finalization hook with the incorrect type annotation on the parameter."""
+ return data
+
+ def cmdfinalization_hook_no_return_annotation(self, data: plugin.CommandFinalizationData):
+ """A command finalizationhook with no type annotation on the return value."""
+ return data
+
+ def cmdfinalization_hook_wrong_return_annotation(self, data: plugin.CommandFinalizationData) -> cmd2.Statement:
+ """A command finalization hook with the wrong return type annotation."""
+ return self.statement_parser.parse('hi there')
+
+
+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 pre and postloop hooks
+#
+###
+def test_register_preloop_hook_too_many_parameters():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_preloop_hook(app.prepost_hook_too_many_parameters)
+
+def test_register_preloop_hook_with_return_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_preloop_hook(app.prepost_hook_with_wrong_return_annotation)
+
+def test_preloop_hook(capsys):
+ app = PluggedApp()
+ app.register_preloop_hook(app.prepost_hook_one)
+ app.cmdqueue.append('say hello')
+ app.cmdqueue.append('quit')
+ app.cmdloop()
+ out, err = capsys.readouterr()
+ assert out == 'one\nhello\n'
+ assert not err
+
+def test_preloop_hooks(capsys):
+ app = PluggedApp()
+ app.register_preloop_hook(app.prepost_hook_one)
+ app.register_preloop_hook(app.prepost_hook_two)
+ app.cmdqueue.append('say hello')
+ app.cmdqueue.append('quit')
+ app.cmdloop()
+ out, err = capsys.readouterr()
+ assert out == 'one\ntwo\nhello\n'
+ assert not err
+
+def test_register_postloop_hook_too_many_parameters():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postloop_hook(app.prepost_hook_too_many_parameters)
+
+def test_register_postloop_hook_with_wrong_return_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postloop_hook(app.prepost_hook_with_wrong_return_annotation)
+
+def test_postloop_hook(capsys):
+ app = PluggedApp()
+ app.register_postloop_hook(app.prepost_hook_one)
+ app.cmdqueue.append('say hello')
+ app.cmdqueue.append('quit')
+ app.cmdloop()
+ out, err = capsys.readouterr()
+ assert out == 'hello\none\n'
+ assert not err
+
+def test_postloop_hooks(capsys):
+ app = PluggedApp()
+ app.register_postloop_hook(app.prepost_hook_one)
+ app.register_postloop_hook(app.prepost_hook_two)
+ app.cmdqueue.append('say hello')
+ app.cmdqueue.append('quit')
+ app.cmdloop()
+ out, err = capsys.readouterr()
+ assert out == 'hello\none\ntwo\n'
+ assert not err
+
+###
+#
+# 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_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postparsing_hook(app.postparse_hook_undeclared_parameter_annotation)
+
+def test_postparsing_hook_wrong_parameter_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postparsing_hook(app.postparse_hook_wrong_parameter_annotation)
+
+def test_postparsing_hook_undeclared_return_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postparsing_hook(app.postparse_hook_undeclared_return_annotation)
+
+def test_postparsing_hook_wrong_return_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postparsing_hook(app.postparse_hook_wrong_return_annotation)
+
+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_postparsing
+
+ app.reset_counters()
+ app.register_postparsing_hook(app.postparse_hook)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ assert app.called_postparsing == 1
+
+ # register the function again, so it should be called twice
+ app.reset_counters()
+ app.register_postparsing_hook(app.postparse_hook)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ assert app.called_postparsing == 2
+
+def test_postparsing_hook_stop_first(capsys):
+ app = PluggedApp()
+ app.register_postparsing_hook(app.postparse_hook_stop)
+ stop = app.onecmd_plus_hooks('say hello')
+ assert app.called_postparsing == 1
+ assert stop
+
+ # register another function but it shouldn't be called
+ app.reset_counters()
+ app.register_postparsing_hook(app.postparse_hook)
+ stop = app.onecmd_plus_hooks('say hello')
+ assert app.called_postparsing == 1
+ assert stop
+
+def test_postparsing_hook_stop_second(capsys):
+ app = PluggedApp()
+ app.register_postparsing_hook(app.postparse_hook)
+ stop = app.onecmd_plus_hooks('say hello')
+ assert app.called_postparsing == 1
+ assert not stop
+
+ # register another function and make sure it gets called
+ app.reset_counters()
+ app.register_postparsing_hook(app.postparse_hook_stop)
+ stop = app.onecmd_plus_hooks('say hello')
+ assert app.called_postparsing == 2
+ assert stop
+
+ # register a third function which shouldn't be called
+ app.reset_counters()
+ app.register_postparsing_hook(app.postparse_hook)
+ stop = app.onecmd_plus_hooks('say hello')
+ assert app.called_postparsing == 2
+ assert stop
+
+def test_postparsing_hook_emptystatement_first(capsys):
+ app = PluggedApp()
+ app.register_postparsing_hook(app.postparse_hook_emptystatement)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert not out
+ assert not err
+ assert app.called_postparsing == 1
+
+ # register another function but it shouldn't be called
+ app.reset_counters()
+ stop = app.register_postparsing_hook(app.postparse_hook)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert not out
+ assert not err
+ assert app.called_postparsing == 1
+
+def test_postparsing_hook_emptystatement_second(capsys):
+ app = PluggedApp()
+ app.register_postparsing_hook(app.postparse_hook)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert out == 'hello\n'
+ assert not err
+ assert app.called_postparsing == 1
+
+ # register another function and make sure it gets called
+ app.reset_counters()
+ app.register_postparsing_hook(app.postparse_hook_emptystatement)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert not out
+ assert not err
+ assert app.called_postparsing == 2
+
+ # register a third function which shouldn't be called
+ app.reset_counters()
+ app.register_postparsing_hook(app.postparse_hook)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert not out
+ assert not err
+ assert app.called_postparsing == 2
+
+def test_postparsing_hook_exception(capsys):
+ app = PluggedApp()
+ app.register_postparsing_hook(app.postparse_hook_exception)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert not out
+ assert err
+ assert app.called_postparsing == 1
+
+ # register another function, but it shouldn't be called
+ app.reset_counters()
+ app.register_postparsing_hook(app.postparse_hook)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert not out
+ assert err
+ assert app.called_postparsing == 1
+
+###
+#
+# 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')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ # without registering any hooks, precmd() should be called
+ assert app.called_precmd == 1
+
+ app.reset_counters()
+ app.register_precmd_hook(app.precmd_hook)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ # with one hook registered, we should get precmd() and the hook
+ assert app.called_precmd == 2
+
+ # register the function again, so it should be called twice
+ app.reset_counters()
+ app.register_precmd_hook(app.precmd_hook)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ # with two hooks registered, we should get precmd() and both hooks
+ assert app.called_precmd == 3
+
+def test_precmd_hook_emptystatement_first(capsys):
+ app = PluggedApp()
+ app.register_precmd_hook(app.precmd_hook_emptystatement)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert not out
+ assert not err
+ # since the registered hooks are called before precmd(), if a registered
+ # hook throws an exception, precmd() is never called
+ assert app.called_precmd == 1
+
+ # register another function but it shouldn't be called
+ app.reset_counters()
+ stop = app.register_precmd_hook(app.precmd_hook)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert not out
+ assert not err
+ # the exception raised by the first hook should prevent the second
+ # hook from being called, and it also prevents precmd() from being
+ # called
+ assert app.called_precmd == 1
+
+def test_precmd_hook_emptystatement_second(capsys):
+ app = PluggedApp()
+ app.register_precmd_hook(app.precmd_hook)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert out == 'hello\n'
+ assert not err
+ # with one hook registered, we should get precmd() and the hook
+ assert app.called_precmd == 2
+
+ # register another function and make sure it gets called
+ app.reset_counters()
+ app.register_precmd_hook(app.precmd_hook_emptystatement)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert not out
+ assert not err
+ # since the registered hooks are called before precmd(), if a registered
+ # hook throws an exception, precmd() is never called
+ assert app.called_precmd == 2
+
+ # register a third function which shouldn't be called
+ app.reset_counters()
+ app.register_precmd_hook(app.precmd_hook)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert not out
+ assert not err
+ # the exception raised by the second hook should prevent the third
+ # hook from being called. since the registered hooks are called before precmd(),
+ # if a registered hook throws an exception, precmd() is never called
+ assert app.called_precmd == 2
+
+###
+#
+# test postcmd hooks
+#
+####
+def test_register_postcmd_hook_parameter_count():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postcmd_hook(app.postcmd_hook_not_enough_parameters)
+ with pytest.raises(TypeError):
+ app.register_postcmd_hook(app.postcmd_hook_too_many_parameters)
+
+def test_register_postcmd_hook_no_parameter_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postcmd_hook(app.postcmd_hook_no_parameter_annotation)
+
+def test_register_postcmd_hook_wrong_parameter_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postcmd_hook(app.postcmd_hook_wrong_parameter_annotation)
+
+def test_register_postcmd_hook_no_return_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postcmd_hook(app.postcmd_hook_no_return_annotation)
+
+def test_register_postcmd_hook_wrong_return_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_postcmd_hook(app.postcmd_hook_wrong_return_annotation)
+
+def test_postcmd(capsys):
+ app = PluggedApp()
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ # without registering any hooks, postcmd() should be called
+ assert app.called_postcmd == 1
+
+ app.reset_counters()
+ app.register_postcmd_hook(app.postcmd_hook)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ # with one hook registered, we should get precmd() and the hook
+ assert app.called_postcmd == 2
+
+ # register the function again, so it should be called twice
+ app.reset_counters()
+ app.register_postcmd_hook(app.postcmd_hook)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ # with two hooks registered, we should get precmd() and both hooks
+ assert app.called_postcmd == 3
+
+def test_postcmd_exception_first(capsys):
+ app = PluggedApp()
+ app.register_postcmd_hook(app.postcmd_hook_exception)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert out == 'hello\n'
+ assert err
+ # since the registered hooks are called before postcmd(), if a registered
+ # hook throws an exception, postcmd() is never called. So we should have
+ # a count of one because we called the hook that raised the exception
+ assert app.called_postcmd == 1
+
+ # register another function but it shouldn't be called
+ app.reset_counters()
+ stop = app.register_postcmd_hook(app.postcmd_hook)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert out == 'hello\n'
+ assert err
+ # the exception raised by the first hook should prevent the second
+ # hook from being called, and it also prevents postcmd() from being
+ # called
+ assert app.called_postcmd == 1
+
+def test_postcmd_exception_second(capsys):
+ app = PluggedApp()
+ app.register_postcmd_hook(app.postcmd_hook)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert out == 'hello\n'
+ assert not err
+ # with one hook registered, we should get the hook and postcmd()
+ assert app.called_postcmd == 2
+
+ # register another function which should be called
+ app.reset_counters()
+ stop = app.register_postcmd_hook(app.postcmd_hook_exception)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert out == 'hello\n'
+ assert err
+ # the exception raised by the first hook should prevent the second
+ # hook from being called, and it also prevents postcmd() from being
+ # called. So we have the first hook, and the second hook, which raised
+ # the exception
+ assert app.called_postcmd == 2
+
+##
+#
+# command finalization
+#
+###
+def test_register_cmdfinalization_hook_parameter_count():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook_not_enough_parameters)
+ with pytest.raises(TypeError):
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook_too_many_parameters)
+
+def test_register_cmdfinalization_hook_no_parameter_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook_no_parameter_annotation)
+
+def test_register_cmdfinalization_hook_wrong_parameter_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook_wrong_parameter_annotation)
+
+def test_register_cmdfinalization_hook_no_return_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook_no_return_annotation)
+
+def test_register_cmdfinalization_hook_wrong_return_annotation():
+ app = PluggedApp()
+ with pytest.raises(TypeError):
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook_wrong_return_annotation)
+
+def test_cmdfinalization(capsys):
+ app = PluggedApp()
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ assert app.called_cmdfinalization == 0
+
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ assert app.called_cmdfinalization == 1
+
+ # register the function again, so it should be called twice
+ app.reset_counters()
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook)
+ app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ assert app.called_cmdfinalization == 2
+
+def test_cmdfinalization_stop_first(capsys):
+ app = PluggedApp()
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook_stop)
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ assert app.called_cmdfinalization == 2
+ assert stop
+
+def test_cmdfinalization_stop_second(capsys):
+ app = PluggedApp()
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook)
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook_stop)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert out == 'hello\n'
+ assert not err
+ assert app.called_cmdfinalization == 2
+ assert stop
+
+def test_cmdfinalization_hook_exception(capsys):
+ app = PluggedApp()
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook_exception)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert out == 'hello\n'
+ assert err
+ assert app.called_cmdfinalization == 1
+
+ # register another function, but it shouldn't be called
+ app.reset_counters()
+ app.register_cmdfinalization_hook(app.cmdfinalization_hook)
+ stop = app.onecmd_plus_hooks('say hello')
+ out, err = capsys.readouterr()
+ assert not stop
+ assert out == 'hello\n'
+ assert err
+ assert app.called_cmdfinalization == 1