Skip to content

Commit

Permalink
remove pydantic-specific logic and support hookspecs too
Browse files Browse the repository at this point in the history
  • Loading branch information
pirate committed Oct 1, 2024
1 parent aae3799 commit 3865359
Showing 1 changed file with 25 additions and 12 deletions.
37 changes: 25 additions & 12 deletions src/pluggy/_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ def _warn_for_function(warning: Warning, function: Callable[..., object]) -> Non
filename=func.__code__.co_filename,
)

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 +192,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 +308,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

0 comments on commit 3865359

Please sign in to comment.