Skip to content

Commit 1037dc9

Browse files
author
goodboy
authored
Merge pull request #85 from tgoodlet/some_docs
Some outstanding docs
2 parents b1b9de2 + 5662715 commit 1037dc9

File tree

5 files changed

+121
-18
lines changed

5 files changed

+121
-18
lines changed

docs/api_reference.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,10 @@ Api Reference
55
:members:
66
:undoc-members:
77
:show-inheritance:
8+
9+
10+
.. automethod:: pluggy._Result.get_result
11+
12+
.. automethod:: pluggy._Result.force_result
13+
14+
.. automethod:: pluggy._HookCaller.call_extra

docs/index.rst

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,10 @@ then the *hookimpl* should be marked with the ``"optionalhook"`` option:
152152
153153
Call time order
154154
^^^^^^^^^^^^^^^
155-
A *hookimpl* can influence its call-time invocation position.
156-
If marked with a ``"tryfirst"`` or ``"trylast"`` option it will be
157-
executed *first* or *last* respectively in the hook call loop:
155+
By default hooks are :ref:`called <calling>` in LIFO registered order, however,
156+
a *hookimpl* can influence its call-time invocation position using special
157+
attributes. If marked with a ``"tryfirst"`` or ``"trylast"`` option it
158+
will be executed *first* or *last* respectively in the hook call loop:
158159

159160
.. code-block:: python
160161
@@ -196,12 +197,16 @@ executed *first* or *last* respectively in the hook call loop:
196197
For another example see the `hook function ordering`_ section of the
197198
``pytest`` docs.
198199

200+
.. note::
201+
``tryfirst`` and ``trylast`` hooks are still invoked in LIFO order within
202+
each category.
203+
199204
Wrappers
200205
^^^^^^^^
201206
A *hookimpl* can be marked with a ``"hookwrapper"`` option which indicates that
202207
the function will be called to *wrap* (or surround) all other normal *hookimpl*
203208
calls. A *hookwrapper* can thus execute some code ahead and after the execution
204-
of all corresponding non-hookwrappper *hookimpls*.
209+
of all corresponding non-wrappper *hookimpls*.
205210

206211
Much in the same way as a `@contextlib.contextmanager`_, *hookwrappers* must
207212
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:
234239
if config.use_defaults:
235240
outcome.force_result(defaults)
236241
237-
The generator is `sent`_ a :py:class:`pluggy._CallOutcome` object which can
242+
The generator is `sent`_ a :py:class:`pluggy._Result` object which can
238243
be assigned in the ``yield`` expression and used to override or inspect
239-
the final result(s) returned back to the hook caller.
244+
the final result(s) returned back to the caller using the
245+
:py:meth:`~pluggy._Result.force_result` or
246+
:py:meth:`~pluggy._Result.get_result` methods.
240247

241248
.. note::
242249
Hook wrappers can **not** return results (as per generator function
243-
semantics); they can only modify them using the ``_CallOutcome`` API.
250+
semantics); they can only modify them using the ``_Result`` API.
244251

245252
Also see the `hookwrapper`_ section in the ``pytest`` docs.
246253

@@ -477,6 +484,8 @@ You can retrieve the *options* applied to a particular
477484
http://doc.pytest.org/en/latest/writing_plugins.html#setuptools-entry-points
478485

479486

487+
.. _calling:
488+
480489
Calling Hooks
481490
*************
482491
The core functionality of ``pluggy`` enables an extension provider
@@ -487,7 +496,7 @@ a :py:class:`pluggy._HookCaller` which in turn *loops* through the
487496
``1:N`` registered *hookimpls* and calls them in sequence.
488497

489498
Every :py:class:`pluggy.PluginManager` has a ``hook`` attribute
490-
which is an instance of a :py:class:`pluggy._HookRelay`.
499+
which is an instance of this :py:class:`pluggy._HookRelay`.
491500
The ``_HookRelay`` itself contains references (by hook name) to each
492501
registered *hookimpl*'s ``_HookCaller`` instance.
493502

@@ -510,6 +519,40 @@ More practically you call a *hook* like so:
510519
511520
Note that you **must** call hooks using keyword `arguments`_ syntax!
512521

522+
Hook implementations are called in LIFO registered order: *the last
523+
registered plugin's hooks are called first*. As an example, the below
524+
assertion should not error:
525+
526+
.. code-block:: python
527+
528+
from pluggy import PluginManager, HookimplMarker
529+
530+
hookimpl = HookimplMarker('myproject')
531+
532+
class Plugin1(object):
533+
def myhook(self, args):
534+
"""Default implementation.
535+
"""
536+
return 1
537+
538+
class Plugin2(object):
539+
def myhook(self, args):
540+
"""Default implementation.
541+
"""
542+
return 2
543+
544+
class Plugin3(object):
545+
def myhook(self, args):
546+
"""Default implementation.
547+
"""
548+
return 3
549+
550+
pm = PluginManager('myproject')
551+
pm.register(Plugin1())
552+
pm.register(Plugin2())
553+
pm.register(Plugin3())
554+
555+
assert pm.hook.myhook(args=()) == [3, 2, 1]
513556
514557
Collecting results
515558
------------------
@@ -562,7 +605,7 @@ Calling with a subset of registered plugins
562605
-------------------------------------------
563606
You can make a call using a subset of plugins by asking the
564607
``PluginManager`` first for a ``_HookCaller`` with those plugins removed
565-
using the :py:meth:`pluggy.PluginManger.subset_hook_caller()` method.
608+
using the :py:meth:`pluggy.PluginManager.subset_hook_caller()` method.
566609

567610
You then can use that ``_HookCaller`` to make normal, ``call_historic()``,
568611
or ``call_extra()`` calls as necessary.

pluggy/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import warnings
33
from .callers import _MultiCall, HookCallError, _raise_wrapfail, _Result
44

5-
__version__ = '0.5.2'
5+
__version__ = '0.5.3.dev'
66

77
__all__ = ["PluginManager", "PluginValidationError", "HookCallError",
88
"HookspecMarker", "HookimplMarker"]
@@ -460,7 +460,7 @@ def before(hook_name, methods, kwargs):
460460

461461
def after(outcome, hook_name, methods, kwargs):
462462
if outcome.excinfo is None:
463-
hooktrace("finish", hook_name, "-->", outcome.result)
463+
hooktrace("finish", hook_name, "-->", outcome.get_result())
464464
hooktrace.root.indent -= 1
465465

466466
return self.add_hookcall_monitoring(before, after)

pluggy/callers.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ class HookCallError(Exception):
2626

2727
class _Result(object):
2828
def __init__(self, result, excinfo):
29-
self.result = result
30-
self.excinfo = excinfo
29+
self._result = result
30+
self._excinfo = excinfo
31+
32+
@property
33+
def excinfo(self):
34+
return self._excinfo
3135

3236
@classmethod
3337
def from_call(cls, func):
@@ -41,15 +45,26 @@ def from_call(cls, func):
4145
return cls(result, excinfo)
4246

4347
def force_result(self, result):
44-
self.result = result
45-
self.excinfo = None
48+
"""Force the result(s) to ``result``.
49+
50+
If the hook was marked as a ``firstresult`` a single value should
51+
be set otherwise set a (modified) list of results. Any exceptions
52+
found during invocation will be deleted.
53+
"""
54+
self._result = result
55+
self._excinfo = None
4656

4757
def get_result(self):
58+
"""Get the result(s) for this hook call.
59+
60+
If the hook was marked as a ``firstresult`` only a single value
61+
will be returned otherwise a list of results.
62+
"""
4863
__tracebackhide__ = True
49-
if self.excinfo is None:
50-
return self.result
64+
if self._excinfo is None:
65+
return self._result
5166
else:
52-
ex = self.excinfo
67+
ex = self._excinfo
5368
if _py3:
5469
raise ex[1].with_traceback(ex[2])
5570
_reraise(*ex) # noqa

testing/test_hookrelay.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,44 @@ def hello(self, arg):
6464
assert comprehensible in str(exc.value)
6565

6666

67+
def test_call_order(pm):
68+
class Api(object):
69+
@hookspec
70+
def hello(self, arg):
71+
"api hook 1"
72+
73+
pm.add_hookspecs(Api)
74+
75+
class Plugin1(object):
76+
@hookimpl
77+
def hello(self, arg):
78+
return 1
79+
80+
class Plugin2(object):
81+
@hookimpl
82+
def hello(self, arg):
83+
return 2
84+
85+
class Plugin3(object):
86+
@hookimpl
87+
def hello(self, arg):
88+
return 3
89+
90+
class Plugin4(object):
91+
@hookimpl(hookwrapper=True)
92+
def hello(self, arg):
93+
assert arg == 0
94+
outcome = yield
95+
assert outcome.get_result() == [3, 2, 1]
96+
97+
pm.register(Plugin1())
98+
pm.register(Plugin2())
99+
pm.register(Plugin3())
100+
pm.register(Plugin4()) # hookwrapper should get same list result
101+
res = pm.hook.hello(arg=0)
102+
assert res == [3, 2, 1]
103+
104+
67105
def test_firstresult_definition(pm):
68106
class Api(object):
69107
@hookspec(firstresult=True)

0 commit comments

Comments
 (0)