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 steps towards exporting our typings #429

Merged
merged 7 commits into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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 @@
"""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 @@
# 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 @@
except BaseException as exc:
exception = exc
finally:
# Fast path - only new-style wrappers, no _Result.
# Fast path - only new-style wrappers, no Result.

Check warning on line 85 in src/pluggy/_callers.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_callers.py#L85

Added line #L85 was not covered by tests
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 @@
# 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