From 9abeeba57d1bbf183cd81b28ae083fff11fa3385 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 4 Aug 2023 10:57:44 +0300 Subject: [PATCH 1/7] docs: require newer Sphinx in RTD By default <2 is used, but we want to use new features. Refs #428 --- .readthedocs.yml | 1 + docs/requirements.txt | 4 ++++ tox.ini | 4 +--- 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yml b/.readthedocs.yml index ac7528ca..d751431e 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -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 diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..7d0b87a3 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +# Higher bound for safety, can bump it if builds fine with new major versions. +sphinx>=6,<8 +pygments +towncrier diff --git a/tox.ini b/tox.ini index 3f50fedc..de464a07 100644 --- a/tox.ini +++ b/tox.ini @@ -22,9 +22,7 @@ deps= [testenv:docs] deps = - sphinx - pygments - towncrier + -r{toxinidir}/docs/requirements.txt commands = python scripts/towncrier-draft-to-file.py # the '-t changelog_towncrier_draft' tags makes sphinx include the draft From 0d2850a76525aae5c4f03d0d5e987f2b8e90f944 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 4 Aug 2023 22:36:16 +0300 Subject: [PATCH 2/7] docs: improve docstrings and API Reference Refs #428 --- docs/api_reference.rst | 37 ++++++++--- docs/conf.py | 2 +- docs/index.rst | 13 +++- src/pluggy/_hooks.py | 136 ++++++++++++++++++++++++++++------------- src/pluggy/_manager.py | 73 ++++++++++++++++++---- src/pluggy/_result.py | 7 +++ 6 files changed, 202 insertions(+), 66 deletions(-) diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 4e73fbaf..ae241049 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -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._Result() + :show-inheritance: + :members: + +.. autoclass:: pluggy._hooks._HookCaller() + :members: + :special-members: __call__ + +.. autoclass:: pluggy.HookCallError() + :show-inheritance: + :members: + +.. autoclass:: pluggy._hooks._HookRelay() + :members: + + .. data:: -.. 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. diff --git a/docs/conf.py b/docs/conf.py index da873cad..66b9b3f2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,7 +58,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 ------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 339fb9e2..4761e01e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 ` in LIFO registered order, however, @@ -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 ^^^^^^^^^^^^^^^^^^ @@ -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 @@ -907,6 +913,8 @@ 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 @@ -925,6 +933,9 @@ You then can use that :py:class:`_HookCaller ` to make normal, :py:meth:`~pluggy._hooks._HookCaller.call_historic`, or :py:meth:`~pluggy._hooks._HookCaller.call_extra` calls as necessary. + +.. _tracing: + Built-in tracing **************** ``pluggy`` comes with some batteries included hook tracing for your diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index af3ceef8..e2df9288 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -38,17 +38,34 @@ class _HookSpecOpts(TypedDict): + """Options for a hook specification.""" + + #: Whether the hook is :ref:`first result only `. firstresult: bool + #: Whether the hook is :ref:`historic `. historic: bool + #: Whether the hook :ref:`warns when implemented `. warn_on_impl: Warning | None class _HookImplOpts(TypedDict): + """Options for a hook implementation.""" + + #: Whether the hook implementation is a :ref:`wrapper `. wrapper: bool + #: Whether the hook implementation is an :ref:`old-style wrapper + #: `. hookwrapper: bool + #: Whether validation against a hook specification is :ref:`optional + #: `. optionalhook: bool + #: Whether to try to order this hook implementation :ref:`first + #: `. tryfirst: bool + #: Whether to try to order this hook implementation :ref:`last + #: `. trylast: bool + #: The name of the hook specification to match, see :ref:`specname`. specname: str | None @@ -57,7 +74,7 @@ class HookspecMarker: Instantiate it with a project_name to get a decorator. Calling :meth:`PluginManager.add_hookspecs` later will discover all marked - functions if the :class:`PluginManager` uses the same project_name. + functions if the :class:`PluginManager` uses the same project name. """ __slots__ = ("project_name",) @@ -98,12 +115,18 @@ def __call__( # noqa: F811 If passed no function, returns a decorator which can be applied to a function later using the attributes supplied. - If ``firstresult`` is ``True``, the 1:N hook call (N being the number of - registered hook implementation functions) will stop at I<=N when the - I'th function returns a non-``None`` result. + :param firstresult: + If ``True``, the 1:N hook call (N being the number of registered + hook implementation functions) will stop at I<=N when the I'th + function returns a non-``None`` result. See :ref:`firstresult`. - If ``historic`` is ``True``, every call to the hook will be memorized - and replayed on plugins registered after the call was made. + :param historic: + If ``True``, every call to the hook will be memorized and replayed + on plugins registered after the call was made. See :ref:`historic`. + + :param warn_on_impl: + If given, every implementation of this hook will trigger the given + warning. See :ref:`warn_on_impl`. """ def setattr_hookspec_opts(func: _F) -> _F: @@ -128,7 +151,7 @@ class HookimplMarker: Instantiate it with a ``project_name`` to get a decorator. Calling :meth:`PluginManager.register` later will discover all marked - functions if the :class:`PluginManager` uses the same project_name. + functions if the :class:`PluginManager` uses the same project name. """ __slots__ = ("project_name",) @@ -178,36 +201,46 @@ def __call__( # noqa: F811 If passed no function, returns a decorator which can be applied to a function later using the attributes supplied. - If ``optionalhook`` is ``True``, a missing matching hook specification - will not result in an error (by default it is an error if no matching - spec is found). - - If ``tryfirst`` is ``True``, this hook implementation will run as early - as possible in the chain of N hook implementations for a specification. - - If ``trylast`` is ``True``, this hook implementation will run as late as - possible in the chain of N hook implementations. - - If ``wrapper`` is ``True``("new-style hook wrapper"), the hook - implementation needs to execute exactly one ``yield``. The code before - the ``yield`` is run early before any non-hook-wrapper function is run. - The code after the ``yield`` is run after all non-hook-wrapper functions - have run. The ``yield`` receives the result value of the inner calls, or - raises the exception of inner calls (including earlier hook wrapper - calls). The return value of the function becomes the return value of the - hook, and a raised exception becomes the exception of the hook. - - If ``hookwrapper`` is ``True`` ("old-style hook wrapper"), the hook - implementation needs to execute exactly one ``yield``. The code before - the ``yield`` is run early before any non-hook-wrapper function is run. - The code after the ``yield`` is run after all non-hook-wrapper function - have run The ``yield`` receives a :class:`_Result` object representing - the exception or result outcome of the inner calls (including earlier - hook wrapper calls). This option is mutually exclusive with ``wrapper``. - - If ``specname`` is provided, it will be used instead of the function - name when matching this hook implementation to a hook specification - during registration. + :param optionalhook: + If ``True``, a missing matching hook specification will not result + in an error (by default it is an error if no matching spec is + found). See :ref:`optionalhook`. + + :param tryfirst: + If ``True``, this hook implementation will run as early as possible + in the chain of N hook implementations for a specification. See + :ref:`callorder`. + + :param trylast: + If ``True``, this hook implementation will run as late as possible + in the chain of N hook implementations for a specification. See + :ref:`callorder`. + + :param wrapper: + If ``True`` ("new-style hook wrapper"), the hook implementation + needs to execute exactly one ``yield``. The code before the + ``yield`` is run early before any non-hook-wrapper function is run. + The code after the ``yield`` is run after all non-hook-wrapper + functions have run. The ``yield`` receives the result value of the + inner calls, or raises the exception of inner calls (including + earlier hook wrapper calls). The return value of the function + becomes the return value of the hook, and a raised exception becomes + the exception of the hook. See :ref:`hookwrapper`. + + :param hookwrapper: + If ``True`` ("old-style hook wrapper"), the hook implementation + needs to execute exactly one ``yield``. The code before the + ``yield`` is run early before any non-hook-wrapper function is run. + The code after the ``yield`` is run after all non-hook-wrapper + function have run The ``yield`` receives a :class:`_Result` object + representing the exception or result outcome of the inner calls + (including earlier hook wrapper calls). This option is mutually + exclusive with ``wrapper``. See :ref:`old_style_hookwrapper`. + + :param specname: + If provided, the given name will be used instead of the function + name when matching this hook implementation to a hook specification + during registration. See :ref:`specname`. .. versionadded:: 1.2.0 The ``wrapper`` parameter. @@ -314,6 +347,9 @@ class _HookRelay: __slots__ = ("__dict__",) + def __init__(self) -> None: + """:meta private:""" + if TYPE_CHECKING: def __getattr__(self, name: str) -> _HookCaller: @@ -324,6 +360,8 @@ def __getattr__(self, name: str) -> _HookCaller: class _HookCaller: + """A caller of all registered implementations of a hook specification.""" + __slots__ = ( "name", "spec", @@ -339,6 +377,7 @@ def __init__( specmodule_or_class: _Namespace | None = None, spec_opts: _HookSpecOpts | None = None, ) -> None: + """:meta private:""" self.name: Final = name self._hookexec: Final = hook_execute self._hookimpls: Final[list[HookImpl]] = [] @@ -348,9 +387,11 @@ def __init__( assert spec_opts is not None self.set_specification(specmodule_or_class, spec_opts) + # TODO: Document, or make private. def has_spec(self) -> bool: return self.spec is not None + # TODO: Document, or make private. def set_specification( self, specmodule_or_class: _Namespace, @@ -366,6 +407,7 @@ def set_specification( self._call_history = [] def is_historic(self) -> bool: + """Whether this caller is :ref:`historic `.""" return self._call_history is not None def _remove_plugin(self, plugin: _Plugin) -> None: @@ -376,6 +418,7 @@ def _remove_plugin(self, plugin: _Plugin) -> None: raise ValueError(f"plugin {plugin!r} not found") def get_hookimpls(self) -> list[HookImpl]: + """Get all registered hook implementations for this hook.""" return self._hookimpls.copy() def _add_hookimpl(self, hookimpl: HookImpl) -> None: @@ -424,6 +467,14 @@ def _verify_all_args_are_provided(self, kwargs: Mapping[str, object]) -> None: break def __call__(self, **kwargs: object) -> Any: + """Call the hook. + + Only accepts keyword arguments, which should match the hook + specification. + + Returns the result(s) of calling all registered plugins, see + :ref:`calling`. + """ assert ( not self.is_historic() ), "Cannot directly call a historic hook - use call_historic instead." @@ -437,10 +488,12 @@ def call_historic( kwargs: Mapping[str, object] | None = None, ) -> None: """Call the hook with given ``kwargs`` for all registered plugins and - for all plugins which will be registered afterwards. + for all plugins which will be registered afterwards, see + :ref:`historic`. - If ``result_callback`` is provided, it will be called for each - non-``None`` result obtained from a hook implementation. + :param result_callback: + If provided, will be called for each non-``None`` result obtained + from a hook implementation. """ assert self._call_history is not None kwargs = kwargs or {} @@ -459,7 +512,8 @@ def call_extra( self, methods: Sequence[Callable[..., object]], kwargs: Mapping[str, object] ) -> Any: """Call the hook with some additional temporarily participating - methods using the specified ``kwargs`` as call parameters.""" + methods using the specified ``kwargs`` as call parameters, see + :ref:`call_extra`.""" assert ( not self.is_historic() ), "Cannot directly call a historic hook - use call_historic instead." diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 30153286..5eae9d68 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -46,10 +46,12 @@ class PluginValidationError(Exception): """Plugin failed validation. :param plugin: The plugin which failed validation. + :param message: Error message. """ def __init__(self, plugin: _Plugin, message: str) -> None: super().__init__(message) + #: The plugin which failed validation. self.plugin = plugin @@ -83,14 +85,23 @@ class PluginManager: For debugging purposes you can call :meth:`PluginManager.enable_tracing` which will subsequently send debug information to the trace helper. + + :param project_name: + The short project name. Prefer snake case. Make sure it's unique! """ def __init__(self, project_name: str) -> None: - self.project_name: Final = project_name + #: The project name. + self.project_name: Final[str] = project_name self._name2plugin: Final[dict[str, _Plugin]] = {} self._plugin_distinfo: Final[list[tuple[_Plugin, DistFacade]]] = [] - self.trace: Final = _tracing.TagTracer().get("pluginmanage") - self.hook: Final = _HookRelay() + #: The "hook relay", used to call a hook on all registered plugins. + #: See :ref:`calling`. + self.hook: Final[_HookRelay] = _HookRelay() + #: The tracing entry point. See :ref:`tracing`. + self.trace: Final[_tracing.TagTracerSub] = _tracing.TagTracer().get( + "pluginmanage" + ) self._inner_hookexec = _multicall def _hookexec( @@ -107,10 +118,13 @@ def _hookexec( def register(self, plugin: _Plugin, name: str | None = None) -> str | None: """Register a plugin and return its name. - If a name is not specified, a name is generated using - :func:`get_canonical_name`. + :param name: + The name under which to register the plugin. If not specified, a + name is generated using :func:`get_canonical_name`. - If the name is blocked from registering, returns ``None``. + :returns: + The plugin name. If the name is blocked from registering, returns + ``None``. If the plugin is already registered, raises a :exc:`ValueError`. """ @@ -153,6 +167,16 @@ def register(self, plugin: _Plugin, name: str | None = None) -> str | None: return plugin_name 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. + + :returns: + The parsed hookimpl options, or None to skip the given item. + + 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`. + """ method: object = getattr(plugin, name) if not inspect.isroutine(method): return None @@ -169,11 +193,13 @@ def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> _HookImplOpts | Non def unregister( self, plugin: _Plugin | None = None, name: str | None = None - ) -> _Plugin: + ) -> Any | None: """Unregister a plugin and all of its hook implementations. The plugin can be specified either by the plugin object or the plugin name. If both are specified, they must agree. + + Returns the unregistered plugin, or ``None`` if not found. """ if name is None: assert plugin is not None, "one of name or plugin needs to be specified" @@ -182,6 +208,8 @@ def unregister( if plugin is None: plugin = self.get_plugin(name) + if plugin is None: + return None hookcallers = self.get_hookcallers(plugin) if hookcallers: @@ -233,6 +261,17 @@ def add_hookspecs(self, module_or_class: _Namespace) -> None: def parse_hookspec_opts( self, module_or_class: _Namespace, name: str ) -> _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. + + :returns: + The parsed hookspec options for defining a hook, or None to skip the + given item. + + This method can be overridden by ``PluginManager`` subclasses to + 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) opts: _HookSpecOpts | None = getattr(method, self.project_name + "_spec", None) return opts @@ -250,7 +289,7 @@ def get_canonical_name(self, plugin: _Plugin) -> str: Note that a plugin may be registered under a different name specified by the caller of :meth:`register(plugin, name) `. - To obtain the name of n registered plugin use :meth:`get_name(plugin) + To obtain the name of a registered plugin use :meth:`get_name(plugin) ` instead. """ name: str | None = getattr(plugin, "__name__", None) @@ -338,10 +377,13 @@ def check_pending(self) -> None: def load_setuptools_entrypoints(self, group: str, name: str | None = None) -> int: """Load modules from querying the specified setuptools ``group``. - :param str group: Entry point group to load plugins. - :param str name: If given, loads only plugins with the given ``name``. - :rtype: int - :return: The number of plugins loaded by this call. + :param group: + Entry point group to load plugins. + :param name: + If given, loads only plugins with the given ``name``. + + :return: + The number of plugins loaded by this call. """ count = 0 for dist in list(importlib.metadata.distributions()): @@ -370,7 +412,12 @@ def list_name_plugin(self) -> list[tuple[str, _Plugin]]: return list(self._name2plugin.items()) def get_hookcallers(self, plugin: _Plugin) -> list[_HookCaller] | None: - """Get all hook callers for the specified plugin.""" + """Get all hook callers for the specified plugin. + + :returns: + The hook callers, or ``None`` if ``plugin`` is not registered in + this plugin manager. + """ if self.get_name(plugin) is None: return None hookcallers = [] diff --git a/src/pluggy/_result.py b/src/pluggy/_result.py index 47eb4618..42387a88 100644 --- a/src/pluggy/_result.py +++ b/src/pluggy/_result.py @@ -37,6 +37,9 @@ class HookCallError(Exception): class _Result(Generic[_T]): + """An object used to inspect and set the result in a :ref:`hook wrapper + `.""" + __slots__ = ("_result", "_exception") def __init__( @@ -44,11 +47,13 @@ def __init__( result: _T | None, exception: BaseException | None, ) -> None: + """:meta private:""" self._result = result self._exception = exception @property def excinfo(self) -> _ExcInfo | None: + """:meta private:""" exc = self._exception if exc is None: return None @@ -57,10 +62,12 @@ def excinfo(self) -> _ExcInfo | None: @property def exception(self) -> BaseException | None: + """:meta private:""" return self._exception @classmethod def from_call(cls, func: Callable[[], _T]) -> _Result[_T]: + """:meta private:""" __tracebackhide__ = True result = exception = None try: From 6f28953a8590c22064c141e9ebb5b996677090e2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 5 Aug 2023 10:19:17 +0300 Subject: [PATCH 3/7] docs: increase sidebar size a bit So it doesn't overflow to the content. Probably there's a better fix for this. --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 66b9b3f2..02ef01d9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,6 +47,7 @@ "github_type": "star", "badge_branch": "master", "page_width": "1080px", + "sidebar_width": "300px", "fixed_sidebar": "false", } html_sidebars = { From d3e72b0d9a1089e49948b039b83b32b16fb9e559 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 5 Aug 2023 09:50:32 +0300 Subject: [PATCH 4/7] Rename _Result -> Result, export as `pluggy.Result` For typing purposes, refs #428. The old name `pluggy._result._Result` is kept for backward compatibility, no reason to break the "offenders" who have imported it before. --- CHANGELOG.rst | 4 ++-- docs/api_reference.rst | 2 +- docs/index.rst | 12 ++++++------ src/pluggy/__init__.py | 3 ++- src/pluggy/_callers.py | 12 ++++++------ src/pluggy/_hooks.py | 6 +++--- src/pluggy/_manager.py | 10 +++++----- src/pluggy/_result.py | 20 ++++++++++++-------- 8 files changed, 37 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 15f936aa..2ee5dcc1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -51,7 +51,7 @@ Features - `#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 ` object. + New-style wrappers are generator functions, similarly to ``hookwrapper``, but do away with the :class:`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. @@ -64,7 +64,7 @@ Features - `#364 `_: Python 3.11 and 3.12 are now officially supported. -- `#394 `_: Added the :meth:`~pluggy._result._Result.force_exception` method to ``_Result``. +- `#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, diff --git a/docs/api_reference.rst b/docs/api_reference.rst index ae241049..7b52a1a1 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -14,7 +14,7 @@ API Reference .. autodecorator:: pluggy.HookimplMarker -.. autoclass:: pluggy._result._Result() +.. autoclass:: pluggy.Result() :show-inheritance: :members: diff --git a/docs/index.rst b/docs/index.rst index 4761e01e..8a54de5c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -472,21 +472,21 @@ execution of all corresponding non-wrappper *hookimpls*. if config.use_defaults: outcome.force_result(defaults) -The generator is :py:meth:`sent ` a :py:class:`pluggy._result._Result` object which can +The generator is :py:meth:`sent ` 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: diff --git a/src/pluggy/__init__.py b/src/pluggy/__init__.py index c530bffc..0acdda9a 100644 --- a/src/pluggy/__init__.py +++ b/src/pluggy/__init__.py @@ -11,8 +11,9 @@ "HookCallError", "HookspecMarker", "HookimplMarker", + "Result", ] from ._manager import PluginManager, PluginValidationError -from ._result import HookCallError +from ._result import HookCallError, Result from ._hooks import HookspecMarker, HookimplMarker diff --git a/src/pluggy/_callers.py b/src/pluggy/_callers.py index 3d63a2c6..9393a582 100644 --- a/src/pluggy/_callers.py +++ b/src/pluggy/_callers.py @@ -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], ] @@ -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: @@ -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 @@ -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): diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index e2df9288..238cbb30 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -23,7 +23,7 @@ from typing import TypeVar from typing import Union -from ._result import _Result +from ._result import Result _T = TypeVar("_T") @@ -34,7 +34,7 @@ [str, Sequence["HookImpl"], Mapping[str, object], bool], Union[object, List[object]], ] -_HookImplFunction = Callable[..., Union[_T, Generator[None, _Result[_T], None]]] +_HookImplFunction = Callable[..., Union[_T, Generator[None, Result[_T], None]]] class _HookSpecOpts(TypedDict): @@ -232,7 +232,7 @@ def __call__( # noqa: F811 needs to execute exactly one ``yield``. The code before the ``yield`` is run early before any non-hook-wrapper function is run. The code after the ``yield`` is run after all non-hook-wrapper - function have run The ``yield`` receives a :class:`_Result` object + function have run The ``yield`` receives a :class:`Result` object representing the exception or result outcome of the inner calls (including earlier hook wrapper calls). This option is mutually exclusive with ``wrapper``. See :ref:`old_style_hookwrapper`. diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 5eae9d68..3a215f2e 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -25,11 +25,11 @@ from ._hooks import HookImpl from ._hooks import HookSpec from ._hooks import normalize_hookimpl_opts -from ._result import _Result +from ._result import Result _BeforeTrace = Callable[[str, Sequence[HookImpl], Mapping[str, Any]], None] -_AfterTrace = Callable[[_Result[Any], str, Sequence[HookImpl], Mapping[str, Any]], None] +_AfterTrace = Callable[[Result[Any], str, Sequence[HookImpl], Mapping[str, Any]], None] def _warn_for_function(warning: Warning, function: Callable[..., object]) -> None: @@ -439,7 +439,7 @@ def add_hookcall_monitoring( of HookImpl instances and the keyword arguments for the hook call. ``after(outcome, hook_name, hook_impls, kwargs)`` receives the - same arguments as ``before`` but also a :class:`~pluggy._result._Result` object + same arguments as ``before`` but also a :class:`~pluggy.Result` object which represents the result of the overall hook call. """ oldcall = self._inner_hookexec @@ -451,7 +451,7 @@ def traced_hookexec( firstresult: bool, ) -> object | list[object]: before(hook_name, hook_impls, caller_kwargs) - outcome = _Result.from_call( + outcome = Result.from_call( lambda: oldcall(hook_name, hook_impls, caller_kwargs, firstresult) ) after(outcome, hook_name, hook_impls, caller_kwargs) @@ -478,7 +478,7 @@ def before( hooktrace(hook_name, kwargs) def after( - outcome: _Result[object], + outcome: Result[object], hook_name: str, methods: Sequence[HookImpl], kwargs: Mapping[str, object], diff --git a/src/pluggy/_result.py b/src/pluggy/_result.py index 42387a88..af26a753 100644 --- a/src/pluggy/_result.py +++ b/src/pluggy/_result.py @@ -16,12 +16,12 @@ _ExcInfo = Tuple[Type[BaseException], BaseException, Optional[TracebackType]] -_T = TypeVar("_T") +ResultType = TypeVar("ResultType") def _raise_wrapfail( wrap_controller: ( - Generator[None, _Result[_T], None] | Generator[None, object, object] + Generator[None, Result[ResultType], None] | Generator[None, object, object] ), msg: str, ) -> NoReturn: @@ -36,7 +36,7 @@ class HookCallError(Exception): """Hook was called incorrectly.""" -class _Result(Generic[_T]): +class Result(Generic[ResultType]): """An object used to inspect and set the result in a :ref:`hook wrapper `.""" @@ -44,7 +44,7 @@ class _Result(Generic[_T]): def __init__( self, - result: _T | None, + result: ResultType | None, exception: BaseException | None, ) -> None: """:meta private:""" @@ -66,7 +66,7 @@ def exception(self) -> BaseException | None: return self._exception @classmethod - def from_call(cls, func: Callable[[], _T]) -> _Result[_T]: + def from_call(cls, func: Callable[[], ResultType]) -> Result[ResultType]: """:meta private:""" __tracebackhide__ = True result = exception = None @@ -76,7 +76,7 @@ def from_call(cls, func: Callable[[], _T]) -> _Result[_T]: exception = exc return cls(result, exception) - def force_result(self, result: _T) -> None: + def force_result(self, result: ResultType) -> None: """Force the result(s) to ``result``. If the hook was marked as a ``firstresult`` a single value should @@ -98,7 +98,7 @@ def force_exception(self, exception: BaseException) -> None: self._result = None self._exception = exception - def get_result(self) -> _T: + def get_result(self) -> ResultType: """Get the result(s) for this hook call. If the hook was marked as a ``firstresult`` only a single value @@ -107,6 +107,10 @@ def get_result(self) -> _T: __tracebackhide__ = True exc = self._exception if exc is None: - return cast(_T, self._result) + return cast(ResultType, self._result) else: raise exc.with_traceback(exc.__traceback__) + + +# Historical name (pluggy<=1.2), kept for backward compatibility. +_Result = Result From 1b5fd512ca9aa6ceb3d6695aae883c8ecb5742d0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 4 Aug 2023 23:25:52 +0300 Subject: [PATCH 5/7] Rename _HookCaller -> HookCaller, export as `pluggy.HookCaller` For typing purposes, refs #428. The old name `pluggy._hooks._HookCaller` is kept for backward compatibility, no reason to break the "offenders" who have imported it before. --- docs/api_reference.rst | 4 ++-- docs/index.rst | 24 ++++++++++++------------ src/pluggy/__init__.py | 3 ++- src/pluggy/_callers.py | 2 +- src/pluggy/_hooks.py | 16 ++++++++++------ src/pluggy/_manager.py | 22 +++++++++++----------- testing/test_hookcaller.py | 24 ++++++++++++------------ testing/test_pluginmanager.py | 2 +- 8 files changed, 51 insertions(+), 46 deletions(-) diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 7b52a1a1..8ae50bb4 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -18,7 +18,7 @@ API Reference :show-inheritance: :members: -.. autoclass:: pluggy._hooks._HookCaller() +.. autoclass:: pluggy.HookCaller() :members: :special-members: __call__ @@ -31,6 +31,6 @@ API Reference .. data:: - :type: _HookCaller + :type: HookCaller The caller for the hook with the given name. diff --git a/docs/index.rst b/docs/index.rst index 8a54de5c..cfdb0720 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -615,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 @@ -739,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. +(by hook name) to each registered *hookimpl*'s :py:class:`~pluggy.HookCaller` instance. More practically you call a *hook* like so: @@ -761,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! @@ -880,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 ` and called -using the :py:meth:`~pluggy._hooks._HookCaller.call_historic()` method: +using the :py:meth:`~pluggy.HookCaller.call_historic()` method: .. code-block:: python @@ -901,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 @@ -919,19 +919,19 @@ 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 ` -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: diff --git a/src/pluggy/__init__.py b/src/pluggy/__init__.py index 0acdda9a..308729f9 100644 --- a/src/pluggy/__init__.py +++ b/src/pluggy/__init__.py @@ -8,6 +8,7 @@ __all__ = [ "PluginManager", "PluginValidationError", + "HookCaller", "HookCallError", "HookspecMarker", "HookimplMarker", @@ -16,4 +17,4 @@ from ._manager import PluginManager, PluginValidationError from ._result import HookCallError, Result -from ._hooks import HookspecMarker, HookimplMarker +from ._hooks import HookspecMarker, HookimplMarker, HookCaller diff --git a/src/pluggy/_callers.py b/src/pluggy/_callers.py index 9393a582..6498eaed 100644 --- a/src/pluggy/_callers.py +++ b/src/pluggy/_callers.py @@ -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] = [] diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index 238cbb30..42c9691b 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -352,14 +352,14 @@ def __init__(self) -> None: if TYPE_CHECKING: - def __getattr__(self, name: str) -> _HookCaller: + def __getattr__(self, name: str) -> HookCaller: ... _CallHistory = List[Tuple[Mapping[str, object], Optional[Callable[[Any], None]]]] -class _HookCaller: +class HookCaller: """A caller of all registered implementations of a hook specification.""" __slots__ = ( @@ -446,7 +446,7 @@ def _add_hookimpl(self, hookimpl: HookImpl) -> None: self._hookimpls.insert(i + 1, hookimpl) def __repr__(self) -> str: - return f"<_HookCaller {self.name!r}>" + return f"" def _verify_all_args_are_provided(self, kwargs: Mapping[str, object]) -> None: # This is written to avoid expensive operations when not needed. @@ -553,11 +553,15 @@ def _maybe_apply_history(self, method: HookImpl) -> None: result_callback(res[0]) -class _SubsetHookCaller(_HookCaller): +# Historical name (pluggy<=1.2), kept for backward compatibility. +_HookCaller = HookCaller + + +class _SubsetHookCaller(HookCaller): """A proxy to another HookCaller which manages calls to all registered plugins except the ones from remove_plugins.""" - # This class is unusual: in inhertits from `_HookCaller` so all of + # This class is unusual: in inhertits from `HookCaller` so all of # the *code* runs in the class, but it delegates all underlying *data* # to the original HookCaller. # `subset_hook_caller` used to be implemented by creating a full-fledged @@ -572,7 +576,7 @@ class _SubsetHookCaller(_HookCaller): "_remove_plugins", ) - def __init__(self, orig: _HookCaller, remove_plugins: AbstractSet[_Plugin]) -> None: + def __init__(self, orig: HookCaller, remove_plugins: AbstractSet[_Plugin]) -> None: self._orig = orig self._remove_plugins = remove_plugins self.name = orig.name # type: ignore[misc] diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 3a215f2e..a403417c 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -14,7 +14,6 @@ from . import _tracing from ._callers import _multicall -from ._hooks import _HookCaller from ._hooks import _HookImplFunction from ._hooks import _HookImplOpts from ._hooks import _HookRelay @@ -22,6 +21,7 @@ from ._hooks import _Namespace from ._hooks import _Plugin from ._hooks import _SubsetHookCaller +from ._hooks import HookCaller from ._hooks import HookImpl from ._hooks import HookSpec from ._hooks import normalize_hookimpl_opts @@ -156,9 +156,9 @@ def register(self, plugin: _Plugin, name: str | None = None) -> str | None: method: _HookImplFunction[object] = getattr(plugin, name) hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts) name = hookimpl_opts.get("specname") or name - hook: _HookCaller | None = getattr(self.hook, name, None) + hook: HookCaller | None = getattr(self.hook, name, None) if hook is None: - hook = _HookCaller(name, self._hookexec) + hook = HookCaller(name, self._hookexec) setattr(self.hook, name, hook) elif hook.has_spec(): self._verify_hook(hook, hookimpl) @@ -242,9 +242,9 @@ def add_hookspecs(self, module_or_class: _Namespace) -> None: for name in dir(module_or_class): spec_opts = self.parse_hookspec_opts(module_or_class, name) if spec_opts is not None: - hc: _HookCaller | None = getattr(self.hook, name, None) + hc: HookCaller | None = getattr(self.hook, name, None) if hc is None: - hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts) + hc = HookCaller(name, self._hookexec, module_or_class, spec_opts) setattr(self.hook, name, hc) else: # Plugins registered this hook without knowing the spec. @@ -311,7 +311,7 @@ def get_name(self, plugin: _Plugin) -> str | None: return name return None - def _verify_hook(self, hook: _HookCaller, hookimpl: HookImpl) -> None: + def _verify_hook(self, hook: HookCaller, hookimpl: HookImpl) -> None: if hook.is_historic() and (hookimpl.hookwrapper or hookimpl.wrapper): raise PluginValidationError( hookimpl.plugin, @@ -364,7 +364,7 @@ def check_pending(self) -> None: :exc:`PluginValidationError`.""" for name in self.hook.__dict__: if name[0] != "_": - hook: _HookCaller = getattr(self.hook, name) + hook: HookCaller = getattr(self.hook, name) if not hook.has_spec(): for hookimpl in hook.get_hookimpls(): if not hookimpl.optionalhook: @@ -411,7 +411,7 @@ def list_name_plugin(self) -> list[tuple[str, _Plugin]]: """Return a list of (name, plugin) pairs for all registered plugins.""" return list(self._name2plugin.items()) - def get_hookcallers(self, plugin: _Plugin) -> list[_HookCaller] | None: + def get_hookcallers(self, plugin: _Plugin) -> list[HookCaller] | None: """Get all hook callers for the specified plugin. :returns: @@ -491,11 +491,11 @@ def after( def subset_hook_caller( self, name: str, remove_plugins: Iterable[_Plugin] - ) -> _HookCaller: - """Return a proxy :py:class:`._hooks._HookCaller` instance for the named + ) -> HookCaller: + """Return a proxy :class:`~pluggy.HookCaller` instance for the named method which manages calls to all registered plugins except the ones from remove_plugins.""" - orig: _HookCaller = getattr(self.hook, name) + orig: HookCaller = getattr(self.hook, name) plugins_to_remove = {plug for plug in remove_plugins if hasattr(plug, name)} if plugins_to_remove: return _SubsetHookCaller(orig, plugins_to_remove) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index ce1cc831..88ed1316 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -9,7 +9,7 @@ from pluggy import HookspecMarker from pluggy import PluginManager from pluggy import PluginValidationError -from pluggy._hooks import _HookCaller +from pluggy._hooks import HookCaller from pluggy._hooks import HookImpl hookspec = HookspecMarker("example") @@ -17,7 +17,7 @@ @pytest.fixture -def hc(pm: PluginManager) -> _HookCaller: +def hc(pm: PluginManager) -> HookCaller: class Hooks: @hookspec def he_method1(self, arg: object) -> None: @@ -31,7 +31,7 @@ def he_method1(self, arg: object) -> None: class AddMeth: - def __init__(self, hc: _HookCaller) -> None: + def __init__(self, hc: HookCaller) -> None: self.hc = hc def __call__( @@ -57,7 +57,7 @@ def wrap(func: FuncT) -> FuncT: @pytest.fixture -def addmeth(hc: _HookCaller) -> AddMeth: +def addmeth(hc: HookCaller) -> AddMeth: return AddMeth(hc) @@ -65,7 +65,7 @@ def funcs(hookmethods: Sequence[HookImpl]) -> List[Callable[..., object]]: return [hookmethod.function for hookmethod in hookmethods] -def test_adding_nonwrappers(hc: _HookCaller, addmeth: AddMeth) -> None: +def test_adding_nonwrappers(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth() def he_method1() -> None: pass @@ -81,7 +81,7 @@ def he_method3() -> None: assert funcs(hc.get_hookimpls()) == [he_method1, he_method2, he_method3] -def test_adding_nonwrappers_trylast(hc: _HookCaller, addmeth: AddMeth) -> None: +def test_adding_nonwrappers_trylast(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth() def he_method1_middle() -> None: pass @@ -97,7 +97,7 @@ def he_method1_b() -> None: assert funcs(hc.get_hookimpls()) == [he_method1, he_method1_middle, he_method1_b] -def test_adding_nonwrappers_trylast3(hc: _HookCaller, addmeth: AddMeth) -> None: +def test_adding_nonwrappers_trylast3(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth() def he_method1_a() -> None: pass @@ -122,7 +122,7 @@ def he_method1_d() -> None: ] -def test_adding_nonwrappers_trylast2(hc: _HookCaller, addmeth: AddMeth) -> None: +def test_adding_nonwrappers_trylast2(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth() def he_method1_middle() -> None: pass @@ -138,7 +138,7 @@ def he_method1() -> None: assert funcs(hc.get_hookimpls()) == [he_method1, he_method1_middle, he_method1_b] -def test_adding_nonwrappers_tryfirst(hc: _HookCaller, addmeth: AddMeth) -> None: +def test_adding_nonwrappers_tryfirst(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth(tryfirst=True) def he_method1() -> None: pass @@ -154,7 +154,7 @@ def he_method1_b() -> None: assert funcs(hc.get_hookimpls()) == [he_method1_middle, he_method1_b, he_method1] -def test_adding_wrappers_ordering(hc: _HookCaller, addmeth: AddMeth) -> None: +def test_adding_wrappers_ordering(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth(hookwrapper=True) def he_method1(): yield @@ -184,7 +184,7 @@ def he_method3(): ] -def test_adding_wrappers_ordering_tryfirst(hc: _HookCaller, addmeth: AddMeth) -> None: +def test_adding_wrappers_ordering_tryfirst(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth(hookwrapper=True, tryfirst=True) def he_method1(): yield @@ -200,7 +200,7 @@ def he_method3(): assert funcs(hc.get_hookimpls()) == [he_method2, he_method1, he_method3] -def test_adding_wrappers_complex(hc: _HookCaller, addmeth: AddMeth) -> None: +def test_adding_wrappers_complex(hc: HookCaller, addmeth: AddMeth) -> None: assert funcs(hc.get_hookimpls()) == [] @addmeth(hookwrapper=True, trylast=True) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index c021ca14..81b86b65 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -276,7 +276,7 @@ def he_method1(self, arg): @pytest.mark.parametrize("result_callback", [True, False]) def test_with_result_memorized(pm: PluginManager, result_callback: bool) -> None: - """Verify that ``_HookCaller._maybe_apply_history()` + """Verify that ``HookCaller._maybe_apply_history()` correctly applies the ``result_callback`` function, when provided, to the result from calling each newly registered hook. """ From 9a9e3c274141a25ef617195b4ccc67cd68497465 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 5 Aug 2023 09:50:48 +0300 Subject: [PATCH 6/7] Rename _HookRelay -> HookRelay, export it as `pluggy.HookRelay` For typing purposes, refs #428. The old name `pluggy._hooks._HookRelay` is kept for backward compatibility, no reason to break the "offenders" who have imported it before. --- docs/api_reference.rst | 2 +- docs/index.rst | 4 ++-- src/pluggy/__init__.py | 3 ++- src/pluggy/_hooks.py | 6 +++++- src/pluggy/_manager.py | 4 ++-- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 8ae50bb4..b00009cb 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -26,7 +26,7 @@ API Reference :show-inheritance: :members: -.. autoclass:: pluggy._hooks._HookRelay() +.. autoclass:: pluggy.HookRelay() :members: .. data:: diff --git a/docs/index.rst b/docs/index.rst index cfdb0720..68b065cd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -743,8 +743,8 @@ 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 +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: diff --git a/src/pluggy/__init__.py b/src/pluggy/__init__.py index 308729f9..ffd516c2 100644 --- a/src/pluggy/__init__.py +++ b/src/pluggy/__init__.py @@ -10,6 +10,7 @@ "PluginValidationError", "HookCaller", "HookCallError", + "HookRelay", "HookspecMarker", "HookimplMarker", "Result", @@ -17,4 +18,4 @@ from ._manager import PluginManager, PluginValidationError from ._result import HookCallError, Result -from ._hooks import HookspecMarker, HookimplMarker, HookCaller +from ._hooks import HookspecMarker, HookimplMarker, HookCaller, HookRelay diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index 42c9691b..ed3cb6e2 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -341,7 +341,7 @@ def varnames(func: object) -> tuple[tuple[str, ...], tuple[str, ...]]: return args, kwargs -class _HookRelay: +class HookRelay: """Hook holder object for performing 1:N hook calls where N is the number of registered plugins.""" @@ -356,6 +356,10 @@ def __getattr__(self, name: str) -> HookCaller: ... +# Historical name (pluggy<=1.2), kept for backward compatibility. +_HookRelay = HookRelay + + _CallHistory = List[Tuple[Mapping[str, object], Optional[Callable[[Any], None]]]] diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index a403417c..5861facd 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -16,13 +16,13 @@ from ._callers import _multicall from ._hooks import _HookImplFunction from ._hooks import _HookImplOpts -from ._hooks import _HookRelay from ._hooks import _HookSpecOpts from ._hooks import _Namespace from ._hooks import _Plugin from ._hooks import _SubsetHookCaller from ._hooks import HookCaller from ._hooks import HookImpl +from ._hooks import HookRelay from ._hooks import HookSpec from ._hooks import normalize_hookimpl_opts from ._result import Result @@ -97,7 +97,7 @@ def __init__(self, project_name: str) -> None: 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] = HookRelay() #: The tracing entry point. See :ref:`tracing`. self.trace: Final[_tracing.TagTracerSub] = _tracing.TagTracer().get( "pluginmanage" From 04e9a9b9c766ff597df3ea45eb7f9b5879acdebd Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 5 Aug 2023 08:45:48 +0300 Subject: [PATCH 7/7] Rename _Hook{Spec,Impl}Opts -> Hook{Spec,Impl}Opts, export from `pluggy` For typing purposes, refs #428. --- src/pluggy/__init__.py | 11 ++++++++++- src/pluggy/_hooks.py | 20 ++++++++++---------- src/pluggy/_manager.py | 12 ++++++------ 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/pluggy/__init__.py b/src/pluggy/__init__.py index ffd516c2..ae5a18fd 100644 --- a/src/pluggy/__init__.py +++ b/src/pluggy/__init__.py @@ -10,6 +10,8 @@ "PluginValidationError", "HookCaller", "HookCallError", + "HookSpecOpts", + "HookImplOpts", "HookRelay", "HookspecMarker", "HookimplMarker", @@ -18,4 +20,11 @@ from ._manager import PluginManager, PluginValidationError from ._result import HookCallError, Result -from ._hooks import HookspecMarker, HookimplMarker, HookCaller, HookRelay +from ._hooks import ( + HookspecMarker, + HookimplMarker, + HookCaller, + HookRelay, + HookSpecOpts, + HookImplOpts, +) diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index ed3cb6e2..09907a3b 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 5861facd..070ad83a 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -15,15 +15,15 @@ from . import _tracing from ._callers import _multicall from ._hooks import _HookImplFunction -from ._hooks import _HookImplOpts -from ._hooks import _HookSpecOpts from ._hooks import _Namespace from ._hooks import _Plugin from ._hooks import _SubsetHookCaller from ._hooks import HookCaller 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 @@ -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 | Non 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]: