Skip to content

Commit

Permalink
Merge pull request #429 from bluetech/typing2
Browse files Browse the repository at this point in the history
Some steps towards exporting our typings
  • Loading branch information
bluetech committed Aug 12, 2023
2 parents b41572e + 04e9a9b commit 475c4aa
Show file tree
Hide file tree
Showing 14 changed files with 327 additions and 163 deletions.
1 change: 1 addition & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ python:
# Without this, sphinx can't find pluggy's version.
- method: pip
path: .
- requirements: docs/requirements.txt

build:
os: ubuntu-22.04
Expand Down
4 changes: 2 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Features

- `#260 <https://github.com/pytest-dev/pluggy/issues/260>`_: Added "new-style" hook wrappers, a simpler but equally powerful alternative to the existing ``hookwrapper=True`` wrappers.

New-style wrappers are generator functions, similarly to ``hookwrapper``, but do away with the :class:`result <pluggy._result._Result>` object.
New-style wrappers are generator functions, similarly to ``hookwrapper``, but do away with the :class:`result <pluggy.Result>` object.
Instead, the return value is sent directly to the ``yield`` statement, or, if inner calls raised an exception, it is raised from the ``yield``.
The wrapper is expected to return a value or raise an exception, which will become the result of the hook call.

Expand All @@ -64,7 +64,7 @@ Features
- `#364 <https://github.com/pytest-dev/pluggy/issues/364>`_: Python 3.11 and 3.12 are now officially supported.


- `#394 <https://github.com/pytest-dev/pluggy/issues/394>`_: Added the :meth:`~pluggy._result._Result.force_exception` method to ``_Result``.
- `#394 <https://github.com/pytest-dev/pluggy/issues/394>`_: Added the :meth:`~pluggy.Result.force_exception` method to ``_Result``.

``force_exception`` allows (old-style) hookwrappers to force an exception or override/adjust an existing exception of a hook invocation,
in a properly behaving manner. Using ``force_exception`` is preferred over raising an exception from the hookwrapper,
Expand Down
37 changes: 27 additions & 10 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,34 @@
API Reference
=============

.. automodule:: pluggy
.. autoclass:: pluggy.PluginManager
:members:
:undoc-members:

.. autoclass:: pluggy._result._Result
.. automethod:: pluggy._result._Result.get_result
.. automethod:: pluggy._result._Result.force_result
.. automethod:: pluggy._result._Result.force_exception
.. autoclass:: pluggy.PluginValidationError
:show-inheritance:
:members:

.. autodecorator:: pluggy.HookspecMarker

.. autodecorator:: pluggy.HookimplMarker

.. autoclass:: pluggy.Result()
:show-inheritance:
:members:

.. autoclass:: pluggy.HookCaller()
:members:
:special-members: __call__

.. autoclass:: pluggy.HookCallError()
:show-inheritance:
:members:

.. autoclass:: pluggy.HookRelay()
:members:

.. data:: <hook name>

.. autoclass:: pluggy._hooks._HookCaller
.. automethod:: pluggy._hooks._HookCaller.call_extra
.. automethod:: pluggy._hooks._HookCaller.call_historic
:type: HookCaller

.. autoclass:: pluggy._hooks._HookRelay
The caller for the hook with the given name.
3 changes: 2 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"github_type": "star",
"badge_branch": "master",
"page_width": "1080px",
"sidebar_width": "300px",
"fixed_sidebar": "false",
}
html_sidebars = {
Expand All @@ -58,7 +59,7 @@
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "pluggy", "pluggy Documentation", [author], 1)]

autodoc_typehints = "none"
autodoc_member_order = "bysource"

# -- Options for Texinfo output -------------------------------------------

Expand Down
53 changes: 32 additions & 21 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ be matched and checked against the ``setup_project`` hookspec:
return config
.. _callorder:

Call time order
^^^^^^^^^^^^^^^
By default hooks are :ref:`called <calling>` in LIFO registered order, however,
Expand Down Expand Up @@ -422,6 +424,8 @@ to the hook caller.

Also see the :ref:`pytest:hookwrapper` section in the ``pytest`` docs.

.. _old_style_hookwrappers:

Old-style wrappers
^^^^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -468,21 +472,21 @@ execution of all corresponding non-wrappper *hookimpls*.
if config.use_defaults:
outcome.force_result(defaults)
The generator is :py:meth:`sent <python:generator.send>` a :py:class:`pluggy._result._Result` object which can
The generator is :py:meth:`sent <python:generator.send>` a :py:class:`pluggy.Result` object which can
be assigned in the ``yield`` expression and used to inspect
the final result(s) or exceptions returned back to the caller using the
:py:meth:`~pluggy._result._Result.get_result` method, override the result
using the :py:meth:`~pluggy._result._Result.force_result`, or override
the exception using the :py:meth:`~pluggy._result._Result.force_exception`
:py:meth:`~pluggy.Result.get_result` method, override the result
using the :py:meth:`~pluggy.Result.force_result`, or override
the exception using the :py:meth:`~pluggy.Result.force_exception`
method.

.. note::
Old-style hook wrappers can **not** return results; they can only modify
them using the :py:meth:`~pluggy._result._Result.force_result` API.
them using the :py:meth:`~pluggy.Result.force_result` API.

Old-style Hook wrappers should **not** raise exceptions; this will cause
further hookwrappers to be skipped. They should use
:py:meth:`~pluggy._result._Result.force_exception` to adjust the
:py:meth:`~pluggy.Result.force_exception` to adjust the
exception.

.. _specs:
Expand Down Expand Up @@ -611,7 +615,7 @@ Also see the :ref:`pytest:firstresult` section in the ``pytest`` docs.
Historic hooks
^^^^^^^^^^^^^^
You can mark a *hookspec* as being *historic* meaning that the hook
can be called with :py:meth:`~pluggy._hooks._HookCaller.call_historic()` **before**
can be called with :py:meth:`~pluggy.HookCaller.call_historic()` **before**
having been registered:

.. code-block:: python
Expand All @@ -629,12 +633,14 @@ dynamically loaded plugins.
For more info see :ref:`call_historic`.


.. _warn_on_impl:

Warnings on hook implementation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

As projects evolve new hooks may be introduced and/or deprecated.

if a hookspec specifies a ``warn_on_impl``, pluggy will trigger it for any plugin implementing the hook.
If a hookspec specifies a ``warn_on_impl``, pluggy will trigger it for any plugin implementing the hook.


.. code-block:: python
Expand Down Expand Up @@ -733,13 +739,13 @@ 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._hooks._HookCaller` which in turn *loops* through the
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 this :py:class:`pluggy._hooks._HookRelay`.
The :py:class:`~pluggy._hooks._HookRelay` itself contains references
(by hook name) to each registered *hookimpl*'s :py:class:`~pluggy._hooks._HookCaller` instance.
which is an instance of :py:class:`pluggy.HookRelay`.
The :py:class:`~pluggy.HookRelay` itself contains references
(by hook name) to each registered *hookimpl*'s :py:class:`~pluggy.HookCaller` instance.

More practically you call a *hook* like so:

Expand All @@ -755,7 +761,7 @@ More practically you call a *hook* like so:
pm.add_hookspecs(mypluginspec)
pm.register(myplugin)
# we invoke the _HookCaller and thus all underlying hookimpls
# 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 :std:term:`python:argument` syntax!
Expand Down Expand Up @@ -874,7 +880,7 @@ only useful if you expect that some *hookimpls* may be registered **after** the
hook is initially invoked.

Historic hooks must be :ref:`specially marked <historic>` and called
using the :py:meth:`~pluggy._hooks._HookCaller.call_historic()` method:
using the :py:meth:`~pluggy.HookCaller.call_historic()` method:

.. code-block:: python
Expand All @@ -895,8 +901,8 @@ using the :py:meth:`~pluggy._hooks._HookCaller.call_historic()` method:
# historic callback is invoked here
pm.register(mylateplugin)
Note that if you :py:meth:`~pluggy._hooks._HookCaller.call_historic()`
the :py:class:`~pluggy._hooks._HookCaller` (and thus your calling code)
Note that if you :py:meth:`~pluggy.HookCaller.call_historic()`
the :py:class:`~pluggy.HookCaller` (and thus your calling code)
can not receive results back from the underlying *hookimpl* functions.
Instead you can provide a *callback* for processing results (like the
``callback`` function above) which will be called as each new plugin
Expand All @@ -907,23 +913,28 @@ is registered.
hooks since only the first registered plugin's hook(s) would
ever be called.

.. _call_extra:

Calling with extras
-------------------
You can call a hook with temporarily participating *implementation* functions
(that aren't in the registry) using the
:py:meth:`pluggy._hooks._HookCaller.call_extra()` method.
: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
:py:class:`~pluggy.PluginManager` first for a
:py:class:`~pluggy._hooks._HookCaller` with those plugins removed
:py:class:`~pluggy.HookCaller` with those plugins removed
using the :py:meth:`pluggy.PluginManager.subset_hook_caller()` method.

You then can use that :py:class:`_HookCaller <pluggy._hooks._HookCaller>`
to make normal, :py:meth:`~pluggy._hooks._HookCaller.call_historic`, or
:py:meth:`~pluggy._hooks._HookCaller.call_extra` calls as necessary.
You then can use that :py:class:`~pluggy.HookCaller`
to make normal, :py:meth:`~pluggy.HookCaller.call_historic`, or
:py:meth:`~pluggy.HookCaller.call_extra` calls as necessary.


.. _tracing:

Built-in tracing
****************
Expand Down
4 changes: 4 additions & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Higher bound for safety, can bump it if builds fine with new major versions.
sphinx>=6,<8
pygments
towncrier
16 changes: 14 additions & 2 deletions src/pluggy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,23 @@
__all__ = [
"PluginManager",
"PluginValidationError",
"HookCaller",
"HookCallError",
"HookSpecOpts",
"HookImplOpts",
"HookRelay",
"HookspecMarker",
"HookimplMarker",
"Result",
]

from ._manager import PluginManager, PluginValidationError
from ._result import HookCallError
from ._hooks import HookspecMarker, HookimplMarker
from ._result import HookCallError, Result
from ._hooks import (
HookspecMarker,
HookimplMarker,
HookCaller,
HookRelay,
HookSpecOpts,
HookImplOpts,
)
14 changes: 7 additions & 7 deletions src/pluggy/_callers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@

from ._hooks import HookImpl
from ._result import _raise_wrapfail
from ._result import _Result
from ._result import HookCallError
from ._result import Result


# Need to distinguish between old- and new-style hook wrappers.
# Wrapping one a singleton tuple is the fastest type-safe way I found to do it.
Teardown = Union[
Tuple[Generator[None, _Result[object], None]],
Tuple[Generator[None, Result[object], None]],
Generator[None, object, object],
]

Expand All @@ -33,7 +33,7 @@ def _multicall(
"""Execute a call into multiple python functions/methods and return the
result(s).
``caller_kwargs`` comes from _HookCaller.__call__().
``caller_kwargs`` comes from HookCaller.__call__().
"""
__tracebackhide__ = True
results: list[object] = []
Expand All @@ -58,7 +58,7 @@ def _multicall(
# If this cast is not valid, a type error is raised below,
# which is the desired response.
res = hook_impl.function(*args)
wrapper_gen = cast(Generator[None, _Result[object], None], res)
wrapper_gen = cast(Generator[None, Result[object], None], res)
next(wrapper_gen) # first yield
teardowns.append((wrapper_gen,))
except StopIteration:
Expand All @@ -82,7 +82,7 @@ def _multicall(
except BaseException as exc:
exception = exc
finally:
# Fast path - only new-style wrappers, no _Result.
# Fast path - only new-style wrappers, no Result.
if only_new_style_wrappers:
if firstresult: # first result hooks return a single value
result = results[0] if results else None
Expand Down Expand Up @@ -117,11 +117,11 @@ def _multicall(
# Slow path - need to support old-style wrappers.
else:
if firstresult: # first result hooks return a single value
outcome: _Result[object | list[object]] = _Result(
outcome: Result[object | list[object]] = Result(
results[0] if results else None, exception
)
else:
outcome = _Result(results, exception)
outcome = Result(results, exception)

# run all wrapper post-yield blocks
for teardown in reversed(teardowns):
Expand Down
Loading

0 comments on commit 475c4aa

Please sign in to comment.