Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some outstanding docs #85

Merged
merged 7 commits into from
Sep 8, 2017
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
61 changes: 52 additions & 9 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <calling>` in LIFO registered order, however,
a *hookimpl* can influence its call-time invocation position using special
attributes. If marked with a ``"tryfirst"`` or ``"trylast"`` option it
will be executed *first* or *last* respectively in the hook call loop:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when more than one hookimpl are marked as tryfirst or trylast? I believe then LIFO registered order for them applies, but perhaps this should be explicitly mentioned.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so something like:

Note that tryfirst and trylast hooks are still invoked in LIFO order within each category

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


.. code-block:: python

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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
------------------
Expand Down Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions pluggy/callers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,21 @@ def from_call(cls, func):
return cls(result, excinfo)

def force_result(self, result):
"""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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to the PR: should we make all attributes private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah maybe?
I don't think the original _CallOutcome did this any differently fwiw.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh definitely, this wasn't a criticism of the current code, just something I thought I might bring up.

If we want to have a well-defined API we might want to start doing that somewhat sooner; a lot of problems of the pytest API stem from the fact that public attributes should never been so, but changing them later is a pain because client code now depends on it (even if by accident).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah totally agree.
I just wasn't sure if .result was initially supposed to be public.
I'm cool to switch it in this PR as long as you guys are.

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
Expand Down
38 changes: 38 additions & 0 deletions testing/test_hookrelay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down