Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid triggering @property methods on plugins when looking for hookimpls during registration #536

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
39 changes: 37 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: object, 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,20 @@
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):
Copy link
Member

Choose a reason for hiding this comment

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

i recently re-discovered inspect.getattr_static
i wonder if that one may be nice for a followup change

Copy link
Member

Choose a reason for hiding this comment

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

When I last looked at it, the main downside of getattr_static was that it was very slow. Though may this has improved (I think I saw mention of that).

# @property methods can have side effects, and are never hookimpls
return None

method: object
try:
method = getattr(plugin, name)
except AttributeError:
# AttributeError: '__signature__' attribute of 'plugin' is class-only
pirate marked this conversation as resolved.
Show resolved Hide resolved
# can be raised when trying to access some descriptor/proxied fields
# https://github.com/pytest-dev/pluggy/pull/536#discussion_r1786431032
return None

if not inspect.isroutine(method):
return None
try:
Expand Down Expand Up @@ -286,7 +310,18 @@
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

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
return None

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
method: object
try:
method = getattr(module_or_class, name)
except AttributeError:

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
# AttributeError: '__signature__' attribute of <m_or_c> is class-only
# can be raised when trying to access some descriptor/proxied fields
# https://github.com/pytest-dev/pluggy/pull/536#discussion_r1786431032
return None
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