Skip to content

Commit 1bac79a

Browse files
committed
remove pydantic-specific logic and support hookspecs too
1 parent aae3799 commit 1bac79a

File tree

2 files changed

+28
-31
lines changed

2 files changed

+28
-31
lines changed

src/pluggy/_manager.py

+25-12
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ def _warn_for_function(warning: Warning, function: Callable[..., object]) -> Non
4545
filename=func.__code__.co_filename,
4646
)
4747

48+
def _attr_is_property(obj: Any, name: str) -> bool:
49+
"""Check if a given attr is a @property on a module, class, or object"""
50+
if inspect.ismodule(obj):
51+
return False # modules can never have @property methods
52+
53+
base_class = obj if inspect.isclass(obj) else type(obj)
54+
if isinstance(getattr(base_class, name, None), property):
55+
return True
56+
return False
57+
4858

4959
class PluginValidationError(Exception):
5060
"""Plugin failed validation.
@@ -182,23 +192,16 @@ def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None
182192
options for items decorated with :class:`HookimplMarker`.
183193
"""
184194

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", {}):
195+
if _attr_is_property(plugin, name):
196+
# @property methods can have side effects, and are never hookimpls
194197
return None
195198

196199
method: object
197200
try:
198201
method = getattr(plugin, name)
199202
except AttributeError:
200-
# AttributeError: '__signature__' attribute of 'Plugin' is class-only
201-
# can happen for some special objects (e.g. proxies, pydantic, etc.)
203+
# AttributeError: '__signature__' attribute of 'plugin' is class-only
204+
# can happen if plugin is a proxy object wrapping a class/module
202205
method = getattr(type(plugin), name) # use class sig instead
203206

204207
if not inspect.isroutine(method):
@@ -305,7 +308,17 @@ def parse_hookspec_opts(
305308
customize how hook specifications are picked up. By default, returns the
306309
options for items decorated with :class:`HookspecMarker`.
307310
"""
308-
method = getattr(module_or_class, name)
311+
if _attr_is_property(module_or_class, name):
312+
# @property methods can have side effects, and are never hookspecs
313+
return None
314+
315+
method: object
316+
try:
317+
method = getattr(module_or_class, name)
318+
except AttributeError:
319+
# AttributeError: '__signature__' attribute of <m_or_c> is class-only
320+
# can happen if module_or_class is a proxy obj wrapping a class/module
321+
method = getattr(type(module_or_class), name) # use class sig instead
309322
opts: HookspecOpts | None = getattr(method, self.project_name + "_spec", None)
310323
return opts
311324

testing/test_pluginmanager.py

+3-19
Original file line numberDiff line numberDiff line change
@@ -124,36 +124,20 @@ class A:
124124
assert pm.register(A(), "somename")
125125

126126

127-
def test_register_skips_properties(he_pm: PluginManager) -> None:
127+
def test_register_ignores_properties(he_pm: PluginManager) -> None:
128128
class ClassWithProperties:
129129
property_was_executed: bool = False
130130

131131
@property
132132
def some_func(self):
133-
self.property_was_executed = True
134-
return None
133+
self.property_was_executed = True # pragma: no cover
134+
return None # pragma: no cover
135135

136136
test_plugin = ClassWithProperties()
137137
he_pm.register(test_plugin)
138138
assert not test_plugin.property_was_executed
139139

140140

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-
157141
def test_register_mismatch_method(he_pm: PluginManager) -> None:
158142
class hello:
159143
@hookimpl

0 commit comments

Comments
 (0)