1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957
|
``pluggy``
==========
**The pytest plugin system**
What is it?
***********
``pluggy`` is the crystallized core of :ref:`plugin management and hook
calling <pytest:writing-plugins>` for :std:doc:`pytest <pytest:index>`.
It enables `500+ plugins`_ to extend and customize ``pytest``'s default
behaviour. Even ``pytest`` itself is composed as a set of ``pluggy`` plugins
which are invoked in sequence according to a well defined set of protocols.
It gives users the ability to extend or modify the behaviour of a
``host program`` by installing a ``plugin`` for that program.
The plugin code will run as part of normal program execution, changing or
enhancing certain aspects of it.
In essence, ``pluggy`` enables function `hooking`_ so you can build
"pluggable" systems.
Why is it useful?
*****************
There are some established mechanisms for modifying the behavior of other
programs/libraries in Python like
`method overriding <https://en.wikipedia.org/wiki/Method_overriding>`_
(e.g. Jinja2) or
`monkey patching <https://en.wikipedia.org/wiki/Monkey_patch>`_ (e.g. gevent
or for :std:doc:`writing tests <pytest:how-to/monkeypatch>`).
These strategies become problematic though when several parties want to
participate in the modification of the same program. Therefore ``pluggy``
does not rely on these mechanisms to enable a more structured approach and
avoid unnecessary exposure of state and behaviour. This leads to a more
`loosely coupled <https://en.wikipedia.org/wiki/Loose_coupling>`_ relationship
between ``host`` and ``plugins``.
The ``pluggy`` approach puts the burden on the designer of the
``host program`` to think carefully about which objects are really
needed in a hook implementation. This gives ``plugin`` creators a clear
framework for how to extend the ``host`` via a well defined set of functions
and objects to work with.
How does it work?
*****************
Let us start with a short overview of what is involved:
* ``host`` or ``host program``: the program offering extensibility
by specifying ``hook functions`` and invoking their implementation(s) as
part of program execution
* ``plugin``: the program implementing (a subset of) the specified hooks and
participating in program execution when the implementations are invoked
by the ``host``
* ``pluggy``: connects ``host`` and ``plugins`` by using ...
- the hook :ref:`specifications <specs>` defining call signatures
provided by the ``host`` (a.k.a ``hookspecs`` - see :ref:`marking_hooks`)
- the hook :ref:`implementations <impls>` provided by registered
``plugins`` (a.k.a ``hookimpl`` - see `callbacks`_)
- the hook :ref:`caller <calling>` - a call loop triggered at appropriate
program positions in the ``host`` invoking the implementations and
collecting the results
... where for each registered hook *specification*, a hook *call* will
invoke up to ``N`` registered hook *implementations*.
* ``user``: the person who installed the ``host program`` and wants to
extend its functionality with ``plugins``. In the simplest case they install
the ``plugin`` in the same environment as the ``host`` and the magic will
happen when the ``host program`` is run the next time. Depending on
the ``plugin``, there might be other things they need to do. For example,
they might have to call the host with an additional commandline parameter
to the host that the ``plugin`` added.
A toy example
-------------
Let us demonstrate the core functionality in one module and show how you can
start experimenting with pluggy functionality.
.. literalinclude:: examples/toy-example.py
Running this directly gets us::
$ python docs/examples/toy-example.py
inside Plugin_2.myhook()
inside Plugin_1.myhook()
[-1, 3]
A complete example
------------------
Now let us demonstrate how this plays together in a vaguely real world scenario.
Let's assume our ``host program`` is called **eggsample** where some eggs will
be prepared and served with a tray containing condiments. As everybody knows:
the more cooks are involved the better the food, so let us make the process
pluggable and write a plugin that improves the meal with some spam and replaces
the steak sauce (nobody likes that anyway) with spam sauce (it's a thing - trust me).
.. note::
**naming markers**: ``HookSpecMarker`` and ``HookImplMarker`` must be
initialized with the name of the ``host`` project (the ``name``
parameter in ``setup()``) - so **eggsample** in our case.
**naming plugin projects**: they should be named in the form of
``<host>-<plugin>`` (e.g. ``pytest-xdist``), therefore we call our
plugin *eggsample-spam*.
The host
^^^^^^^^
``eggsample/eggsample/__init__.py``
.. literalinclude:: examples/eggsample/eggsample/__init__.py
``eggsample/eggsample/hookspecs.py``
.. literalinclude:: examples/eggsample/eggsample/hookspecs.py
``eggsample/eggsample/lib.py``
.. literalinclude:: examples/eggsample/eggsample/lib.py
``eggsample/eggsample/host.py``
.. literalinclude:: examples/eggsample/eggsample/host.py
``eggsample/setup.py``
.. literalinclude:: examples/eggsample/setup.py
Let's get cooking - we install the host and see what a program run looks like::
$ pip install --editable pluggy/docs/examples/eggsample
$ eggsample
Your food. Enjoy some egg, egg, salt, egg, egg, pepper, egg
Some condiments? We have pickled walnuts, steak sauce, mushy peas, mint sauce
The plugin
^^^^^^^^^^
``eggsample-spam/eggsample_spam.py``
.. literalinclude:: examples/eggsample-spam/eggsample_spam.py
``eggsample-spam/setup.py``
.. literalinclude:: examples/eggsample-spam/setup.py
Let's get cooking with more cooks - we install the plugin and and see what
we get::
$ pip install --editable pluggy/docs/examples/eggsample-spam
$ eggsample
Your food. Enjoy some egg, lovely spam, salt, egg, egg, egg, wonderous spam, egg, pepper
Some condiments? We have pickled walnuts, mushy peas, mint sauce, spam sauce
Now this is what I call a condiments tray!
More real world examples
------------------------
To see how ``pluggy`` is used in the real world, have a look at these projects
documentation and source code:
* :ref:`pytest <pytest:writing-plugins>`
* :std:doc:`tox <tox:plugins>`
* :std:doc:`devpi <devpi:devguide/index>`
For more details and advanced usage please read on.
.. _define:
Define and collect hooks
************************
A *plugin* is a :ref:`namespace <python:tut-scopes>` type (currently one of a
``class`` or module) which defines a set of *hook* functions.
As mentioned in :ref:`manage`, all *plugins* which specify *hooks*
are managed by an instance of a :py:class:`pluggy.PluginManager` which
defines the primary ``pluggy`` API.
In order for a :py:class:`~pluggy.PluginManager` to detect functions in a namespace
intended to be *hooks*, they must be decorated using special ``pluggy`` *marks*.
.. _marking_hooks:
Marking hooks
-------------
The :py:class:`~pluggy.HookspecMarker` and :py:class:`~pluggy.HookimplMarker`
decorators are used to *mark* functions for detection by a
:py:class:`~pluggy.PluginManager`:
.. code-block:: python
from pluggy import HookspecMarker, HookimplMarker
hookspec = HookspecMarker("project_name")
hookimpl = HookimplMarker("project_name")
Each decorator type takes a single ``project_name`` string as its
lone argument the value of which is used to mark hooks for detection by
a similarly configured :py:class:`~pluggy.PluginManager` instance.
That is, a *mark* type called with ``project_name`` returns an object which
can be used to decorate functions which will then be detected by a
:py:class:`~pluggy.PluginManager` which was instantiated with the same
``project_name`` value.
Furthermore, each *hookimpl* or *hookspec* decorator can configure the
underlying call-time behavior of each *hook* object by providing special
*options* passed as keyword arguments.
.. note::
The following sections correspond to similar documentation in
``pytest`` for :ref:`pytest:writinghooks` and can be used as
a supplementary resource.
.. _impls:
Implementations
---------------
A hook *implementation* (*hookimpl*) is just a (callback) function
which has been appropriately marked.
*hookimpls* are loaded from a plugin using the
:py:meth:`~pluggy.PluginManager.register()` method:
.. code-block:: python
import sys
from pluggy import PluginManager, HookimplMarker
hookimpl = HookimplMarker("myproject")
@hookimpl
def setup_project(config, args):
"""This hook is used to process the initial config
and possibly input arguments.
"""
if args:
config.process_args(args)
return config
pm = PluginManager("myproject")
# load all hookimpls from the local module's namespace
plugin_name = pm.register(sys.modules[__name__])
.. _optionalhook:
Optional validation
^^^^^^^^^^^^^^^^^^^
Normally each *hookimpl* should be validated against a corresponding
hook :ref:`specification <specs>`. If you want to make an exception
then the *hookimpl* should be marked with the ``"optionalhook"`` option:
.. code-block:: python
@hookimpl(optionalhook=True)
def setup_project(config, args):
"""This hook is used to process the initial config
and possibly input arguments.
"""
if args:
config.process_args(args)
return config
.. _specname:
Hookspec name matching
^^^^^^^^^^^^^^^^^^^^^^
During plugin :ref:`registration <registration>`, pluggy attempts to match each
hook implementation declared by the *plugin* to a hook
:ref:`specification <specs>` in the *host* program with the **same name** as
the function being decorated by ``@hookimpl`` (e.g. ``setup_project`` in the
example above). Note: there is *no* strict requirement that each *hookimpl*
has a corresponding *hookspec* (see
:ref:`enforcing spec validation <enforcing>`).
*new in version 0.13.2:*
To override the default behavior, a *hookimpl* may also be matched to a
*hookspec* in the *host* program with a non-matching function name by using
the ``specname`` option. Continuing the example above, the *hookimpl* function
does not need to be named ``setup_project``, but if the argument
``specname="setup_project"`` is provided to the ``hookimpl`` decorator, it will
be matched and checked against the ``setup_project`` hookspec:
.. code-block:: python
@hookimpl(specname="setup_project")
def any_plugin_function(config, args):
"""This hook is used to process the initial config
and possibly input arguments.
"""
if args:
config.process_args(args)
return config
Call time order
^^^^^^^^^^^^^^^
By default hooks are :ref:`called <calling>` in LIFO registered order, however,
a *hookimpl* can influence its call-time invocation position using special
attributes. If marked with a ``"tryfirst"`` or ``"trylast"`` option it
will be executed *first* or *last* respectively in the hook call loop:
.. code-block:: python
import sys
from pluggy import PluginManager, HookimplMarker
hookimpl = HookimplMarker("myproject")
@hookimpl(trylast=True)
def setup_project(config, args):
"""Default implementation."""
if args:
config.process_args(args)
return config
class SomeOtherPlugin:
"""Some other plugin defining the same hook."""
@hookimpl(tryfirst=True)
def setup_project(self, config, args):
"""Report what args were passed before calling
downstream hooks.
"""
if args:
print("Got args: {}".format(args))
return config
pm = PluginManager("myproject")
# load from the local module's namespace
pm.register(sys.modules[__name__])
# load a plugin defined on a class
pm.register(SomeOtherPlugin())
For another example see the :ref:`pytest:plugin-hookorder` section of the
``pytest`` docs.
.. note::
``tryfirst`` and ``trylast`` hooks are still invoked in LIFO order within
each category.
.. _hookwrappers:
Wrappers
^^^^^^^^
A *hookimpl* can be marked with a ``"hookwrapper"`` option which indicates that
the function will be called to *wrap* (or surround) all other normal *hookimpl*
calls. A *hookwrapper* can thus execute some code ahead and after the execution
of all corresponding non-wrappper *hookimpls*.
Much in the same way as a :py:func:`@contextlib.contextmanager <python:contextlib.contextmanager>`, *hookwrappers* must
be implemented as generator function with a single ``yield`` in its body:
.. code-block:: python
@hookimpl(hookwrapper=True)
def setup_project(config, args):
"""Wrap calls to ``setup_project()`` implementations which
should return json encoded config options.
"""
if config.debug:
print("Pre-hook config is {}".format(config.tojson()))
# get initial default config
defaults = config.tojson()
# all corresponding hookimpls are invoked here
outcome = yield
for item in outcome.get_result():
print("JSON config override is {}".format(item))
if config.debug:
print("Post-hook config is {}".format(config.tojson()))
if config.use_defaults:
outcome.force_result(defaults)
The generator is :py:meth:`sent <python:generator.send>` a :py:class:`pluggy._callers._Result` object which can
be assigned in the ``yield`` expression and used to override or inspect
the final result(s) returned back to the caller using the
:py:meth:`~pluggy._callers._Result.force_result` or
:py:meth:`~pluggy._callers._Result.get_result` methods.
.. note::
Hook wrappers can **not** return results (as per generator function
semantics); they can only modify them using the ``_Result`` API.
Also see the :ref:`pytest:hookwrapper` section in the ``pytest`` docs.
.. _specs:
Specifications
--------------
A hook *specification* (*hookspec*) is a definition used to validate each
*hookimpl* ensuring that an extension writer has correctly defined their
callback function *implementation* .
*hookspecs* are defined using similarly marked functions however only the
function *signature* (its name and names of all its arguments) is analyzed
and stored. As such, often you will see a *hookspec* defined with only
a docstring in its body.
*hookspecs* are loaded using the
:py:meth:`~pluggy.PluginManager.add_hookspecs()` method and normally
should be added before registering corresponding *hookimpls*:
.. code-block:: python
import sys
from pluggy import PluginManager, HookspecMarker
hookspec = HookspecMarker("myproject")
@hookspec
def setup_project(config, args):
"""This hook is used to process the initial config and input
arguments.
"""
pm = PluginManager("myproject")
# load from the local module's namespace
pm.add_hookspecs(sys.modules[__name__])
Registering a *hookimpl* which does not meet the constraints of its
corresponding *hookspec* will result in an error.
A *hookspec* can also be added **after** some *hookimpls* have been
registered however this is not normally recommended as it results in
delayed hook validation.
.. note::
The term *hookspec* can sometimes refer to the plugin-namespace
which defines ``hookspec`` decorated functions as in the case of
``pytest``'s `hookspec module`_
.. _enforcing:
Enforcing spec validation
^^^^^^^^^^^^^^^^^^^^^^^^^
By default there is no strict requirement that each *hookimpl* has
a corresponding *hookspec*. However, if you'd like you enforce this
behavior you can run a check with the
:py:meth:`~pluggy.PluginManager.check_pending()` method. If you'd like
to enforce requisite *hookspecs* but with certain exceptions for some hooks
then make sure to mark those hooks as :ref:`optional <optionalhook>`.
Opt-in arguments
^^^^^^^^^^^^^^^^
To allow for *hookspecs* to evolve over the lifetime of a project,
*hookimpls* can accept **less** arguments then defined in the spec.
This allows for extending hook arguments (and thus semantics) without
breaking existing *hookimpls*.
In other words this is ok:
.. code-block:: python
@hookspec
def myhook(config, args):
pass
@hookimpl
def myhook(args):
print(args)
whereas this is not:
.. code-block:: python
@hookspec
def myhook(config, args):
pass
@hookimpl
def myhook(config, args, extra_arg):
print(args)
.. note::
The one exception to this rule (that a *hookspec* must have as least as
many arguments as its *hookimpls*) is the conventional :ref:`self <python:tut-remarks>` arg; this
is always ignored when *hookimpls* are defined as :ref:`methods <python:tut-methodobjects>`.
.. _firstresult:
First result only
^^^^^^^^^^^^^^^^^
A *hookspec* can be marked such that when the *hook* is called the call loop
will only invoke up to the first *hookimpl* which returns a result other
then ``None``.
.. code-block:: python
@hookspec(firstresult=True)
def myhook(config, args):
pass
This can be useful for optimizing a call loop for which you are only
interested in a single core *hookimpl*. An example is the
:func:`~_pytest.hookspec.pytest_cmdline_main` central routine of ``pytest``.
Note that all ``hookwrappers`` are still invoked with the first result.
Also see the :ref:`pytest:firstresult` section in the ``pytest`` docs.
.. _historic:
Historic hooks
^^^^^^^^^^^^^^
You can mark a *hookspec* as being *historic* meaning that the hook
can be called with :py:meth:`~pluggy._hooks._HookCaller.call_historic()` **before**
having been registered:
.. code-block:: python
@hookspec(historic=True)
def myhook(config, args):
pass
The implication is that late registered *hookimpls* will be called back
immediately at register time and **can not** return a result to the caller.
This turns out to be particularly useful when dealing with lazy or
dynamically loaded plugins.
For more info see :ref:`call_historic`.
Warnings on hook implementation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
As projects evolve new hooks may be introduced and/or deprecated.
if a hookspec specifies a ``warn_on_impl``, pluggy will trigger it for any plugin implementing the hook.
.. code-block:: python
@hookspec(
warn_on_impl=DeprecationWarning("oldhook is deprecated and will be removed soon")
)
def oldhook():
pass
.. _manage:
The Plugin registry
*******************
``pluggy`` manages plugins using instances of the
:py:class:`pluggy.PluginManager`.
A :py:class:`~pluggy.PluginManager` is instantiated with a single
``str`` argument, the ``project_name``:
.. code-block:: python
import pluggy
pm = pluggy.PluginManager("my_project_name")
The ``project_name`` value is used when a :py:class:`~pluggy.PluginManager`
scans for *hook* functions :ref:`defined on a plugin <define>`.
This allows for multiple plugin managers from multiple projects
to define hooks alongside each other.
.. _registration:
Registration
------------
Each :py:class:`~pluggy.PluginManager` maintains a *plugin* registry where each *plugin*
contains a set of *hookimpl* definitions. Loading *hookimpl* and *hookspec*
definitions to populate the registry is described in detail in the section on
:ref:`define`.
In summary, you pass a plugin namespace object to the
:py:meth:`~pluggy.PluginManager.register()` and
:py:meth:`~pluggy.PluginManager.add_hookspecs()` methods to collect
hook *implementations* and *specifications* from *plugin* namespaces respectively.
You can unregister any *plugin*'s hooks using
:py:meth:`~pluggy.PluginManager.unregister()` and check if a plugin is
registered by passing its name to the
:py:meth:`~pluggy.PluginManager.is_registered()` method.
Loading ``setuptools`` entry points
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You can automatically load plugins registered through
:ref:`setuptools entry points <setuptools:entry_points>`
with the :py:meth:`~pluggy.PluginManager.load_setuptools_entrypoints()`
method.
An example use of this is the :ref:`pytest entry point <pytest:pip-installable plugins>`.
Blocking
--------
You can block any plugin from being registered using
:py:meth:`~pluggy.PluginManager.set_blocked()` and check if a given
*plugin* is blocked by name using :py:meth:`~pluggy.PluginManager.is_blocked()`.
Inspection
----------
You can use a variety of methods to inspect both the registry
and particular plugins in it:
- :py:meth:`~pluggy.PluginManager.list_name_plugin()` -
return a list of name-plugin pairs
- :py:meth:`~pluggy.PluginManager.get_plugins()` - retrieve all plugins
- :py:meth:`~pluggy.PluginManager.get_canonical_name()`- get a *plugin*'s
canonical name (the name it was registered with)
- :py:meth:`~pluggy.PluginManager.get_plugin()` - retrieve a plugin by its
canonical name
Parsing mark options
^^^^^^^^^^^^^^^^^^^^
You can retrieve the *options* applied to a particular
*hookspec* or *hookimpl* as per :ref:`marking_hooks` using the
:py:meth:`~pluggy.PluginManager.parse_hookspec_opts()` and
:py:meth:`~pluggy.PluginManager.parse_hookimpl_opts()` respectively.
.. _calling:
Calling hooks
*************
The core functionality of ``pluggy`` enables an extension provider
to override function calls made at certain points throughout a program.
A particular *hook* is invoked by calling an instance of
a :py:class:`pluggy._hooks._HookCaller` which in turn *loops* through the
``1:N`` registered *hookimpls* and calls them in sequence.
Every :py:class:`~pluggy.PluginManager` has a ``hook`` attribute
which is an instance of this :py:class:`pluggy._hooks._HookRelay`.
The :py:class:`~pluggy._hooks._HookRelay` itself contains references
(by hook name) to each registered *hookimpl*'s :py:class:`~pluggy._hooks._HookCaller` instance.
More practically you call a *hook* like so:
.. code-block:: python
import sys
import pluggy
import mypluginspec
import myplugin
from configuration import config
pm = pluggy.PluginManager("myproject")
pm.add_hookspecs(mypluginspec)
pm.register(myplugin)
# we invoke the _HookCaller and thus all underlying hookimpls
result_list = pm.hook.myhook(config=config, args=sys.argv)
Note that you **must** call hooks using keyword :std:term:`python:argument` syntax!
Hook implementations are called in LIFO registered order: *the last
registered plugin's hooks are called first*. As an example, the below
assertion should not error:
.. code-block:: python
from pluggy import PluginManager, HookimplMarker
hookimpl = HookimplMarker("myproject")
class Plugin1:
@hookimpl
def myhook(self, args):
"""Default implementation."""
return 1
class Plugin2:
@hookimpl
def myhook(self, args):
"""Default implementation."""
return 2
class Plugin3:
@hookimpl
def myhook(self, args):
"""Default implementation."""
return 3
pm = PluginManager("myproject")
pm.register(Plugin1())
pm.register(Plugin2())
pm.register(Plugin3())
assert pm.hook.myhook(args=()) == [3, 2, 1]
Collecting results
------------------
By default calling a hook results in all underlying :ref:`hookimpls
<impls>` functions to be invoked in sequence via a loop. Any function
which returns a value other then a ``None`` result will have that result
appended to a :py:class:`list` which is returned by the call.
The only exception to this behaviour is if the hook has been marked to return
its :ref:`first result only <firstresult>` in which case only the first
single value (which is not ``None``) will be returned.
.. _call_historic:
Exception handling
------------------
If any *hookimpl* errors with an exception no further callbacks
are invoked and the exception is packaged up and delivered to
any :ref:`wrappers <hookwrappers>` before being re-raised at the
hook invocation point:
.. code-block:: python
from pluggy import PluginManager, HookimplMarker
hookimpl = HookimplMarker("myproject")
class Plugin1:
@hookimpl
def myhook(self, args):
return 1
class Plugin2:
@hookimpl
def myhook(self, args):
raise RuntimeError
class Plugin3:
@hookimpl
def myhook(self, args):
return 3
@hookimpl(hookwrapper=True)
def myhook(self, args):
outcome = yield
try:
outcome.get_result()
except RuntimeError:
# log the error details
print(outcome.excinfo)
pm = PluginManager("myproject")
# register plugins
pm.register(Plugin1())
pm.register(Plugin2())
pm.register(Plugin3())
# register wrapper
pm.register(sys.modules[__name__])
# this raises RuntimeError due to Plugin2
pm.hook.myhook(args=())
Historic calls
--------------
A *historic call* allows for all newly registered functions to receive all hook
calls that happened before their registration. The implication is that this is
only useful if you expect that some *hookimpls* may be registered **after** the
hook is initially invoked.
Historic hooks must be :ref:`specially marked <historic>` and called
using the :py:meth:`~pluggy._hooks._HookCaller.call_historic()` method:
.. code-block:: python
def callback(result):
print("historic call result is {result}".format(result=result))
# call with history; no results returned
pm.hook.myhook.call_historic(
kwargs={"config": config, "args": sys.argv}, result_callback=callback
)
# ... more of our program ...
# late loading of some plugin
import mylateplugin
# historic callback is invoked here
pm.register(mylateplugin)
Note that if you :py:meth:`~pluggy._hooks._HookCaller.call_historic()`
the :py:class:`~pluggy._hooks._HookCaller` (and thus your calling code)
can not receive results back from the underlying *hookimpl* functions.
Instead you can provide a *callback* for processing results (like the
``callback`` function above) which will be called as each new plugin
is registered.
.. note::
*historic* calls are incompatible with :ref:`firstresult` marked
hooks since only the first registered plugin's hook(s) would
ever be called.
Calling with extras
-------------------
You can call a hook with temporarily participating *implementation* functions
(that aren't in the registry) using the
:py:meth:`pluggy._hooks._HookCaller.call_extra()` method.
Calling with a subset of registered plugins
-------------------------------------------
You can make a call using a subset of plugins by asking the
:py:class:`~pluggy.PluginManager` first for a
:py:class:`~pluggy._hooks._HookCaller` with those plugins removed
using the :py:meth:`pluggy.PluginManager.subset_hook_caller()` method.
You then can use that :py:class:`_HookCaller <pluggy._hooks._HookCaller>`
to make normal, :py:meth:`~pluggy._hooks._HookCaller.call_historic`, or
:py:meth:`~pluggy._hooks._HookCaller.call_extra` calls as necessary.
Built-in tracing
****************
``pluggy`` comes with some batteries included hook tracing for your
debugging needs.
Call tracing
------------
To enable tracing use the
:py:meth:`pluggy.PluginManager.enable_tracing()` method which returns an
undo function to disable the behaviour.
.. code-block:: python
pm = PluginManager("myproject")
# magic line to set a writer function
pm.trace.root.setwriter(print)
undo = pm.enable_tracing()
Call monitoring
---------------
Instead of using the built-in tracing mechanism you can also add your
own ``before`` and ``after`` monitoring functions using
:py:class:`pluggy.PluginManager.add_hookcall_monitoring()`.
The expected signature and default implementations for these functions is:
.. code-block:: python
def before(hook_name, methods, kwargs):
pass
def after(outcome, hook_name, methods, kwargs):
pass
Public API
**********
Please see the :doc:`api_reference`.
Development
***********
Great care must taken when hacking on ``pluggy`` since multiple mature
projects rely on it. Our Github integrated CI process runs the full
`tox test suite`_ on each commit so be sure your changes can run on
all required `Python interpreters`_ and ``pytest`` versions.
For development, we suggest to create a virtual environment and install ``pluggy`` in
editable mode and ``dev`` dependencies::
$ python3 -m venv .env
$ source .env/bin/activate
$ pip install -e .[dev]
To make sure you follow the code style used in the project, install pre-commit_ which
will run style checks before each commit::
$ pre-commit install
Release Policy
**************
Pluggy uses `Semantic Versioning`_. Breaking changes are only foreseen for
Major releases (incremented X in "X.Y.Z"). If you want to use ``pluggy``
in your project you should thus use a dependency restriction like
``"pluggy>=0.1.0,<1.0"`` to avoid surprises.
Table of contents
*****************
.. toctree::
:maxdepth: 2
api_reference
changelog
.. hyperlinks
.. _hookspec module:
https://docs.pytest.org/en/latest/_modules/_pytest/hookspec.html
.. _request-response pattern:
https://en.wikipedia.org/wiki/Request%E2%80%93response
.. _publish-subscribe:
https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern
.. _hooking:
https://en.wikipedia.org/wiki/Hooking
.. _callbacks:
https://en.wikipedia.org/wiki/Callback_(computer_programming)
.. _tox test suite:
https://github.com/pytest-dev/pluggy/blob/master/tox.ini
.. _Semantic Versioning:
https://semver.org/
.. _Python interpreters:
https://github.com/pytest-dev/pluggy/blob/master/tox.ini#L2
.. _500+ plugins:
http://plugincompat.herokuapp.com/
.. _pre-commit:
https://pre-commit.com/
.. Indices and tables
.. ==================
.. * :ref:`genindex`
.. * :ref:`modindex`
.. * :ref:`search`
|