Skip to content
37 changes: 35 additions & 2 deletions src/pluggy/_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@
)


def _attr_is_property(obj: Any, name: str) -> bool:
"""Check if a given attr is a @property on a module, class, or object"""
if inspect.ismodule(obj):
return False # modules can never have @property methods

base_class = obj if inspect.isclass(obj) else type(obj)
if isinstance(getattr(base_class, name, None), property):
return True
return False


Check warning on line 59 in src/pluggy/_manager.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_manager.py#L59

Added line #L59 was not covered by tests
class PluginValidationError(Exception):
"""Plugin failed validation.

Expand Down Expand Up @@ -181,7 +192,19 @@
customize how hook implementation are picked up. By default, returns the
options for items decorated with :class:`HookimplMarker`.
"""
method: object = getattr(plugin, name)

Check warning on line 195 in src/pluggy/_manager.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_manager.py#L195

Added line #L195 was not covered by tests
if _attr_is_property(plugin, name):
# @property methods can have side effects, and are never hookimpls
return None

method: object
try:
method = getattr(plugin, name)
except AttributeError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're able to cook a test for this case that would be nice.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment here: #536 (comment)

# AttributeError: '__signature__' attribute of 'plugin' is class-only
# can happen if plugin is a proxy object wrapping a class/module
method = getattr(type(plugin), name) # use class sig instead

if not inspect.isroutine(method):
return None
try:
Expand Down Expand Up @@ -286,7 +309,17 @@
customize how hook specifications are picked up. By default, returns the
options for items decorated with :class:`HookspecMarker`.
"""
method = getattr(module_or_class, name)
if _attr_is_property(module_or_class, name):
# @property methods can have side effects, and are never hookspecs
return None

Check warning on line 314 in src/pluggy/_manager.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_manager.py#L314

Added line #L314 was not covered by tests

method: object

Check warning on line 316 in src/pluggy/_manager.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_manager.py#L316

Added line #L316 was not covered by tests
try:
method = getattr(module_or_class, name)
except AttributeError:
# AttributeError: '__signature__' attribute of <m_or_c> is class-only

Check warning on line 320 in src/pluggy/_manager.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_manager.py#L320

Added line #L320 was not covered by tests
# can happen if module_or_class is a proxy obj wrapping a class/module
method = getattr(type(module_or_class), name) # use class sig instead
opts: HookspecOpts | None = getattr(method, self.project_name + "_spec", None)
return opts

Expand Down
17 changes: 17 additions & 0 deletions testing/test_pluginmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,23 @@ class A:
assert pm.register(A(), "somename")


def test_register_ignores_properties(he_pm: PluginManager) -> None:
class ClassWithProperties:
property_was_executed: bool = False

@property
def some_func(self):
self.property_was_executed = True # pragma: no cover
return None # pragma: no cover

# test registering it as a class
he_pm.register(ClassWithProperties)
# test registering it as an instance
test_plugin = ClassWithProperties()
he_pm.register(test_plugin)
assert not test_plugin.property_was_executed


def test_register_mismatch_method(he_pm: PluginManager) -> None:
class hello:
@hookimpl
Expand Down