From 2cb13c72f64d5efb60f3a5e4552880979171f4c1 Mon Sep 17 00:00:00 2001 From: Bill Huneke Date: Fri, 22 Sep 2023 23:51:53 -0400 Subject: [PATCH 1/2] add force_not_a_hook() and unit test --- src/pluggy/__init__.py | 3 ++- src/pluggy/_manager.py | 47 ++++++++++++++++++++++++++++++++++- testing/test_pluginmanager.py | 21 ++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/pluggy/__init__.py b/src/pluggy/__init__.py index 9d9e873b..6f03357d 100644 --- a/src/pluggy/__init__.py +++ b/src/pluggy/__init__.py @@ -18,9 +18,10 @@ "HookspecMarker", "HookimplMarker", "Result", + "force_not_a_hook", ] -from ._manager import PluginManager, PluginValidationError +from ._manager import PluginManager, PluginValidationError, force_not_a_hook from ._result import HookCallError, Result from ._hooks import ( HookspecMarker, diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 84717e6e..70b7f4cc 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -5,6 +5,7 @@ import types import warnings from typing import Any +from typing import TypeVar from typing import Callable from typing import cast from typing import Final @@ -72,6 +73,32 @@ def __dir__(self) -> list[str]: return sorted(dir(self._dist) + ["_dist", "project_name"]) +_T = TypeVar("_T") + + +_pluggy_hide_attr_name = "_pluggy_hide_mark" + + +def force_not_a_hook(obj: _T) -> _T: + """ + Use this to mark a function or method as *hidden* from discovery as a plugin hook. + + This is useful in rare cases. Use it where hooks are discovered by prefix name but some objects exist with + matching names which are _not_ to be considered as hook implementations. + + e.g. When using _pytest_, use this marker to mark a function that has a name starting with 'pytest_' but which + is not intended to be picked up as a hook implementation. + + >>> @force_not_a_hook + ... def pytest_some_function(arg1): + ... # For some reason, we needed to name this function with `pytest_` prefix, but we don't want it treated as a + ... # hook + """ + assert not hasattr(obj, _pluggy_hide_attr_name) + setattr(obj, _pluggy_hide_attr_name, True) + return obj + + class PluginManager: """Core class which manages registration of plugin objects and 1:N hook calling. @@ -148,7 +175,7 @@ def register(self, plugin: _Plugin, name: str | None = None) -> str | None: self._name2plugin[plugin_name] = plugin # register matching hook implementations of the plugin - for name in dir(plugin): + for name in self._find_plugin_attrs(plugin): hookimpl_opts = self.parse_hookimpl_opts(plugin, name) if hookimpl_opts is not None: normalize_hookimpl_opts(hookimpl_opts) @@ -165,6 +192,24 @@ def register(self, plugin: _Plugin, name: str | None = None) -> str | None: hook._add_hookimpl(hookimpl) return plugin_name + def _find_plugin_attrs(self, plugin: _Namespace) -> Iterable[str]: + """ + Override this method to customize the way we select the attribute names from an object to inspect as potential + hook implementations. + + The results from this method will run through `parse_hookimpl_opts` which may do additional filtering. + """ + for name in dir(plugin): # , method in inspect.getmembers(plugin): + try: + method = getattr(plugin, name, None) + pluggy_hide_mark = getattr(method, _pluggy_hide_attr_name, None) + except Exception: + # Ignore all kinds of exceptions. Pluggy has to be very exception-tolerant during plugin registration + continue + + if pluggy_hide_mark is not True: + yield 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. diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 81b86b65..3caeed4a 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -10,6 +10,7 @@ from pluggy import HookCallError from pluggy import HookimplMarker from pluggy import HookspecMarker +from pluggy import force_not_a_hook from pluggy import PluginManager from pluggy import PluginValidationError @@ -211,6 +212,26 @@ def he_method1(self, arg): assert len(hookcallers) == 1 +def test_register_force_not_hook(pm: PluginManager) -> None: + class Hooks: + @hookspec + def he_method1(self): + pass + + pm.add_hookspecs(Hooks) + + class Plugin: + @force_not_a_hook + @hookimpl + def he_method1(self): + return 1 + + pm.register(Plugin()) + hc = pm.hook + out = hc.he_method1() + assert out == [] + + def test_register_historic(pm: PluginManager) -> None: class Hooks: @hookspec(historic=True) From c28dff8bbbf72ba7a28aa5ab0d11e44b653605d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 23 Sep 2023 03:56:38 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pluggy/_manager.py | 2 +- testing/test_pluginmanager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 70b7f4cc..8eea0b41 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -5,13 +5,13 @@ import types import warnings from typing import Any -from typing import TypeVar from typing import Callable from typing import cast from typing import Final from typing import Iterable from typing import Mapping from typing import Sequence +from typing import TypeVar from . import _tracing from ._callers import _multicall diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 3caeed4a..3e28dc49 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -7,10 +7,10 @@ import pytest +from pluggy import force_not_a_hook from pluggy import HookCallError from pluggy import HookimplMarker from pluggy import HookspecMarker -from pluggy import force_not_a_hook from pluggy import PluginManager from pluggy import PluginValidationError