Skip to content

Commit abb2f64

Browse files
Centralize hook configuration extraction in ProjectSpec
Move hook configuration extraction methods from HookspecMarker and HookimplMarker to ProjectSpec to provide a unified API for accessing hook configurations. Changes: - Add ProjectSpec.get_hookspec_config() and get_hookimpl_config() methods - Remove HookspecMarker._get_hookconfig() and HookimplMarker.get_hookconfig() - Update PluginManager to use ProjectSpec methods for configuration extraction - Use getattr with default None instead of exception handling for better performance - Update tests to use the new ProjectSpec-based API 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent d509871 commit abb2f64

File tree

6 files changed

+60
-55
lines changed

6 files changed

+60
-55
lines changed

src/pluggy/_hooks.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -296,17 +296,6 @@ def setattr_hookspec_opts(func: _F) -> _F:
296296
else:
297297
return setattr_hookspec_opts
298298

299-
def _get_hookconfig(self, func: Callable[..., object]) -> HookspecConfiguration:
300-
"""Extract hook specification configuration from a decorated function.
301-
302-
:param func: A function decorated with this HookspecMarker
303-
:return: HookspecConfiguration object containing the hook specification options
304-
:raises AttributeError: If the function is not decorated with this marker
305-
"""
306-
attr_name = self.project_name + "_spec"
307-
config: HookspecConfiguration = getattr(func, attr_name)
308-
return config
309-
310299

311300
@final
312301
class HookimplMarker:
@@ -433,17 +422,6 @@ def setattr_hookimpl_opts(func: _F) -> _F:
433422
else:
434423
return setattr_hookimpl_opts(function)
435424

436-
def get_hookconfig(self, func: Callable[..., object]) -> HookimplConfiguration:
437-
"""Extract hook implementation configuration from a decorated function.
438-
439-
:param func: A function decorated with this HookimplMarker
440-
:return: HookimplConfiguration object containing the hook implementation options
441-
:raises AttributeError: If the function is not decorated with this marker
442-
"""
443-
attr_name = self.project_name + "_impl"
444-
config: HookimplConfiguration = getattr(func, attr_name)
445-
return config
446-
447425

448426
_PYPY = hasattr(sys, "pypy_version_info")
449427

src/pluggy/_manager.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,10 @@ def _parse_hookimpl(
197197
if not inspect.isroutine(method):
198198
return None
199199

200-
try:
201-
# Get hook implementation configuration directly
202-
impl_attr = getattr(method, self.project_name + "_impl", None)
203-
except Exception: # pragma: no cover
204-
impl_attr = None
205-
206-
if isinstance(impl_attr, HookimplConfiguration):
207-
return impl_attr
200+
# Get hook implementation configuration using ProjectSpec
201+
impl_config = self._project_spec.get_hookimpl_config(method)
202+
if impl_config is not None:
203+
return impl_config
208204

209205
# Fall back to legacy parse_hookimpl_opts for compatibility
210206
# (e.g. pytest override)
@@ -231,14 +227,10 @@ def _parse_hookspec(
231227
if not inspect.isroutine(method):
232228
return None
233229

234-
try:
235-
# Get hook specification configuration directly
236-
spec_attr = getattr(method, self.project_name + "_spec", None)
237-
except Exception: # pragma: no cover
238-
spec_attr = None
239-
240-
if isinstance(spec_attr, HookspecConfiguration):
241-
return spec_attr
230+
# Get hook specification configuration using ProjectSpec
231+
spec_config = self._project_spec.get_hookspec_config(method)
232+
if spec_config is not None:
233+
return spec_config
242234

243235
# Fall back to legacy parse_hookspec_opts for compatibility
244236
legacy_opts = self.parse_hookspec_opts(module_or_class, name)

src/pluggy/_project.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
from __future__ import annotations
66

7+
from typing import Callable
78
from typing import Final
89
from typing import TYPE_CHECKING
910

1011

1112
if TYPE_CHECKING:
13+
from ._hooks import HookimplConfiguration
14+
from ._hooks import HookspecConfiguration
1215
from ._manager import PluginManager
1316

1417

@@ -56,5 +59,31 @@ def create_plugin_manager(self) -> PluginManager:
5659
"""
5760
return self._plugin_manager_cls(self)
5861

62+
def get_hookspec_config(
63+
self, func: Callable[..., object]
64+
) -> HookspecConfiguration | None:
65+
"""Extract hook specification configuration from a decorated function.
66+
67+
:param func: A function that may be decorated with this project's
68+
hookspec marker
69+
:return: HookspecConfiguration object if found, None if not decorated
70+
with this project's hookspec marker
71+
"""
72+
attr_name = self.project_name + "_spec"
73+
return getattr(func, attr_name, None)
74+
75+
def get_hookimpl_config(
76+
self, func: Callable[..., object]
77+
) -> HookimplConfiguration | None:
78+
"""Extract hook implementation configuration from a decorated function.
79+
80+
:param func: A function that may be decorated with this project's
81+
hookimpl marker
82+
:return: HookimplConfiguration object if found, None if not decorated
83+
with this project's hookimpl marker
84+
"""
85+
attr_name = self.project_name + "_impl"
86+
return getattr(func, attr_name, None)
87+
5988
def __repr__(self) -> str:
6089
return f"ProjectSpec(project_name={self.project_name!r})"

testing/test_hookcaller.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55

66
import pytest
77

8-
from pluggy import HookimplMarker
98
from pluggy import HookspecConfiguration
10-
from pluggy import HookspecMarker
119
from pluggy import PluginManager
1210
from pluggy import PluginValidationError
11+
from pluggy import ProjectSpec
1312
from pluggy._hooks import HookCaller
1413
from pluggy._hooks import HookImpl
1514

1615

17-
hookspec = HookspecMarker("example")
18-
hookimpl = HookimplMarker("example")
16+
project_spec = ProjectSpec("example")
17+
hookspec = project_spec.hookspec
18+
hookimpl = project_spec.hookimpl
1919

2020

2121
@pytest.fixture
@@ -50,12 +50,14 @@ def wrap(func: FuncT) -> FuncT:
5050
hookwrapper=hookwrapper,
5151
wrapper=wrapper,
5252
)(func)
53+
config = project_spec.get_hookimpl_config(func)
54+
assert config is not None # Test functions should be decorated
5355
self.hc._add_hookimpl(
5456
HookImpl(
5557
None,
5658
"<temp>",
5759
func,
58-
hookimpl.get_hookconfig(func),
60+
config,
5961
),
6062
)
6163
return func
@@ -549,15 +551,16 @@ def test_hookspec_configuration() -> None:
549551

550552

551553
def test_hookspec_marker_config_extraction() -> None:
552-
"""Test that HookspecMarker creates and extracts HookspecConfiguration correctly."""
553-
marker = HookspecMarker("test")
554+
"""Test that ProjectSpec can extract HookspecConfiguration correctly."""
555+
test_project_spec = ProjectSpec("test")
556+
marker = test_project_spec.hookspec
554557

555558
@marker(firstresult=True, historic=False)
556559
def test_hook(arg1: str) -> str:
557560
return arg1
558561

559-
# Test private config extraction method
560-
config = marker._get_hookconfig(test_hook)
562+
# Test config extraction method via ProjectSpec
563+
config = test_project_spec.get_hookspec_config(test_hook)
561564
assert isinstance(config, HookspecConfiguration)
562565
assert config.firstresult is True
563566
assert config.historic is False

testing/test_multicall.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
import pytest
77

88
from pluggy import HookCallError
9-
from pluggy import HookimplMarker
10-
from pluggy import HookspecMarker
9+
from pluggy import ProjectSpec
1110
from pluggy._callers import _multicall
1211
from pluggy._hooks import HookImpl
1312

1413

15-
hookspec = HookspecMarker("example")
16-
hookimpl = HookimplMarker("example")
14+
project_spec = ProjectSpec("example")
15+
hookspec = project_spec.hookspec
16+
hookimpl = project_spec.hookimpl
1717

1818

1919
def MC(
@@ -24,7 +24,9 @@ def MC(
2424
caller = _multicall
2525
hookfuncs = []
2626
for method in methods:
27-
f = HookImpl(None, "<temp>", method, hookimpl.get_hookconfig(method))
27+
config = project_spec.get_hookimpl_config(method)
28+
assert config is not None # Test functions should be decorated
29+
f = HookImpl(None, "<temp>", method, config)
2830
hookfuncs.append(f)
2931
return caller("foo", hookfuncs, kwargs, firstresult)
3032

testing/test_project_spec.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def test_hook_impl() -> None:
143143

144144

145145
def test_project_spec_with_get_hookconfig() -> None:
146-
"""Test that ProjectSpec works with HookimplMarker.get_hookconfig()."""
146+
"""Test that ProjectSpec works with get_hookimpl_config()."""
147147
project = ProjectSpec("testproject")
148148
hookimpl = project.hookimpl
149149

@@ -152,9 +152,10 @@ def test_project_spec_with_get_hookconfig() -> None:
152152
def my_hook_impl() -> None:
153153
pass
154154

155-
# Get the configuration
156-
config = hookimpl.get_hookconfig(my_hook_impl)
155+
# Get the configuration using ProjectSpec
156+
config = project.get_hookimpl_config(my_hook_impl)
157157

158+
assert config is not None
158159
assert config.tryfirst is True
159160
assert config.optionalhook is True
160161
assert config.wrapper is False

0 commit comments

Comments
 (0)