From e98b20c15234db25355dd50fa6594ad1a77a2e03 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 31 Jan 2017 01:36:53 -0500 Subject: [PATCH 1/5] Initial docs draft Covers everything in the original pluggy.py module's doc string in much more detail with links to external resources as seemed fit. Resolves #14 --- docs/api_reference.rst | 4 - docs/calling.rst | 94 ++++++++++ docs/define.rst | 341 ++++++++++++++++++++++++++++++++++ docs/examples/firstexample.py | 44 +++++ docs/index.rst | 115 ++++++++++-- docs/manage.rst | 78 ++++++++ 6 files changed, 659 insertions(+), 17 deletions(-) create mode 100644 docs/calling.rst create mode 100644 docs/define.rst create mode 100644 docs/examples/firstexample.py create mode 100644 docs/manage.rst diff --git a/docs/api_reference.rst b/docs/api_reference.rst index c87ae98b..d14a89bf 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -1,5 +1,3 @@ - - Api Reference ============= @@ -7,5 +5,3 @@ Api Reference :members: :undoc-members: :show-inheritance: - - \ No newline at end of file diff --git a/docs/calling.rst b/docs/calling.rst new file mode 100644 index 00000000..94f480d9 --- /dev/null +++ b/docs/calling.rst @@ -0,0 +1,94 @@ +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._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 a :py:class:`pluggy._HookRelay`. +The ``_HookRelay`` itself contains references (by hook name) to each +registered *hookimpl*'s ``_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 `arguments`_ syntax! + + +Collecting results +------------------ +By default calling a hook results in all underlying :ref:`hookimpls +` 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:`firstresult` in which case only the first single value (which is not +``None``) will be returned. + +.. _call_historic: + +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 ` and called +using the :py:meth:`pluggy._HookCaller.call_historic()` method: + +.. code-block:: python + + # call with history; no results returned + pm.hook.myhook.call_historic(config=config, args=sys.argv) + + # ... more of our program ... + + # late loading of some plugin + import mylateplugin + + # historic call back is done here + pm.register(mylateplugin) + +Note that if you ``call_historic()`` the ``_HookCaller`` (and thus your +calling code) can not receive results back from the underlying *hookimpl* +functions. + +Calling with extras +------------------- +You can call a hook with temporarily participating *implementation* functions +(that aren't in the registry) using the +:py:meth:`pluggy._HookCaller.call_extra()` method. + + +Calling with a subset of registered plugins +------------------------------------------- +You can make a call using a subset of plugins by asking the +``PluginManager`` first for a ``_HookCaller`` with those plugins removed +using the :py:meth:`pluggy.PluginManger.subset_hook_caller()` method. + +You then can use that ``_HookCaller`` to make normal, ``call_historic()``, +or ``call_extra()`` calls as necessary. + + +.. links +.. _arguments: + https://docs.python.org/3/glossary.html#term-argument diff --git a/docs/define.rst b/docs/define.rst new file mode 100644 index 00000000..88f8dcb7 --- /dev/null +++ b/docs/define.rst @@ -0,0 +1,341 @@ +Defining and Collecting Hooks +============================= +A *plugin* is a namespace type (currently one of a ``class`` or module) +which defines a set of *hook* functions. + +As mentioned in :doc:`manage`, all *plugins* which define *hooks* +are managed by an instance of a :py:class:`pluggy.PluginManager` which +defines the primary ``pluggy`` API. + +In order for a ``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 ``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 +by a similarly configured ``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 +``PluginManager`` which was instantiated with the 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 `Writing hook functions`_ 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 a against a corresponding +hook :ref:`specification `. 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 + +Call time order +^^^^^^^^^^^^^^^ +A *hookimpl* can influence its call-time invocation position. +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(object): + """Some other plugin defining the same hook. + """ + @hookimpl(tryfirst=True) + def setup_project(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(SomePlugin()) + +For another example see the `hook function ordering`_ section of the +``pytest`` docs. + +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-hookwrappper *hookimpls*. + +Much in the same way as a `@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 `sent`_ a :py:class:`pluggy._CallOutcome` object which can +be assigned in the ``yield`` expression and used to override or inspect +the final result(s) returned back to the hook caller. + +.. note:: + Hook wrappers can **not** return results (as per generator function + semantics); they can only modify them using the ``_CallOutcome`` API. + +Also see the `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 inital 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 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 `. + +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) + +.. _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 +`pytest_cmdline_main`_ central routine of ``pytest``. + +Also see the `first result`_ 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.PluginManager.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`. + + +.. links +.. _@contextlib.contextmanager: + https://docs.python.org/3.6/library/contextlib.html#contextlib.contextmanager +.. _pytest_cmdline_main: + https://github.com/pytest-dev/pytest/blob/master/_pytest/hookspec.py#L80 +.. _hookspec module: + https://github.com/pytest-dev/pytest/blob/master/_pytest/hookspec.py +.. _Writing hook functions: + http://doc.pytest.org/en/latest/writing_plugins.html#writing-hook-functions +.. _hookwrapper: + http://doc.pytest.org/en/latest/writing_plugins.html#hookwrapper-executing-around-other-hooks +.. _hook function ordering: + http://doc.pytest.org/en/latest/writing_plugins.html#hook-function-ordering-call-example +.. _first result: + http://doc.pytest.org/en/latest/writing_plugins.html#firstresult-stop-at-first-non-none-result +.. _sent: + https://docs.python.org/3/reference/expressions.html#generator.send diff --git a/docs/examples/firstexample.py b/docs/examples/firstexample.py new file mode 100644 index 00000000..ccd0b02e --- /dev/null +++ b/docs/examples/firstexample.py @@ -0,0 +1,44 @@ +import pluggy + +hookspec = pluggy.HookspecMarker("myproject") +hookimpl = pluggy.HookimplMarker("myproject") + + +class MySpec: + """A hook specification namespace. + """ + @hookspec + def myhook(self, arg1, arg2): + """My special little hook that you can customize. + """ + + +class Plugin_1: + """A hook implementation namespace. + """ + @hookimpl + def myhook(self, arg1, arg2): + print("inside Plugin_1.myhook()") + return arg1 + arg2 + + +class Plugin_2: + """A 2nd hook implementation namespace. + """ + @hookimpl + def myhook(self, arg1, arg2): + print("inside Plugin_2.myhook()") + return arg1 - arg2 + + +# create a manager and add the spec +pm = pluggy.PluginManager("myproject") +pm.add_hookspecs(MySpec) + +# register plugins +pm.register(Plugin_1()) +pm.register(Plugin_2()) + +# call our `myhook` hook +results = pm.hook.myhook(arg1=1, arg2=2) +print(results) diff --git a/docs/index.rst b/docs/index.rst index 021b6b24..18b1d396 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,23 +1,112 @@ -.. pluggy documentation master file, created by - sphinx-quickstart on Mon Nov 14 11:08:31 2016. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +``pluggy`` +========== -Welcome to pluggy's documentation! -================================== +The ``pytest`` plugin system +---------------------------- +``pluggy`` is the crystallized core of `plugin management and hook +calling`_ for `pytest`_. -Contents: +In fact, ``pytest`` is itself composed as a set of ``pluggy`` plugins +which are invoked in sequence according to a well defined set of protocols. +Some `200+ plugins`_ use ``pluggy`` to extend and customize ``pytest``'s default behaviour. +In essence, ``pluggy`` enables function `hooking`_ so you can build "pluggable" systems. + +How's it work? +-------------- +A `plugin` is a `namespace`_ which defines hook functions. + +``pluggy`` manages *plugins* by relying on: + +- a hook *specification* - defines a call signature +- a set of hook *implementations* - aka `callbacks`_ +- the hook *caller* - a call loop which collects results + +where for each registered hook *specification*, a hook *call* will invoke up to ``N`` +registered hook *implementations*. + +``pluggy`` accomplishes all this by implementing a `request-response pattern`_ using *function* +subscriptions and can be thought of and used as a rudimentary busless `publish-subscribe`_ +event system. + +``pluggy``'s approach is meant to let a designer think carefuly about which objects are +explicitly needed by an extension writer. This is in contrast to subclass-based extension +systems which may expose unecessary state and behaviour or encourage `tight coupling`_ +in overlying frameworks. + + +A first example +--------------- + +.. literalinclude:: examples/firstexample.py + +Running this directly gets us:: + + $ python docs/examples/example1.py + + inside Plugin_2.myhook() + inside Plugin_1.myhook() + [-1, 3] + +For more details and advanced usage see our + +User Guide +---------- .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + define + manage + calling + tracing api_reference +.. tracing + +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. + +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. + -Indices and tables -================== +.. hyperlinks +.. _pytest: + http://pytest.org +.. _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 +.. _plugin management and hook calling: + http://doc.pytest.org/en/latest/writing_plugins.html +.. _namespace: + https://docs.python.org/3.6/tutorial/classes.html#python-scopes-and-namespaces +.. _callbacks: + https://en.wikipedia.org/wiki/Callback_(computer_programming) +.. _tox test suite: + https://github.com/pytest-dev/pluggy/blob/master/tox.ini +.. _Semantic Versioning: + http://semver.org/ +.. _tight coupling: + https://en.wikipedia.org/wiki/Coupling_%28computer_programming%29#Types_of_coupling +.. _Python interpreters: + https://github.com/pytest-dev/pluggy/blob/master/tox.ini#L2 +.. _200+ plugins: + http://plugincompat.herokuapp.com/ -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +.. Indices and tables +.. ================== +.. * :ref:`genindex` +.. * :ref:`modindex` +.. * :ref:`search` diff --git a/docs/manage.rst b/docs/manage.rst new file mode 100644 index 00000000..40e9310e --- /dev/null +++ b/docs/manage.rst @@ -0,0 +1,78 @@ +The Plugin Registry +=================== +``pluggy`` manages plugins using instances of the +:py:class:`pluggy.PluginManager`. + +A ``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 ``PluginManager`` scans for *hook* +functions :doc:`defined on a plugin `. +This allows for multiple +plugin managers from multiple projects to define hooks alongside each other. + + +Registration +------------ +Each ``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 +:doc:`define`. + +In summary, you pass a plugin namespace object to the +:py:meth:`~pluggy.PluginManager.register()` and +:py:meth:`~pluggy.PluginManager.add_hookspec()` methods to collect +hook *implementations* and *specfications* 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 `setuptools entry points`_ +with the :py:meth:`~pluggy.PluginManager.load_setuptools_entrypoints()` +method. + +An example use of this is the `pytest entry point`_. + + +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 the 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. + +.. links +.. _setuptools entry points: + http://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins +.. _pytest entry point: + http://doc.pytest.org/en/latest/writing_plugins.html#setuptools-entry-points From 4d7decb1f48aa9f8c09ab9f866cd8dca4ff34ecf Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 4 Feb 2017 18:41:59 -0500 Subject: [PATCH 2/5] Merge all documents into index.rst --- docs/calling.rst | 94 -------- docs/define.rst | 341 ---------------------------- docs/index.rst | 576 +++++++++++++++++++++++++++++++++++++++++++++-- docs/manage.rst | 78 ------- 4 files changed, 563 insertions(+), 526 deletions(-) delete mode 100644 docs/calling.rst delete mode 100644 docs/define.rst delete mode 100644 docs/manage.rst diff --git a/docs/calling.rst b/docs/calling.rst deleted file mode 100644 index 94f480d9..00000000 --- a/docs/calling.rst +++ /dev/null @@ -1,94 +0,0 @@ -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._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 a :py:class:`pluggy._HookRelay`. -The ``_HookRelay`` itself contains references (by hook name) to each -registered *hookimpl*'s ``_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 `arguments`_ syntax! - - -Collecting results ------------------- -By default calling a hook results in all underlying :ref:`hookimpls -` 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:`firstresult` in which case only the first single value (which is not -``None``) will be returned. - -.. _call_historic: - -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 ` and called -using the :py:meth:`pluggy._HookCaller.call_historic()` method: - -.. code-block:: python - - # call with history; no results returned - pm.hook.myhook.call_historic(config=config, args=sys.argv) - - # ... more of our program ... - - # late loading of some plugin - import mylateplugin - - # historic call back is done here - pm.register(mylateplugin) - -Note that if you ``call_historic()`` the ``_HookCaller`` (and thus your -calling code) can not receive results back from the underlying *hookimpl* -functions. - -Calling with extras -------------------- -You can call a hook with temporarily participating *implementation* functions -(that aren't in the registry) using the -:py:meth:`pluggy._HookCaller.call_extra()` method. - - -Calling with a subset of registered plugins -------------------------------------------- -You can make a call using a subset of plugins by asking the -``PluginManager`` first for a ``_HookCaller`` with those plugins removed -using the :py:meth:`pluggy.PluginManger.subset_hook_caller()` method. - -You then can use that ``_HookCaller`` to make normal, ``call_historic()``, -or ``call_extra()`` calls as necessary. - - -.. links -.. _arguments: - https://docs.python.org/3/glossary.html#term-argument diff --git a/docs/define.rst b/docs/define.rst deleted file mode 100644 index 88f8dcb7..00000000 --- a/docs/define.rst +++ /dev/null @@ -1,341 +0,0 @@ -Defining and Collecting Hooks -============================= -A *plugin* is a namespace type (currently one of a ``class`` or module) -which defines a set of *hook* functions. - -As mentioned in :doc:`manage`, all *plugins* which define *hooks* -are managed by an instance of a :py:class:`pluggy.PluginManager` which -defines the primary ``pluggy`` API. - -In order for a ``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 ``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 -by a similarly configured ``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 -``PluginManager`` which was instantiated with the 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 `Writing hook functions`_ 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 a against a corresponding -hook :ref:`specification `. 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 - -Call time order -^^^^^^^^^^^^^^^ -A *hookimpl* can influence its call-time invocation position. -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(object): - """Some other plugin defining the same hook. - """ - @hookimpl(tryfirst=True) - def setup_project(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(SomePlugin()) - -For another example see the `hook function ordering`_ section of the -``pytest`` docs. - -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-hookwrappper *hookimpls*. - -Much in the same way as a `@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 `sent`_ a :py:class:`pluggy._CallOutcome` object which can -be assigned in the ``yield`` expression and used to override or inspect -the final result(s) returned back to the hook caller. - -.. note:: - Hook wrappers can **not** return results (as per generator function - semantics); they can only modify them using the ``_CallOutcome`` API. - -Also see the `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 inital 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 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 `. - -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) - -.. _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 -`pytest_cmdline_main`_ central routine of ``pytest``. - -Also see the `first result`_ 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.PluginManager.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`. - - -.. links -.. _@contextlib.contextmanager: - https://docs.python.org/3.6/library/contextlib.html#contextlib.contextmanager -.. _pytest_cmdline_main: - https://github.com/pytest-dev/pytest/blob/master/_pytest/hookspec.py#L80 -.. _hookspec module: - https://github.com/pytest-dev/pytest/blob/master/_pytest/hookspec.py -.. _Writing hook functions: - http://doc.pytest.org/en/latest/writing_plugins.html#writing-hook-functions -.. _hookwrapper: - http://doc.pytest.org/en/latest/writing_plugins.html#hookwrapper-executing-around-other-hooks -.. _hook function ordering: - http://doc.pytest.org/en/latest/writing_plugins.html#hook-function-ordering-call-example -.. _first result: - http://doc.pytest.org/en/latest/writing_plugins.html#firstresult-stop-at-first-non-none-result -.. _sent: - https://docs.python.org/3/reference/expressions.html#generator.send diff --git a/docs/index.rst b/docs/index.rst index 18b1d396..ddb0548c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,7 +2,7 @@ ========== The ``pytest`` plugin system ----------------------------- +**************************** ``pluggy`` is the crystallized core of `plugin management and hook calling`_ for `pytest`_. @@ -42,29 +42,579 @@ A first example Running this directly gets us:: - $ python docs/examples/example1.py + $ python docs/examples/firstexample.py inside Plugin_2.myhook() inside Plugin_1.myhook() [-1, 3] -For more details and advanced usage see our +For more details and advanced usage please read on. -User Guide +.. _define: + +Defining and Collecting Hooks +***************************** +A *plugin* is a namespace type (currently one of a ``class`` or module) +which defines a set of *hook* functions. + +As mentioned in :ref:`manage`, all *plugins* which define *hooks* +are managed by an instance of a :py:class:`pluggy.PluginManager` which +defines the primary ``pluggy`` API. + +In order for a ``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 ``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 +by a similarly configured ``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 +``PluginManager`` which was instantiated with the 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 `Writing hook functions`_ 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 a against a corresponding +hook :ref:`specification `. 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 + +Call time order +^^^^^^^^^^^^^^^ +A *hookimpl* can influence its call-time invocation position. +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(object): + """Some other plugin defining the same hook. + """ + @hookimpl(tryfirst=True) + def setup_project(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(SomePlugin()) + +For another example see the `hook function ordering`_ section of the +``pytest`` docs. + +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-hookwrappper *hookimpls*. + +Much in the same way as a `@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 `sent`_ a :py:class:`pluggy._CallOutcome` object which can +be assigned in the ``yield`` expression and used to override or inspect +the final result(s) returned back to the hook caller. + +.. note:: + Hook wrappers can **not** return results (as per generator function + semantics); they can only modify them using the ``_CallOutcome`` API. + +Also see the `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 inital 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 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 `. + +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) + +.. _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 +`pytest_cmdline_main`_ central routine of ``pytest``. + +Also see the `first result`_ 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.PluginManager.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`. + + +.. links +.. _@contextlib.contextmanager: + https://docs.python.org/3.6/library/contextlib.html#contextlib.contextmanager +.. _pytest_cmdline_main: + https://github.com/pytest-dev/pytest/blob/master/_pytest/hookspec.py#L80 +.. _hookspec module: + https://github.com/pytest-dev/pytest/blob/master/_pytest/hookspec.py +.. _Writing hook functions: + http://doc.pytest.org/en/latest/writing_plugins.html#writing-hook-functions +.. _hookwrapper: + http://doc.pytest.org/en/latest/writing_plugins.html#hookwrapper-executing-around-other-hooks +.. _hook function ordering: + http://doc.pytest.org/en/latest/writing_plugins.html#hook-function-ordering-call-example +.. _first result: + http://doc.pytest.org/en/latest/writing_plugins.html#firstresult-stop-at-first-non-none-result +.. _sent: + https://docs.python.org/3/reference/expressions.html#generator.send + +.. _manage: + +The Plugin Registry +******************* +``pluggy`` manages plugins using instances of the +:py:class:`pluggy.PluginManager`. + +A ``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 ``PluginManager`` scans for *hook* +functions :ref:`defined on a plugin `. +This allows for multiple +plugin managers from multiple projects to define hooks alongside each other. + + +Registration +------------ +Each ``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_hookspec()` methods to collect +hook *implementations* and *specfications* 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 `setuptools entry points`_ +with the :py:meth:`~pluggy.PluginManager.load_setuptools_entrypoints()` +method. + +An example use of this is the `pytest entry point`_. + + +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 ---------- -.. toctree:: - :maxdepth: 1 +You can use a variety of methods to inspect the 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. + +.. links +.. _setuptools entry points: + http://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins +.. _pytest entry point: + http://doc.pytest.org/en/latest/writing_plugins.html#setuptools-entry-points + + +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._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 a :py:class:`pluggy._HookRelay`. +The ``_HookRelay`` itself contains references (by hook name) to each +registered *hookimpl*'s ``_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 `arguments`_ syntax! + + +Collecting results +------------------ +By default calling a hook results in all underlying :ref:`hookimpls +` 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:`firstresult` in which case only the first single value (which is not +``None``) will be returned. + +.. _call_historic: + +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 ` and called +using the :py:meth:`pluggy._HookCaller.call_historic()` method: + +.. code-block:: python + + # call with history; no results returned + pm.hook.myhook.call_historic(config=config, args=sys.argv) + + # ... more of our program ... + + # late loading of some plugin + import mylateplugin + + # historic call back is done here + pm.register(mylateplugin) + +Note that if you ``call_historic()`` the ``_HookCaller`` (and thus your +calling code) can not receive results back from the underlying *hookimpl* +functions. + +Calling with extras +------------------- +You can call a hook with temporarily participating *implementation* functions +(that aren't in the registry) using the +:py:meth:`pluggy._HookCaller.call_extra()` method. + + +Calling with a subset of registered plugins +------------------------------------------- +You can make a call using a subset of plugins by asking the +``PluginManager`` first for a ``_HookCaller`` with those plugins removed +using the :py:meth:`pluggy.PluginManger.subset_hook_caller()` method. + +You then can use that ``_HookCaller`` to make normal, ``call_historic()``, +or ``call_extra()`` calls as necessary. + + +.. links +.. _arguments: + https://docs.python.org/3/glossary.html#term-argument + + +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 - define - manage - calling - tracing - api_reference + def after(outcome, hook_name, methods, kwargs): + pass -.. tracing +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 diff --git a/docs/manage.rst b/docs/manage.rst deleted file mode 100644 index 40e9310e..00000000 --- a/docs/manage.rst +++ /dev/null @@ -1,78 +0,0 @@ -The Plugin Registry -=================== -``pluggy`` manages plugins using instances of the -:py:class:`pluggy.PluginManager`. - -A ``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 ``PluginManager`` scans for *hook* -functions :doc:`defined on a plugin `. -This allows for multiple -plugin managers from multiple projects to define hooks alongside each other. - - -Registration ------------- -Each ``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 -:doc:`define`. - -In summary, you pass a plugin namespace object to the -:py:meth:`~pluggy.PluginManager.register()` and -:py:meth:`~pluggy.PluginManager.add_hookspec()` methods to collect -hook *implementations* and *specfications* 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 `setuptools entry points`_ -with the :py:meth:`~pluggy.PluginManager.load_setuptools_entrypoints()` -method. - -An example use of this is the `pytest entry point`_. - - -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 the 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. - -.. links -.. _setuptools entry points: - http://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins -.. _pytest entry point: - http://doc.pytest.org/en/latest/writing_plugins.html#setuptools-entry-points From 9476b02a9a04e070f9176ecf1efc29774634803c Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 31 Jan 2017 01:39:56 -0500 Subject: [PATCH 3/5] Drop the monolithic doc string We now have full sphinx docs as per #14. pluggy.py is the entire project and it doesn't make much sense to describe that module's contents other then saying "this is it folks". --- pluggy.py | 66 ------------------------------------------------------- 1 file changed, 66 deletions(-) diff --git a/pluggy.py b/pluggy.py index 08d8c3fc..37dd6b51 100644 --- a/pluggy.py +++ b/pluggy.py @@ -1,69 +1,3 @@ -""" -PluginManager, basic initialization and tracing. - -pluggy is the cristallized core of plugin management as used -by some 150 plugins for pytest. - -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. - -pluggy is concerned with hook specification, hook implementations and hook -calling. For any given hook specification a hook call invokes up to N implementations. -A hook implementation can influence its position and type of execution: -if attributed "tryfirst" or "trylast" it will be tried to execute -first or last. However, if attributed "hookwrapper" an implementation -can wrap all calls to non-hookwrapper implementations. A hookwrapper -can thus execute some code ahead and after the execution of other hooks. - -Hook specification is done by way of a regular python function where -both the function name and the names of all its arguments are significant. -Each hook implementation function is verified against the original specification -function, including the names of all its arguments. To allow for hook specifications -to evolve over the livetime of a project, hook implementations can -accept less arguments. One can thus add new arguments and semantics to -a hook specification by adding another argument typically without breaking -existing hook implementations. - -The chosen approach is meant to let a hook designer think carefuly about -which objects are needed by an extension writer. By contrast, subclass-based -extension mechanisms often expose a lot more state and behaviour than needed, -thus restricting future developments. - -Pluggy currently consists of functionality for: - -- a way to register new hook specifications. Without a hook - specification no hook calling can be performed. - -- a registry of plugins which contain hook implementation functions. It - is possible to register plugins for which a hook specification is not yet - known and validate all hooks when the system is in a more referentially - consistent state. Setting an "optionalhook" attribution to a hook - implementation will avoid PluginValidationError's if a specification - is missing. This allows to have optional integration between plugins. - -- a "hook" relay object from which you can launch 1:N calls to - registered hook implementation functions - -- a mechanism for ordering hook implementation functions - -- mechanisms for two different type of 1:N calls: "firstresult" for when - the call should stop when the first implementation returns a non-None result. - And the other (default) way of guaranteeing that all hook implementations - will be called and their non-None result collected. - -- mechanisms for "historic" extension points such that all newly - registered functions will receive all hook calls that happened - before their registration. - -- a mechanism for discovering plugin objects which are based on - setuptools based entry points. - -- a simple tracing mechanism, including tracing of plugin calls and - their arguments. - -""" import sys import inspect From 04d7b295c59f7ac0d3d9f96e91d2e4eebecb7db0 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 5 Feb 2017 13:38:25 -0500 Subject: [PATCH 4/5] Add a plug logo Resolves #40 --- docs/_static/img/plug.png | Bin 0 -> 9350 bytes docs/conf.py | 12 ++++++++++++ 2 files changed, 12 insertions(+) create mode 100644 docs/_static/img/plug.png diff --git a/docs/_static/img/plug.png b/docs/_static/img/plug.png new file mode 100644 index 0000000000000000000000000000000000000000..3339f8a608d25ff7bdc6531b4290935f58ba907a GIT binary patch literal 9350 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 Date: Sun, 5 Feb 2017 13:56:23 -0500 Subject: [PATCH 5/5] Fix up README --- README.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index d1e2325d..2b2a2773 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,15 @@ Plugin registration and hook calling for Python .. image:: https://img.shields.io/appveyor/ci/pytestbot/pluggy/master.svg :target: https://ci.appveyor.com/project/pytestbot/pluggy -This is the plugin manager as used by [pytest](http://pytest.org), [tox](https://tox.readthedocs.org), [devpi](http://doc.devpi.net) and probably other projects. +This is the core plugin system used by the `pytest`_, `tox`_, and `devpi`_ projects. +Please `read the docs`_ to learn more! -During the 0.x series this plugin does not have much documentation -except extensive docstrings in the pluggy.py module. +.. links +.. _pytest: + http://pytest.org +.. _tox: + https://tox.readthedocs.org +.. _devpi: + http://doc.devpi.net +.. _read the docs: + https://pluggy.readthedocs.io/en/latest/