Skip to content

Commit 24895d0

Browse files
committed
Avoid triggering property methods when inspecting plugin attribute signatures
1 parent b2cf1ff commit 24895d0

File tree

2 files changed

+45
-0
lines changed

2 files changed

+45
-0
lines changed

src/pluggy/_manager.py

+14
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,20 @@ def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None
181181
customize how hook implementation are picked up. By default, returns the
182182
options for items decorated with :class:`HookimplMarker`.
183183
"""
184+
185+
# IMPORTANT: @property methods can have side effects, and are never hookimpl
186+
# if attr is a property, skip it in advance
187+
plugin_class = plugin if inspect.isclass(plugin) else type(plugin)
188+
if isinstance(getattr(plugin_class, name, None), property):
189+
return None
190+
191+
# pydantic model fields are like attrs and also can never be hookimpls
192+
plugin_is_pydantic_obj = hasattr(plugin, "__pydantic_core_schema__")
193+
if plugin_is_pydantic_obj and name in getattr(plugin, "model_fields", {}):
194+
# pydantic models mess with the class and attr __signature__
195+
# so inspect.isroutine(...) throws exceptions and cant be used
196+
return None
197+
184198
method: object = getattr(plugin, name)
185199
if not inspect.isroutine(method):
186200
return None

testing/test_pluginmanager.py

+31
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import importlib.metadata
66
from typing import Any
7+
from typing import Dict
78
from typing import List
89

910
import pytest
@@ -123,6 +124,36 @@ class A:
123124
assert pm.register(A(), "somename")
124125

125126

127+
def test_register_skips_properties(he_pm: PluginManager) -> None:
128+
class ClassWithProperties:
129+
property_was_executed: bool = False
130+
131+
@property
132+
def some_func(self):
133+
self.property_was_executed = True
134+
return None
135+
136+
test_plugin = ClassWithProperties()
137+
he_pm.register(test_plugin)
138+
assert not test_plugin.property_was_executed
139+
140+
141+
def test_register_skips_pydantic_fields(he_pm: PluginManager) -> None:
142+
class PydanticModelClass:
143+
# stub to make object look like a pydantic model
144+
model_fields: Dict[str, bool] = {"some_attr": True}
145+
146+
def __pydantic_core_schema__(self): ...
147+
148+
@hookimpl
149+
def some_attr(self): ...
150+
151+
test_plugin = PydanticModelClass()
152+
he_pm.register(test_plugin)
153+
with pytest.raises(AttributeError):
154+
he_pm.hook.some_attr.get_hookimpls()
155+
156+
126157
def test_register_mismatch_method(he_pm: PluginManager) -> None:
127158
class hello:
128159
@hookimpl

0 commit comments

Comments
 (0)