From 24895d0ddc6154f02e711daa6dd480f40546464c Mon Sep 17 00:00:00 2001 From: Nick Sweeting Date: Thu, 26 Sep 2024 20:39:13 -0700 Subject: [PATCH] Avoid triggering property methods when inspecting plugin attribute signatures --- src/pluggy/_manager.py | 14 ++++++++++++++ testing/test_pluginmanager.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index d778334b..a52044f0 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -181,6 +181,20 @@ def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None customize how hook implementation are picked up. By default, returns the options for items decorated with :class:`HookimplMarker`. """ + + # IMPORTANT: @property methods can have side effects, and are never hookimpl + # if attr is a property, skip it in advance + plugin_class = plugin if inspect.isclass(plugin) else type(plugin) + if isinstance(getattr(plugin_class, name, None), property): + return None + + # pydantic model fields are like attrs and also can never be hookimpls + plugin_is_pydantic_obj = hasattr(plugin, "__pydantic_core_schema__") + if plugin_is_pydantic_obj and name in getattr(plugin, "model_fields", {}): + # pydantic models mess with the class and attr __signature__ + # so inspect.isroutine(...) throws exceptions and cant be used + return None + method: object = getattr(plugin, name) if not inspect.isroutine(method): return None diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index c4ce08f3..67ea6789 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -4,6 +4,7 @@ import importlib.metadata from typing import Any +from typing import Dict from typing import List import pytest @@ -123,6 +124,36 @@ class A: assert pm.register(A(), "somename") +def test_register_skips_properties(he_pm: PluginManager) -> None: + class ClassWithProperties: + property_was_executed: bool = False + + @property + def some_func(self): + self.property_was_executed = True + return None + + test_plugin = ClassWithProperties() + he_pm.register(test_plugin) + assert not test_plugin.property_was_executed + + +def test_register_skips_pydantic_fields(he_pm: PluginManager) -> None: + class PydanticModelClass: + # stub to make object look like a pydantic model + model_fields: Dict[str, bool] = {"some_attr": True} + + def __pydantic_core_schema__(self): ... + + @hookimpl + def some_attr(self): ... + + test_plugin = PydanticModelClass() + he_pm.register(test_plugin) + with pytest.raises(AttributeError): + he_pm.hook.some_attr.get_hookimpls() + + def test_register_mismatch_method(he_pm: PluginManager) -> None: class hello: @hookimpl