From f6a96ecade003c4a1fbbe61df2a330466f3ea04d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 13 Aug 2023 21:23:49 +0300 Subject: [PATCH 1/7] docs/api_reference: order the types in a logical order --- docs/api_reference.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/api_reference.rst b/docs/api_reference.rst index b00009cb..08c50dbb 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -14,10 +14,15 @@ API Reference .. autodecorator:: pluggy.HookimplMarker -.. autoclass:: pluggy.Result() - :show-inheritance: +.. autoclass:: pluggy.HookRelay() :members: + .. data:: + + :type: HookCaller + + The caller for the hook with the given name. + .. autoclass:: pluggy.HookCaller() :members: :special-members: __call__ @@ -26,11 +31,6 @@ API Reference :show-inheritance: :members: -.. autoclass:: pluggy.HookRelay() +.. autoclass:: pluggy.Result() + :show-inheritance: :members: - - .. data:: - - :type: HookCaller - - The caller for the hook with the given name. From 9538f2a75d248afafeecfd7dbb44a28d4d12aba9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 13 Aug 2023 20:51:52 +0300 Subject: [PATCH 2/7] Rename `HookImplOpts` -> `HookimplOpts`, `HookSpecOpts` -> `HookspecOpts` We already have established types `HookimplMarker`, `HookspecMarker` so we should be consistent with the casing. The types haven't been public in a released version yet so OK to change. --- src/pluggy/__init__.py | 8 ++++---- src/pluggy/_hooks.py | 20 ++++++++++---------- src/pluggy/_manager.py | 12 ++++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/pluggy/__init__.py b/src/pluggy/__init__.py index ae5a18fd..850c47a3 100644 --- a/src/pluggy/__init__.py +++ b/src/pluggy/__init__.py @@ -10,8 +10,8 @@ "PluginValidationError", "HookCaller", "HookCallError", - "HookSpecOpts", - "HookImplOpts", + "HookspecOpts", + "HookimplOpts", "HookRelay", "HookspecMarker", "HookimplMarker", @@ -25,6 +25,6 @@ HookimplMarker, HookCaller, HookRelay, - HookSpecOpts, - HookImplOpts, + HookspecOpts, + HookimplOpts, ) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index 09907a3b..0581e8ad 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -37,7 +37,7 @@ _HookImplFunction = Callable[..., Union[_T, Generator[None, Result[_T], None]]] -class HookSpecOpts(TypedDict): +class HookspecOpts(TypedDict): """Options for a hook specification.""" #: Whether the hook is :ref:`first result only `. @@ -48,7 +48,7 @@ class HookSpecOpts(TypedDict): warn_on_impl: Warning | None -class HookImplOpts(TypedDict): +class HookimplOpts(TypedDict): """Options for a hook implementation.""" #: Whether the hook implementation is a :ref:`wrapper `. @@ -132,7 +132,7 @@ def __call__( # noqa: F811 def setattr_hookspec_opts(func: _F) -> _F: if historic and firstresult: raise ValueError("cannot have a historic firstresult hook") - opts: HookSpecOpts = { + opts: HookspecOpts = { "firstresult": firstresult, "historic": historic, "warn_on_impl": warn_on_impl, @@ -247,7 +247,7 @@ def __call__( # noqa: F811 """ def setattr_hookimpl_opts(func: _F) -> _F: - opts: HookImplOpts = { + opts: HookimplOpts = { "wrapper": wrapper, "hookwrapper": hookwrapper, "optionalhook": optionalhook, @@ -264,7 +264,7 @@ def setattr_hookimpl_opts(func: _F) -> _F: return setattr_hookimpl_opts(function) -def normalize_hookimpl_opts(opts: HookImplOpts) -> None: +def normalize_hookimpl_opts(opts: HookimplOpts) -> None: opts.setdefault("tryfirst", False) opts.setdefault("trylast", False) opts.setdefault("wrapper", False) @@ -379,7 +379,7 @@ def __init__( name: str, hook_execute: _HookExec, specmodule_or_class: _Namespace | None = None, - spec_opts: HookSpecOpts | None = None, + spec_opts: HookspecOpts | None = None, ) -> None: """:meta private:""" self.name: Final = name @@ -399,7 +399,7 @@ def has_spec(self) -> bool: def set_specification( self, specmodule_or_class: _Namespace, - spec_opts: HookSpecOpts, + spec_opts: HookspecOpts, ) -> None: if self.spec is not None: raise ValueError( @@ -522,7 +522,7 @@ def call_extra( not self.is_historic() ), "Cannot directly call a historic hook - use call_historic instead." self._verify_all_args_are_provided(kwargs) - opts: HookImplOpts = { + opts: HookimplOpts = { "wrapper": False, "hookwrapper": False, "optionalhook": False, @@ -626,7 +626,7 @@ def __init__( plugin: _Plugin, plugin_name: str, function: _HookImplFunction[object], - hook_impl_opts: HookImplOpts, + hook_impl_opts: HookimplOpts, ) -> None: self.function: Final = function self.argnames, self.kwargnames = varnames(self.function) @@ -654,7 +654,7 @@ class HookSpec: "warn_on_impl", ) - def __init__(self, namespace: _Namespace, name: str, opts: HookSpecOpts) -> None: + def __init__(self, namespace: _Namespace, name: str, opts: HookspecOpts) -> None: self.namespace = namespace self.function: Callable[..., object] = getattr(namespace, name) self.name = name diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 070ad83a..2aad8bbe 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -20,10 +20,10 @@ from ._hooks import _SubsetHookCaller from ._hooks import HookCaller from ._hooks import HookImpl -from ._hooks import HookImplOpts +from ._hooks import HookimplOpts from ._hooks import HookRelay from ._hooks import HookSpec -from ._hooks import HookSpecOpts +from ._hooks import HookspecOpts from ._hooks import normalize_hookimpl_opts from ._result import Result @@ -166,7 +166,7 @@ def register(self, plugin: _Plugin, name: str | None = None) -> str | None: hook._add_hookimpl(hookimpl) return plugin_name - def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookImplOpts | None: + def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None: """Try to obtain a hook implementation from an item with the given name in the given plugin which is being searched for hook impls. @@ -181,7 +181,7 @@ def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookImplOpts | None if not inspect.isroutine(method): return None try: - res: HookImplOpts | None = getattr( + res: HookimplOpts | None = getattr( method, self.project_name + "_impl", None ) except Exception: @@ -260,7 +260,7 @@ def add_hookspecs(self, module_or_class: _Namespace) -> None: def parse_hookspec_opts( self, module_or_class: _Namespace, name: str - ) -> HookSpecOpts | None: + ) -> HookspecOpts | None: """Try to obtain a hook specification from an item with the given name in the given module or class which is being searched for hook specs. @@ -273,7 +273,7 @@ def parse_hookspec_opts( options for items decorated with :class:`HookspecMarker`. """ method: HookSpec = getattr(module_or_class, name) - opts: HookSpecOpts | None = getattr(method, self.project_name + "_spec", None) + opts: HookspecOpts | None = getattr(method, self.project_name + "_spec", None) return opts def get_plugins(self) -> set[Any]: From de915b08f37e6dbed8822abb77ae91763d061529 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 13 Aug 2023 20:53:27 +0300 Subject: [PATCH 3/7] docs: add `HookspecOpts`, `HookimplOpts` to API reference The return value of the `pm.parse_hook{spec,impl}_opts` see need to document them. --- docs/api_reference.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 08c50dbb..8a9dde1a 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -34,3 +34,11 @@ API Reference .. autoclass:: pluggy.Result() :show-inheritance: :members: + +.. autoclass:: pluggy.HookspecOpts() + :show-inheritance: + :members: + +.. autoclass:: pluggy.HookimplOpts() + :show-inheritance: + :members: From 739fcd4e8aa61c6a2dea845bfe44f45245e2ff8a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 13 Aug 2023 21:18:28 +0300 Subject: [PATCH 4/7] Export `HookImpl` as `pluggy.HookImpl` Used in hook monitoring and in `pm.hook.my_hook.get_hookimpls()`. Refs #428. --- docs/api_reference.rst | 3 +++ docs/index.rst | 4 ++-- src/pluggy/__init__.py | 2 ++ src/pluggy/_hooks.py | 22 +++++++++++++++++++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 8a9dde1a..6580fa9e 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -35,6 +35,9 @@ API Reference :show-inheritance: :members: +.. autoclass:: pluggy.HookImpl() + :members: + .. autoclass:: pluggy.HookspecOpts() :show-inheritance: :members: diff --git a/docs/index.rst b/docs/index.rst index 68b065cd..d527a507 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -966,11 +966,11 @@ The expected signature and default implementations for these functions is: .. code-block:: python - def before(hook_name, methods, kwargs): + def before(hook_name, hook_impls, kwargs): pass - def after(outcome, hook_name, methods, kwargs): + def after(outcome, hook_name, hook_impls, kwargs): pass Public API diff --git a/src/pluggy/__init__.py b/src/pluggy/__init__.py index 850c47a3..7708ac3a 100644 --- a/src/pluggy/__init__.py +++ b/src/pluggy/__init__.py @@ -12,6 +12,7 @@ "HookCallError", "HookspecOpts", "HookimplOpts", + "HookImpl", "HookRelay", "HookspecMarker", "HookimplMarker", @@ -27,4 +28,5 @@ HookRelay, HookspecOpts, HookimplOpts, + HookImpl, ) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index 0581e8ad..a4006ca3 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -607,6 +607,8 @@ def __repr__(self) -> str: class HookImpl: + """A hook implementation in a :class:`HookCaller`.""" + __slots__ = ( "function", "argnames", @@ -628,15 +630,33 @@ def __init__( function: _HookImplFunction[object], hook_impl_opts: HookimplOpts, ) -> None: + """:meta private:""" + #: The hook implementation function. self.function: Final = function - self.argnames, self.kwargnames = varnames(self.function) + argnames, kwargnames = varnames(self.function) + #: The positional parameter names of ``function```. + self.argnames = argnames + #: The keyword parameter names of ``function```. + self.kwargnames = kwargnames + #: The plugin which defined this hook implementation. self.plugin = plugin + #: The :class:`HookimplOpts` used to configure this hook implementation. self.opts = hook_impl_opts + #: The name of the plugin which defined this hook implementation. self.plugin_name = plugin_name + #: Whether the hook implementation is a :ref:`wrapper `. self.wrapper = hook_impl_opts["wrapper"] + #: Whether the hook implementation is an :ref:`old-style wrapper + #: `. self.hookwrapper = hook_impl_opts["hookwrapper"] + #: Whether validation against a hook specification is :ref:`optional + #: `. self.optionalhook = hook_impl_opts["optionalhook"] + #: Whether to try to order this hook implementation :ref:`first + #: `. self.tryfirst = hook_impl_opts["tryfirst"] + #: Whether to try to order this hook implementation :ref:`last + #: `. self.trylast = hook_impl_opts["trylast"] def __repr__(self) -> str: From b4c214659c61f8c6746755af70aab58bd78372cc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 13 Aug 2023 21:41:26 +0300 Subject: [PATCH 5/7] manager: fix an incorrect (internal) type annotation --- src/pluggy/_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 2aad8bbe..4af589da 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -22,7 +22,6 @@ from ._hooks import HookImpl from ._hooks import HookimplOpts from ._hooks import HookRelay -from ._hooks import HookSpec from ._hooks import HookspecOpts from ._hooks import normalize_hookimpl_opts from ._result import Result @@ -272,7 +271,7 @@ def parse_hookspec_opts( customize how hook specifications are picked up. By default, returns the options for items decorated with :class:`HookspecMarker`. """ - method: HookSpec = getattr(module_or_class, name) + method = getattr(module_or_class, name) opts: HookspecOpts | None = getattr(method, self.project_name + "_spec", None) return opts From 80a73dcf58eb0bf8e707c89bd572a46cc69eb141 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 13 Aug 2023 21:35:16 +0300 Subject: [PATCH 6/7] Mark some fields and classes `Final` and `@final` Refs #428. --- src/pluggy/_hooks.py | 28 ++++++++++++++++++---------- src/pluggy/_manager.py | 4 ++-- src/pluggy/_result.py | 2 ++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index a4006ca3..916ca704 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -11,6 +11,7 @@ from typing import Any from typing import Callable from typing import Final +from typing import final from typing import Generator from typing import List from typing import Mapping @@ -69,6 +70,7 @@ class HookimplOpts(TypedDict): specname: str | None +@final class HookspecMarker: """Decorator for marking functions as hook specifications. @@ -146,6 +148,7 @@ def setattr_hookspec_opts(func: _F) -> _F: return setattr_hookspec_opts +@final class HookimplMarker: """Decorator for marking functions as hook implementations. @@ -341,6 +344,7 @@ def varnames(func: object) -> tuple[tuple[str, ...], tuple[str, ...]]: return args, kwargs +@final class HookRelay: """Hook holder object for performing 1:N hook calls where N is the number of registered plugins.""" @@ -382,10 +386,12 @@ def __init__( spec_opts: HookspecOpts | None = None, ) -> None: """:meta private:""" + #: Name of the hook getting called. self.name: Final = name self._hookexec: Final = hook_execute self._hookimpls: Final[list[HookImpl]] = [] self._call_history: _CallHistory | None = None + # TODO: Document, or make private. self.spec: HookSpec | None = None if specmodule_or_class is not None: assert spec_opts is not None @@ -606,6 +612,7 @@ def __repr__(self) -> str: return f"<_SubsetHookCaller {self.name!r}>" +@final class HookImpl: """A hook implementation in a :class:`HookCaller`.""" @@ -635,34 +642,35 @@ def __init__( self.function: Final = function argnames, kwargnames = varnames(self.function) #: The positional parameter names of ``function```. - self.argnames = argnames + self.argnames: Final = argnames #: The keyword parameter names of ``function```. - self.kwargnames = kwargnames + self.kwargnames: Final = kwargnames #: The plugin which defined this hook implementation. - self.plugin = plugin + self.plugin: Final = plugin #: The :class:`HookimplOpts` used to configure this hook implementation. - self.opts = hook_impl_opts + self.opts: Final = hook_impl_opts #: The name of the plugin which defined this hook implementation. - self.plugin_name = plugin_name + self.plugin_name: Final = plugin_name #: Whether the hook implementation is a :ref:`wrapper `. - self.wrapper = hook_impl_opts["wrapper"] + self.wrapper: Final = hook_impl_opts["wrapper"] #: Whether the hook implementation is an :ref:`old-style wrapper #: `. - self.hookwrapper = hook_impl_opts["hookwrapper"] + self.hookwrapper: Final = hook_impl_opts["hookwrapper"] #: Whether validation against a hook specification is :ref:`optional #: `. - self.optionalhook = hook_impl_opts["optionalhook"] + self.optionalhook: Final = hook_impl_opts["optionalhook"] #: Whether to try to order this hook implementation :ref:`first #: `. - self.tryfirst = hook_impl_opts["tryfirst"] + self.tryfirst: Final = hook_impl_opts["tryfirst"] #: Whether to try to order this hook implementation :ref:`last #: `. - self.trylast = hook_impl_opts["trylast"] + self.trylast: Final = hook_impl_opts["trylast"] def __repr__(self) -> str: return f"" +@final class HookSpec: __slots__ = ( "namespace", diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 4af589da..2d6d0da0 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -91,12 +91,12 @@ class PluginManager: def __init__(self, project_name: str) -> None: #: The project name. - self.project_name: Final[str] = project_name + self.project_name: Final = project_name self._name2plugin: Final[dict[str, _Plugin]] = {} self._plugin_distinfo: Final[list[tuple[_Plugin, DistFacade]]] = [] #: The "hook relay", used to call a hook on all registered plugins. #: See :ref:`calling`. - self.hook: Final[HookRelay] = HookRelay() + self.hook: Final = HookRelay() #: The tracing entry point. See :ref:`tracing`. self.trace: Final[_tracing.TagTracerSub] = _tracing.TagTracer().get( "pluginmanage" diff --git a/src/pluggy/_result.py b/src/pluggy/_result.py index af26a753..29859eb9 100644 --- a/src/pluggy/_result.py +++ b/src/pluggy/_result.py @@ -6,6 +6,7 @@ from types import TracebackType from typing import Callable from typing import cast +from typing import final from typing import Generator from typing import Generic from typing import NoReturn @@ -36,6 +37,7 @@ class HookCallError(Exception): """Hook was called incorrectly.""" +@final class Result(Generic[ResultType]): """An object used to inspect and set the result in a :ref:`hook wrapper `.""" From 20b419d7711ec84c5357efa2d6eefbd8ca1c4112 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 13 Aug 2023 20:48:09 +0300 Subject: [PATCH 7/7] docs: enable nitpicky mode Catch broken references. --- docs/conf.py | 13 +++++++++++++ src/pluggy/_manager.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 02ef01d9..ece874a2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,6 +61,19 @@ autodoc_member_order = "bysource" +nitpicky = True +nitpick_ignore = { + # Don't want to expose this yet (see #428). + ("py:class", "pluggy._tracing.TagTracerSub"), + # Compat hack, don't want to expose it. + ("py:class", "pluggy._manager.DistFacade"), + # `types.ModuleType` turns into `module` but then fails to resolve... + ("py:class", "module"), + # Just a TypeVar. + ("py:obj", "pluggy._result.ResultType"), + ("py:class", "pluggy._result.ResultType"), +} + # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 2d6d0da0..84717e6e 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -174,7 +174,7 @@ def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None This method can be overridden by ``PluginManager`` subclasses to customize how hook implementation are picked up. By default, returns the - options for items decorated with :class:`HookImplMarker`. + options for items decorated with :class:`HookimplMarker`. """ method: object = getattr(plugin, name) if not inspect.isroutine(method):