Skip to content

Commit

Permalink
remove pydantic-specific logic and support hookspecs too
Browse files Browse the repository at this point in the history
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.7 → v0.6.8](astral-sh/ruff-pre-commit@v0.6.7...v0.6.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
  • Loading branch information
pirate committed Oct 1, 2024
1 parent aae3799 commit 90d9f80
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.6.7"
rev: "v0.6.8"
hooks:
- id: ruff
args: ["--fix"]
Expand Down
38 changes: 26 additions & 12 deletions src/pluggy/_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ def _warn_for_function(warning: Warning, function: Callable[..., object]) -> Non
)


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


class PluginValidationError(Exception):
"""Plugin failed validation.
Expand Down Expand Up @@ -182,23 +193,16 @@ def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None
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", {}):
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:
# AttributeError: '__signature__' attribute of 'Plugin' is class-only
# can happen for some special objects (e.g. proxies, pydantic, etc.)
# 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):
Expand Down Expand Up @@ -305,7 +309,17 @@ def parse_hookspec_opts(
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

method: object
try:
method = getattr(module_or_class, name)
except AttributeError:
# AttributeError: '__signature__' attribute of <m_or_c> is class-only
# 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
23 changes: 3 additions & 20 deletions testing/test_pluginmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import importlib.metadata
from typing import Any
from typing import Dict
from typing import List

import pytest
Expand Down Expand Up @@ -124,36 +123,20 @@ class A:
assert pm.register(A(), "somename")


def test_register_skips_properties(he_pm: PluginManager) -> None:
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
return None
self.property_was_executed = True # pragma: no cover
return None # pragma: no cover

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
Expand Down

0 comments on commit 90d9f80

Please sign in to comment.