353 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
			
		
		
	
	
			353 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
| .. _`writinghooks`:
 | |
| 
 | |
| Writing hook functions
 | |
| ======================
 | |
| 
 | |
| 
 | |
| .. _validation:
 | |
| 
 | |
| hook function validation and execution
 | |
| --------------------------------------
 | |
| 
 | |
| pytest calls hook functions from registered plugins for any
 | |
| given hook specification.  Let's look at a typical hook function
 | |
| for the ``pytest_collection_modifyitems(session, config,
 | |
| items)`` hook which pytest calls after collection of all test items is
 | |
| completed.
 | |
| 
 | |
| When we implement a ``pytest_collection_modifyitems`` function in our plugin
 | |
| pytest will during registration verify that you use argument
 | |
| names which match the specification and bail out if not.
 | |
| 
 | |
| Let's look at a possible implementation:
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     def pytest_collection_modifyitems(config, items):
 | |
|         # called after collection is completed
 | |
|         # you can modify the ``items`` list
 | |
|         ...
 | |
| 
 | |
| Here, ``pytest`` will pass in ``config`` (the pytest config object)
 | |
| and ``items`` (the list of collected test items) but will not pass
 | |
| in the ``session`` argument because we didn't list it in the function
 | |
| signature.  This dynamic "pruning" of arguments allows ``pytest`` to
 | |
| be "future-compatible": we can introduce new hook named parameters without
 | |
| breaking the signatures of existing hook implementations.  It is one of
 | |
| the reasons for the general long-lived compatibility of pytest plugins.
 | |
| 
 | |
| Note that hook functions other than ``pytest_runtest_*`` are not
 | |
| allowed to raise exceptions.  Doing so will break the pytest run.
 | |
| 
 | |
| 
 | |
| 
 | |
| .. _firstresult:
 | |
| 
 | |
| firstresult: stop at first non-None result
 | |
| -------------------------------------------
 | |
| 
 | |
| Most calls to ``pytest`` hooks result in a **list of results** which contains
 | |
| all non-None results of the called hook functions.
 | |
| 
 | |
| Some hook specifications use the ``firstresult=True`` option so that the hook
 | |
| call only executes until the first of N registered functions returns a
 | |
| non-None result which is then taken as result of the overall hook call.
 | |
| The remaining hook functions will not be called in this case.
 | |
| 
 | |
| .. _`hookwrapper`:
 | |
| 
 | |
| hookwrapper: executing around other hooks
 | |
| -------------------------------------------------
 | |
| 
 | |
| .. currentmodule:: _pytest.core
 | |
| 
 | |
| 
 | |
| 
 | |
| pytest plugins can implement hook wrappers which wrap the execution
 | |
| of other hook implementations.  A hook wrapper is a generator function
 | |
| which yields exactly once. When pytest invokes hooks it first executes
 | |
| hook wrappers and passes the same arguments as to the regular hooks.
 | |
| 
 | |
| At the yield point of the hook wrapper pytest will execute the next hook
 | |
| implementations and return their result to the yield point in the form of
 | |
| a :py:class:`Result <pluggy._Result>` instance which encapsulates a result or
 | |
| exception info.  The yield point itself will thus typically not raise
 | |
| exceptions (unless there are bugs).
 | |
| 
 | |
| Here is an example definition of a hook wrapper:
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     import pytest
 | |
| 
 | |
| 
 | |
|     @pytest.hookimpl(hookwrapper=True)
 | |
|     def pytest_pyfunc_call(pyfuncitem):
 | |
|         do_something_before_next_hook_executes()
 | |
| 
 | |
|         outcome = yield
 | |
|         # outcome.excinfo may be None or a (cls, val, tb) tuple
 | |
| 
 | |
|         res = outcome.get_result()  # will raise if outcome was exception
 | |
| 
 | |
|         post_process_result(res)
 | |
| 
 | |
|         outcome.force_result(new_res)  # to override the return value to the plugin system
 | |
| 
 | |
| Note that hook wrappers don't return results themselves, they merely
 | |
| perform tracing or other side effects around the actual hook implementations.
 | |
| If the result of the underlying hook is a mutable object, they may modify
 | |
| that result but it's probably better to avoid it.
 | |
| 
 | |
| For more information, consult the
 | |
| :ref:`pluggy documentation about hookwrappers <pluggy:hookwrappers>`.
 | |
| 
 | |
| .. _plugin-hookorder:
 | |
| 
 | |
| Hook function ordering / call example
 | |
| -------------------------------------
 | |
| 
 | |
| For any given hook specification there may be more than one
 | |
| implementation and we thus generally view ``hook`` execution as a
 | |
| ``1:N`` function call where ``N`` is the number of registered functions.
 | |
| There are ways to influence if a hook implementation comes before or
 | |
| after others, i.e.  the position in the ``N``-sized list of functions:
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     # Plugin 1
 | |
|     @pytest.hookimpl(tryfirst=True)
 | |
|     def pytest_collection_modifyitems(items):
 | |
|         # will execute as early as possible
 | |
|         ...
 | |
| 
 | |
| 
 | |
|     # Plugin 2
 | |
|     @pytest.hookimpl(trylast=True)
 | |
|     def pytest_collection_modifyitems(items):
 | |
|         # will execute as late as possible
 | |
|         ...
 | |
| 
 | |
| 
 | |
|     # Plugin 3
 | |
|     @pytest.hookimpl(hookwrapper=True)
 | |
|     def pytest_collection_modifyitems(items):
 | |
|         # will execute even before the tryfirst one above!
 | |
|         outcome = yield
 | |
|         # will execute after all non-hookwrappers executed
 | |
| 
 | |
| Here is the order of execution:
 | |
| 
 | |
| 1. Plugin3's pytest_collection_modifyitems called until the yield point
 | |
|    because it is a hook wrapper.
 | |
| 
 | |
| 2. Plugin1's pytest_collection_modifyitems is called because it is marked
 | |
|    with ``tryfirst=True``.
 | |
| 
 | |
| 3. Plugin2's pytest_collection_modifyitems is called because it is marked
 | |
|    with ``trylast=True`` (but even without this mark it would come after
 | |
|    Plugin1).
 | |
| 
 | |
| 4. Plugin3's pytest_collection_modifyitems then executing the code after the yield
 | |
|    point.  The yield receives a :py:class:`Result <pluggy._Result>` instance which encapsulates
 | |
|    the result from calling the non-wrappers.  Wrappers shall not modify the result.
 | |
| 
 | |
| It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with
 | |
| ``hookwrapper=True`` in which case it will influence the ordering of hookwrappers
 | |
| among each other.
 | |
| 
 | |
| 
 | |
| Declaring new hooks
 | |
| ------------------------
 | |
| 
 | |
| .. note::
 | |
| 
 | |
|     This is a quick overview on how to add new hooks and how they work in general, but a more complete
 | |
|     overview can be found in `the pluggy documentation <https://pluggy.readthedocs.io/en/latest/>`__.
 | |
| 
 | |
| .. currentmodule:: _pytest.hookspec
 | |
| 
 | |
| Plugins and ``conftest.py`` files may declare new hooks that can then be
 | |
| implemented by other plugins in order to alter behaviour or interact with
 | |
| the new plugin:
 | |
| 
 | |
| .. autofunction:: pytest_addhooks
 | |
|     :noindex:
 | |
| 
 | |
| Hooks are usually declared as do-nothing functions that contain only
 | |
| documentation describing when the hook will be called and what return values
 | |
| are expected. The names of the functions must start with `pytest_` otherwise pytest won't recognize them.
 | |
| 
 | |
| Here's an example. Let's assume this code is in the ``sample_hook.py`` module.
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     def pytest_my_hook(config):
 | |
|         """
 | |
|         Receives the pytest config and does things with it
 | |
|         """
 | |
| 
 | |
| To register the hooks with pytest they need to be structured in their own module or class. This
 | |
| class or module can then be passed to the ``pluginmanager`` using the ``pytest_addhooks`` function
 | |
| (which itself is a hook exposed by pytest).
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     def pytest_addhooks(pluginmanager):
 | |
|         """This example assumes the hooks are grouped in the 'sample_hook' module."""
 | |
|         from my_app.tests import sample_hook
 | |
| 
 | |
|         pluginmanager.add_hookspecs(sample_hook)
 | |
| 
 | |
| For a real world example, see `newhooks.py`_ from `xdist <https://github.com/pytest-dev/pytest-xdist>`_.
 | |
| 
 | |
| .. _`newhooks.py`: https://github.com/pytest-dev/pytest-xdist/blob/974bd566c599dc6a9ea291838c6f226197208b46/xdist/newhooks.py
 | |
| 
 | |
| Hooks may be called both from fixtures or from other hooks. In both cases, hooks are called
 | |
| through the ``hook`` object, available in the ``config`` object. Most hooks receive a
 | |
| ``config`` object directly, while fixtures may use the ``pytestconfig`` fixture which provides the same object.
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     @pytest.fixture()
 | |
|     def my_fixture(pytestconfig):
 | |
|         # call the hook called "pytest_my_hook"
 | |
|         # 'result' will be a list of return values from all registered functions.
 | |
|         result = pytestconfig.hook.pytest_my_hook(config=pytestconfig)
 | |
| 
 | |
| .. note::
 | |
|     Hooks receive parameters using only keyword arguments.
 | |
| 
 | |
| Now your hook is ready to be used. To register a function at the hook, other plugins or users must
 | |
| now simply define the function ``pytest_my_hook`` with the correct signature in their ``conftest.py``.
 | |
| 
 | |
| Example:
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     def pytest_my_hook(config):
 | |
|         """
 | |
|         Print all active hooks to the screen.
 | |
|         """
 | |
|         print(config.hook)
 | |
| 
 | |
| 
 | |
| .. _`addoptionhooks`:
 | |
| 
 | |
| 
 | |
| Using hooks in pytest_addoption
 | |
| -------------------------------
 | |
| 
 | |
| Occasionally, it is necessary to change the way in which command line options
 | |
| are defined by one plugin based on hooks in another plugin. For example,
 | |
| a plugin may expose a command line option for which another plugin needs
 | |
| to define the default value. The pluginmanager can be used to install and
 | |
| use hooks to accomplish this. The plugin would define and add the hooks
 | |
| and use pytest_addoption as follows:
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|    # contents of hooks.py
 | |
| 
 | |
|    # Use firstresult=True because we only want one plugin to define this
 | |
|    # default value
 | |
|    @hookspec(firstresult=True)
 | |
|    def pytest_config_file_default_value():
 | |
|        """Return the default value for the config file command line option."""
 | |
| 
 | |
| 
 | |
|    # contents of myplugin.py
 | |
| 
 | |
| 
 | |
|    def pytest_addhooks(pluginmanager):
 | |
|        """This example assumes the hooks are grouped in the 'hooks' module."""
 | |
|        from . import hooks
 | |
| 
 | |
|        pluginmanager.add_hookspecs(hooks)
 | |
| 
 | |
| 
 | |
|    def pytest_addoption(parser, pluginmanager):
 | |
|        default_value = pluginmanager.hook.pytest_config_file_default_value()
 | |
|        parser.addoption(
 | |
|            "--config-file",
 | |
|            help="Config file to use, defaults to %(default)s",
 | |
|            default=default_value,
 | |
|        )
 | |
| 
 | |
| The conftest.py that is using myplugin would simply define the hook as follows:
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     def pytest_config_file_default_value():
 | |
|         return "config.yaml"
 | |
| 
 | |
| 
 | |
| Optionally using hooks from 3rd party plugins
 | |
| ---------------------------------------------
 | |
| 
 | |
| Using new hooks from plugins as explained above might be a little tricky
 | |
| because of the standard :ref:`validation mechanism <validation>`:
 | |
| if you depend on a plugin that is not installed, validation will fail and
 | |
| the error message will not make much sense to your users.
 | |
| 
 | |
| One approach is to defer the hook implementation to a new plugin instead of
 | |
| declaring the hook functions directly in your plugin module, for example:
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     # contents of myplugin.py
 | |
| 
 | |
| 
 | |
|     class DeferPlugin:
 | |
|         """Simple plugin to defer pytest-xdist hook functions."""
 | |
| 
 | |
|         def pytest_testnodedown(self, node, error):
 | |
|             """standard xdist hook function."""
 | |
| 
 | |
| 
 | |
|     def pytest_configure(config):
 | |
|         if config.pluginmanager.hasplugin("xdist"):
 | |
|             config.pluginmanager.register(DeferPlugin())
 | |
| 
 | |
| This has the added benefit of allowing you to conditionally install hooks
 | |
| depending on which plugins are installed.
 | |
| 
 | |
| .. _plugin-stash:
 | |
| 
 | |
| Storing data on items across hook functions
 | |
| -------------------------------------------
 | |
| 
 | |
| Plugins often need to store data on :class:`~pytest.Item`\s in one hook
 | |
| implementation, and access it in another. One common solution is to just
 | |
| assign some private attribute directly on the item, but type-checkers like
 | |
| mypy frown upon this, and it may also cause conflicts with other plugins.
 | |
| So pytest offers a better way to do this, :attr:`item.stash <_pytest.nodes.Node.stash>`.
 | |
| 
 | |
| To use the "stash" in your plugins, first create "stash keys" somewhere at the
 | |
| top level of your plugin:
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     been_there_key = pytest.StashKey[bool]()
 | |
|     done_that_key = pytest.StashKey[str]()
 | |
| 
 | |
| then use the keys to stash your data at some point:
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     def pytest_runtest_setup(item: pytest.Item) -> None:
 | |
|         item.stash[been_there_key] = True
 | |
|         item.stash[done_that_key] = "no"
 | |
| 
 | |
| and retrieve them at another point:
 | |
| 
 | |
| .. code-block:: python
 | |
| 
 | |
|     def pytest_runtest_teardown(item: pytest.Item) -> None:
 | |
|         if not item.stash[been_there_key]:
 | |
|             print("Oh?")
 | |
|         item.stash[done_that_key] = "yes!"
 | |
| 
 | |
| Stashes are available on all node types (like :class:`~pytest.Class`,
 | |
| :class:`~pytest.Session`) and also on :class:`~pytest.Config`, if needed.
 |