diff --git a/docs/api_reference.rst b/docs/api_reference.rst index d14a89bf..aa15135a 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -5,3 +5,10 @@ Api Reference :members: :undoc-members: :show-inheritance: + + +.. automethod:: pluggy._Result.get_result + +.. automethod:: pluggy._Result.force_result + +.. automethod:: pluggy._HookCaller.call_extra diff --git a/docs/index.rst b/docs/index.rst index ddb0548c..db65a102 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -152,9 +152,10 @@ then the *hookimpl* should be marked with the ``"optionalhook"`` option: 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: +By default hooks are :ref:`called ` 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 @@ -196,12 +197,16 @@ executed *first* or *last* respectively in the hook call loop: For another example see the `hook function ordering`_ section of the ``pytest`` docs. +.. note:: + ``tryfirst`` and ``trylast`` hooks are still invoked in LIFO order within + each category. + 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*. +of all corresponding non-wrappper *hookimpls*. Much in the same way as a `@contextlib.contextmanager`_, *hookwrappers* must be implemented as generator function with a single ``yield`` in its body: @@ -234,13 +239,15 @@ be implemented as generator function with a single ``yield`` in its body: if config.use_defaults: outcome.force_result(defaults) -The generator is `sent`_ a :py:class:`pluggy._CallOutcome` object which can +The generator is `sent`_ a :py:class:`pluggy._Result` 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. +the final result(s) returned back to the caller using the +:py:meth:`~pluggy._Result.force_result` or +:py:meth:`~pluggy._Result.get_result` methods. .. note:: Hook wrappers can **not** return results (as per generator function - semantics); they can only modify them using the ``_CallOutcome`` API. + semantics); they can only modify them using the ``_Result`` API. Also see the `hookwrapper`_ section in the ``pytest`` docs. @@ -477,6 +484,8 @@ You can retrieve the *options* applied to a particular http://doc.pytest.org/en/latest/writing_plugins.html#setuptools-entry-points +.. _calling: + Calling Hooks ************* The core functionality of ``pluggy`` enables an extension provider @@ -487,7 +496,7 @@ 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`. +which is an instance of this :py:class:`pluggy._HookRelay`. The ``_HookRelay`` itself contains references (by hook name) to each registered *hookimpl*'s ``_HookCaller`` instance. @@ -510,6 +519,40 @@ More practically you call a *hook* like so: Note that you **must** call hooks using keyword `arguments`_ 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(object): + def myhook(self, args): + """Default implementation. + """ + return 1 + + class Plugin2(object): + def myhook(self, args): + """Default implementation. + """ + return 2 + + class Plugin3(object): + 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 ------------------ @@ -562,7 +605,7 @@ 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. +using the :py:meth:`pluggy.PluginManager.subset_hook_caller()` method. You then can use that ``_HookCaller`` to make normal, ``call_historic()``, or ``call_extra()`` calls as necessary. diff --git a/pluggy/__init__.py b/pluggy/__init__.py index 54df5335..863b04d3 100644 --- a/pluggy/__init__.py +++ b/pluggy/__init__.py @@ -2,7 +2,7 @@ import warnings from .callers import _MultiCall, HookCallError, _raise_wrapfail, _Result -__version__ = '0.5.2' +__version__ = '0.5.3.dev' __all__ = ["PluginManager", "PluginValidationError", "HookCallError", "HookspecMarker", "HookimplMarker"] @@ -460,7 +460,7 @@ def before(hook_name, methods, kwargs): def after(outcome, hook_name, methods, kwargs): if outcome.excinfo is None: - hooktrace("finish", hook_name, "-->", outcome.result) + hooktrace("finish", hook_name, "-->", outcome.get_result()) hooktrace.root.indent -= 1 return self.add_hookcall_monitoring(before, after) diff --git a/pluggy/callers.py b/pluggy/callers.py index 517fcf68..de900d6e 100644 --- a/pluggy/callers.py +++ b/pluggy/callers.py @@ -26,8 +26,12 @@ class HookCallError(Exception): class _Result(object): def __init__(self, result, excinfo): - self.result = result - self.excinfo = excinfo + self._result = result + self._excinfo = excinfo + + @property + def excinfo(self): + return self._excinfo @classmethod def from_call(cls, func): @@ -41,15 +45,26 @@ def from_call(cls, func): return cls(result, excinfo) def force_result(self, result): - self.result = result - self.excinfo = None + """Force the result(s) to ``result``. + + If the hook was marked as a ``firstresult`` a single value should + be set otherwise set a (modified) list of results. Any exceptions + found during invocation will be deleted. + """ + self._result = result + self._excinfo = None def get_result(self): + """Get the result(s) for this hook call. + + If the hook was marked as a ``firstresult`` only a single value + will be returned otherwise a list of results. + """ __tracebackhide__ = True - if self.excinfo is None: - return self.result + if self._excinfo is None: + return self._result else: - ex = self.excinfo + ex = self._excinfo if _py3: raise ex[1].with_traceback(ex[2]) _reraise(*ex) # noqa diff --git a/testing/test_hookrelay.py b/testing/test_hookrelay.py index 7d9b6f52..a3b14783 100644 --- a/testing/test_hookrelay.py +++ b/testing/test_hookrelay.py @@ -64,6 +64,44 @@ def hello(self, arg): assert comprehensible in str(exc.value) +def test_call_order(pm): + class Api(object): + @hookspec + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + + class Plugin1(object): + @hookimpl + def hello(self, arg): + return 1 + + class Plugin2(object): + @hookimpl + def hello(self, arg): + return 2 + + class Plugin3(object): + @hookimpl + def hello(self, arg): + return 3 + + class Plugin4(object): + @hookimpl(hookwrapper=True) + def hello(self, arg): + assert arg == 0 + outcome = yield + assert outcome.get_result() == [3, 2, 1] + + pm.register(Plugin1()) + pm.register(Plugin2()) + pm.register(Plugin3()) + pm.register(Plugin4()) # hookwrapper should get same list result + res = pm.hook.hello(arg=0) + assert res == [3, 2, 1] + + def test_firstresult_definition(pm): class Api(object): @hookspec(firstresult=True)