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/ diff --git a/docs/_static/img/plug.png b/docs/_static/img/plug.png new file mode 100644 index 00000000..3339f8a6 Binary files /dev/null and b/docs/_static/img/plug.png differ 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/conf.py b/docs/conf.py index 28202fb2..04065218 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,7 +33,19 @@ language = None pygments_style = 'sphinx' +html_logo = '_static/img/plug.png' html_theme = 'alabaster' +html_theme_options = { + # 'logo': 'img/plug.png', + # 'logo_name': 'true', + 'description': 'The `pytest` plugin system', + 'github_user': 'pytest-dev', + 'github_repo': 'pluggy', + 'github_button': 'true', + 'github_banner': 'true', + 'page_width': '1080px', + 'fixed_sidebar': 'false', +} html_static_path = ['_static'] # One entry per manual page. List of tuples 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..ddb0548c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,23 +1,662 @@ -.. 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. -.. toctree:: - :maxdepth: 2 +In essence, ``pluggy`` enables function `hooking`_ so you can build "pluggable" systems. - api_reference +How's it work? +-------------- +A `plugin` is a `namespace`_ which defines hook functions. +``pluggy`` manages *plugins* by relying on: -Indices and tables -================== +- a hook *specification* - defines a call signature +- a set of hook *implementations* - aka `callbacks`_ +- the hook *caller* - a call loop which collects results -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +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/firstexample.py + + inside Plugin_2.myhook() + inside Plugin_1.myhook() + [-1, 3] + +For more details and advanced usage please read on. + +.. _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 +---------- +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 + + 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. + +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. + + +.. 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/ + + +.. Indices and tables +.. ================== +.. * :ref:`genindex` +.. * :ref:`modindex` +.. * :ref:`search` 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