Skip to content
Open
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
8 changes: 8 additions & 0 deletions changelog/704.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Hook options are now :class:`pluggy.HookspecConfiguration` /
:class:`pluggy.HookimplConfiguration` objects (markers attach these instead of
dicts). ``PluginManager.parse_hookimpl_opts`` /
``parse_hookspec_opts`` remain as a deprecated pytest/support concession that
returns legacy dicts and are only invoked during registration when a subclass
overrides them and no modern configuration attribute was found.
``HookspecOpts`` / ``HookimplOpts`` TypedDicts remain importable for
pytest/typing compatibility.
6 changes: 2 additions & 4 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,10 @@ API Reference
.. autoclass:: pluggy.HookImpl()
:members:

.. autoclass:: pluggy.HookspecOpts()
:show-inheritance:
.. autoclass:: pluggy.HookspecConfiguration()
:members:

.. autoclass:: pluggy.HookimplOpts()
:show-inheritance:
.. autoclass:: pluggy.HookimplConfiguration()
:members:


Expand Down
9 changes: 6 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -767,10 +767,13 @@ and particular plugins in it:

Parsing mark options
^^^^^^^^^^^^^^^^^^^^
You can retrieve the *options* applied to a particular
*hookspec* or *hookimpl* as per :ref:`marking_hooks` using the
Markers attach :class:`~pluggy.HookspecConfiguration` /
:class:`~pluggy.HookimplConfiguration` objects to functions. The
:py:meth:`~pluggy.PluginManager.parse_hookspec_opts()` and
:py:meth:`~pluggy.PluginManager.parse_hookimpl_opts()` respectively.
:py:meth:`~pluggy.PluginManager.parse_hookimpl_opts()` methods remain as a
**deprecated** pytest/support concession that returns legacy dict-shaped
options; registration only calls them when a subclass overrides them and no
modern configuration attribute was found.


.. _calling:
Expand Down
8 changes: 6 additions & 2 deletions src/pluggy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
"HookCaller",
"HookImpl",
"HookRelay",
"HookimplConfiguration",
"HookimplMarker",
"HookimplOpts",
"HookspecConfiguration",
"HookspecMarker",
"HookspecOpts",
"PluggyTeardownRaisedWarning",
Expand All @@ -14,15 +16,17 @@
"Result",
"__version__",
]
from ._config import HookimplConfiguration
from ._config import HookspecConfiguration
from ._hooks import HookCaller
from ._hooks import HookImpl
from ._hooks import HookimplMarker
from ._hooks import HookimplOpts
from ._hooks import HookRelay
from ._hooks import HookspecMarker
from ._hooks import HookspecOpts
from ._manager import PluginManager
from ._manager import PluginValidationError
from ._pytest_compat import HookimplOpts
from ._pytest_compat import HookspecOpts
from ._result import HookCallError
from ._result import Result
from ._warnings import PluggyTeardownRaisedWarning
Expand Down
23 changes: 8 additions & 15 deletions src/pluggy/_caller.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
from typing import TypeAlias
import warnings

from ._config import HookimplOpts
from ._config import HookspecOpts
from ._config import HookimplConfiguration
from ._config import HookspecConfiguration
from ._decorators import _Namespace
from ._decorators import HookSpec
from ._implementation import _Plugin
Expand Down Expand Up @@ -69,7 +69,7 @@ def __init__(
name: str,
hook_execute: _HookExec,
specmodule_or_class: _Namespace | None = None,
spec_opts: HookspecOpts | None = None,
spec_opts: HookspecConfiguration | None = None,
) -> None:
""":meta private:"""
#: Name of the hook getting called.
Expand Down Expand Up @@ -98,15 +98,15 @@ def has_spec(self) -> bool:
def set_specification(
self,
specmodule_or_class: _Namespace,
spec_opts: HookspecOpts,
spec_opts: HookspecConfiguration,
) -> None:
if self.spec is not None:
raise ValueError(
f"Hook {self.spec.name!r} is already registered "
f"within namespace {self.spec.namespace}"
)
self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
if spec_opts.get("historic"):
if spec_opts.historic:
self._call_history = []

def is_historic(self) -> bool:
Expand Down Expand Up @@ -183,7 +183,7 @@ def __call__(self, **kwargs: object) -> Any:
"Cannot directly call a historic hook - use call_historic instead."
)
self._verify_all_args_are_provided(kwargs)
firstresult = self.spec.opts.get("firstresult", False) if self.spec else False
firstresult = self.spec.opts.firstresult if self.spec else False
# Copy because plugins may register other plugins during iteration (#438).
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)

Expand Down Expand Up @@ -224,14 +224,7 @@ def call_extra(
"Cannot directly call a historic hook - use call_historic instead."
)
self._verify_all_args_are_provided(kwargs)
opts: HookimplOpts = {
"wrapper": False,
"hookwrapper": False,
"optionalhook": False,
"trylast": False,
"tryfirst": False,
"specname": None,
}
opts = HookimplConfiguration()
hookimpls = self._hookimpls.copy()
for method in methods:
hookimpl = HookImpl(None, "<temp>", method, opts)
Expand All @@ -245,7 +238,7 @@ def call_extra(
):
i -= 1
hookimpls.insert(i + 1, hookimpl)
firstresult = self.spec.opts.get("firstresult", False) if self.spec else False
firstresult = self.spec.opts.firstresult if self.spec else False
return self._hookexec(self.name, hookimpls, kwargs, firstresult)

def _maybe_apply_history(self, method: HookImpl) -> None:
Expand Down
207 changes: 160 additions & 47 deletions src/pluggy/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,163 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import TypedDict


class HookspecOpts(TypedDict):
"""Options for a hook specification."""

#: Whether the hook is :ref:`first result only <firstresult>`.
firstresult: bool
#: Whether the hook is :ref:`historic <historic>`.
historic: bool
#: Whether the hook :ref:`warns when implemented <warn_on_impl>`.
warn_on_impl: Warning | None
#: Whether the hook warns when :ref:`certain arguments are requested
#: <warn_on_impl>`.
#:
#: .. versionadded:: 1.5
warn_on_impl_args: Mapping[str, Warning] | None


class HookimplOpts(TypedDict):
"""Options for a hook implementation."""

#: Whether the hook implementation is a :ref:`wrapper <hookwrapper>`.
wrapper: bool
#: Whether the hook implementation is an :ref:`old-style wrapper
#: <old_style_hookwrappers>`.
hookwrapper: bool
#: Whether validation against a hook specification is :ref:`optional
#: <optionalhook>`.
optionalhook: bool
#: Whether to try to order this hook implementation :ref:`first
#: <callorder>`.
tryfirst: bool
#: Whether to try to order this hook implementation :ref:`last
#: <callorder>`.
trylast: bool
#: The name of the hook specification to match, see :ref:`specname`.
specname: str | None


def normalize_hookimpl_opts(opts: HookimplOpts) -> None:
opts.setdefault("tryfirst", False)
opts.setdefault("trylast", False)
opts.setdefault("wrapper", False)
opts.setdefault("hookwrapper", False)
opts.setdefault("optionalhook", False)
opts.setdefault("specname", None)
from typing import Any
from typing import Final
from typing import final


@final
class HookspecConfiguration:
"""Configuration for a hook specification."""

__slots__ = (
"firstresult",
"historic",
"warn_on_impl",
"warn_on_impl_args",
)
firstresult: Final[bool]
historic: Final[bool]
warn_on_impl: Final[Warning | None]
warn_on_impl_args: Final[Mapping[str, Warning] | None]

def __init__(
self,
firstresult: bool = False,
historic: bool = False,
warn_on_impl: Warning | None = None,
warn_on_impl_args: Mapping[str, Warning] | None = None,
) -> None:
if historic and firstresult:
raise ValueError("cannot have a historic firstresult hook")
#: Whether the hook is :ref:`first result only <firstresult>`.
self.firstresult = firstresult
#: Whether the hook is :ref:`historic <historic>`.
self.historic = historic
#: Whether the hook :ref:`warns when implemented <warn_on_impl>`.
self.warn_on_impl = warn_on_impl
#: Whether the hook warns when :ref:`certain arguments are requested
#: <warn_on_impl>`.
self.warn_on_impl_args = warn_on_impl_args

def __repr__(self) -> str:
attrs = [
f"{slot}={getattr(self, slot)!r}"
for slot in self.__slots__
if getattr(self, slot)
]
return f"HookspecConfiguration({', '.join(attrs)})"


@final
class HookimplConfiguration:
"""Configuration for a hook implementation."""

__slots__ = (
"hookwrapper",
"optionalhook",
"specname",
"tryfirst",
"trylast",
"wrapper",
)
wrapper: Final[bool]
hookwrapper: Final[bool]
optionalhook: Final[bool]
tryfirst: Final[bool]
trylast: Final[bool]
specname: Final[str | None]

def __init__(
self,
wrapper: bool = False,
hookwrapper: bool = False,
optionalhook: bool = False,
tryfirst: bool = False,
trylast: bool = False,
specname: str | None = None,
) -> None:
#: Whether the hook implementation is a :ref:`wrapper <hookwrapper>`.
self.wrapper = wrapper
#: Whether the hook implementation is an :ref:`old-style wrapper
#: <old_style_hookwrappers>`.
self.hookwrapper = hookwrapper
#: Whether validation against a hook specification is :ref:`optional
#: <optionalhook>`.
self.optionalhook = optionalhook
#: Whether to try to order this hook implementation :ref:`first
#: <callorder>`.
self.tryfirst = tryfirst
#: Whether to try to order this hook implementation :ref:`last
#: <callorder>`.
self.trylast = trylast
#: The name of the hook specification to match, see :ref:`specname`.
self.specname = specname

def __repr__(self) -> str:
attrs = [
f"{slot}={getattr(self, slot)!r}"
for slot in self.__slots__
if getattr(self, slot)
]
return f"HookimplConfiguration({', '.join(attrs)})"


def hookspec_config_from_mapping(
opts: Mapping[str, Any],
) -> HookspecConfiguration:
"""Build a :class:`HookspecConfiguration` from a mapping.

Intended for pytest/support migration only — not the public options API.
Prefer constructing :class:`HookspecConfiguration` directly.
"""
return HookspecConfiguration(
firstresult=bool(opts.get("firstresult", False)),
historic=bool(opts.get("historic", False)),
warn_on_impl=opts.get("warn_on_impl"),
warn_on_impl_args=opts.get("warn_on_impl_args"),
)


def hookimpl_config_from_mapping(
opts: Mapping[str, Any],
) -> HookimplConfiguration:
"""Build a :class:`HookimplConfiguration` from a mapping.

Intended for pytest/support migration only — not the public options API.
Prefer constructing :class:`HookimplConfiguration` directly.
"""
return HookimplConfiguration(
wrapper=bool(opts.get("wrapper", False)),
hookwrapper=bool(opts.get("hookwrapper", False)),
optionalhook=bool(opts.get("optionalhook", False)),
tryfirst=bool(opts.get("tryfirst", False)),
trylast=bool(opts.get("trylast", False)),
specname=opts.get("specname"),
)


def hookspec_config_to_mapping(
config: HookspecConfiguration,
) -> dict[str, Any]:
"""Serialize configuration to a legacy mapping (pytest/support only)."""
return {
"firstresult": config.firstresult,
"historic": config.historic,
"warn_on_impl": config.warn_on_impl,
"warn_on_impl_args": config.warn_on_impl_args,
}


def hookimpl_config_to_mapping(
config: HookimplConfiguration,
) -> dict[str, Any]:
"""Serialize configuration to a legacy mapping (pytest/support only)."""
return {
"wrapper": config.wrapper,
"hookwrapper": config.hookwrapper,
"optionalhook": config.optionalhook,
"tryfirst": config.tryfirst,
"trylast": config.trylast,
"specname": config.specname,
}
Loading