Skip to content

Commit 8a69479

Browse files
Add ProjectSpec unified project management API
- Add new ProjectSpec class that manages hook markers and plugin manager creation - ProjectSpec encapsulates HookspecMarker, HookimplMarker, and PluginManager creation - Support for custom PluginManager subclasses via plugin_manager_cls parameter - All components share the same project_name ensuring consistent behavior - Update marker classes and PluginManager to accept str or ProjectSpec instances - Use module references (_project.ProjectSpec) to avoid circular imports - Add comprehensive test suite covering all ProjectSpec functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 6123cc5 commit 8a69479

File tree

6 files changed

+353
-13
lines changed

6 files changed

+353
-13
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ It provides hook specification and implementation mechanisms through a plugin ma
2222
- `ruff format` - Format code with Ruff
2323
- `uv run mypy src/` - Type checking with mypy
2424
- `pre-commit run --all-files` - Run all pre-commit hooks
25+
- Use pre-commit to lint and fix code
2526

2627
### Documentation
2728
- `tox -e docs` - Build documentation

src/pluggy/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"HookRelay",
1212
"HookspecMarker",
1313
"HookimplMarker",
14+
"ProjectSpec",
1415
"Result",
1516
"PluggyWarning",
1617
"PluggyTeardownRaisedWarning",
@@ -25,6 +26,7 @@
2526
from ._hooks import HookspecOpts
2627
from ._manager import PluginManager
2728
from ._manager import PluginValidationError
29+
from ._project import ProjectSpec
2830
from ._result import HookCallError
2931
from ._result import Result
3032
from ._warnings import PluggyTeardownRaisedWarning

src/pluggy/_hooks.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from typing import Union
2424
import warnings
2525

26+
from . import _project
2627
from ._result import Result
2728

2829

@@ -155,15 +156,24 @@ def __repr__(self) -> str:
155156
class HookspecMarker:
156157
"""Decorator for marking functions as hook specifications.
157158
158-
Instantiate it with a project_name to get a decorator.
159+
Instantiate it with a project_name or ProjectSpec to get a decorator.
159160
Calling :meth:`PluginManager.add_hookspecs` later will discover all marked
160161
functions if the :class:`PluginManager` uses the same project name.
161162
"""
162163

163-
__slots__ = ("project_name",)
164+
__slots__ = ("_project_spec",)
164165

165-
def __init__(self, project_name: str) -> None:
166-
self.project_name: Final = project_name
166+
def __init__(self, project_name_or_spec: str | _project.ProjectSpec) -> None:
167+
self._project_spec: Final = (
168+
_project.ProjectSpec(project_name_or_spec)
169+
if isinstance(project_name_or_spec, str)
170+
else project_name_or_spec
171+
)
172+
173+
@property
174+
def project_name(self) -> str:
175+
"""The project name from the associated ProjectSpec."""
176+
return self._project_spec.project_name
167177

168178
@overload
169179
def __call__(
@@ -242,15 +252,24 @@ def setattr_hookspec_opts(func: _F) -> _F:
242252
class HookimplMarker:
243253
"""Decorator for marking functions as hook implementations.
244254
245-
Instantiate it with a ``project_name`` to get a decorator.
255+
Instantiate it with a ``project_name`` or ProjectSpec to get a decorator.
246256
Calling :meth:`PluginManager.register` later will discover all marked
247257
functions if the :class:`PluginManager` uses the same project name.
248258
"""
249259

250-
__slots__ = ("project_name",)
260+
__slots__ = ("_project_spec",)
251261

252-
def __init__(self, project_name: str) -> None:
253-
self.project_name: Final = project_name
262+
def __init__(self, project_name_or_spec: str | _project.ProjectSpec) -> None:
263+
self._project_spec: Final = (
264+
_project.ProjectSpec(project_name_or_spec)
265+
if isinstance(project_name_or_spec, str)
266+
else project_name_or_spec
267+
)
268+
269+
@property
270+
def project_name(self) -> str:
271+
"""The project name from the associated ProjectSpec."""
272+
return self._project_spec.project_name
254273

255274
@overload
256275
def __call__(

src/pluggy/_manager.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import TYPE_CHECKING
1313
import warnings
1414

15+
from . import _project
1516
from . import _tracing
1617
from ._callers import _multicall
1718
from ._hooks import _HookImplFunction
@@ -91,13 +92,17 @@ class PluginManager:
9192
For debugging purposes you can call :meth:`PluginManager.enable_tracing`
9293
which will subsequently send debug information to the trace helper.
9394
94-
:param project_name:
95-
The short project name. Prefer snake case. Make sure it's unique!
95+
:param project_name_or_spec:
96+
The short project name (string) or a ProjectSpec instance.
9697
"""
9798

98-
def __init__(self, project_name: str) -> None:
99-
#: The project name.
100-
self.project_name: Final = project_name
99+
def __init__(self, project_name_or_spec: str | _project.ProjectSpec) -> None:
100+
self._project_spec: Final = (
101+
_project.ProjectSpec(project_name_or_spec)
102+
if isinstance(project_name_or_spec, str)
103+
else project_name_or_spec
104+
)
105+
101106
self._name2plugin: Final[dict[str, _Plugin]] = {}
102107
self._plugin_distinfo: Final[list[tuple[_Plugin, DistFacade]]] = []
103108
#: The "hook relay", used to call a hook on all registered plugins.
@@ -109,6 +114,11 @@ def __init__(self, project_name: str) -> None:
109114
)
110115
self._inner_hookexec = _multicall
111116

117+
@property
118+
def project_name(self) -> str:
119+
"""The project name from the associated ProjectSpec."""
120+
return self._project_spec.project_name
121+
112122
def _hookexec(
113123
self,
114124
hook_name: str,

src/pluggy/_project.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
Project configuration and management for pluggy projects.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from typing import Final
8+
from typing import TYPE_CHECKING
9+
10+
11+
if TYPE_CHECKING:
12+
from ._manager import PluginManager
13+
14+
15+
class ProjectSpec:
16+
"""Manages hook markers and plugin manager creation for a pluggy project.
17+
18+
This class provides a unified interface for creating and managing the core
19+
components of a pluggy project: HookspecMarker, HookimplMarker, and PluginManager.
20+
21+
All components share the same project_name, ensuring consistent behavior
22+
across hook specifications, implementations, and plugin management.
23+
24+
:param project_name:
25+
The short project name. Prefer snake case. Make sure it's unique!
26+
:param plugin_manager_cls:
27+
Custom PluginManager subclass to use (defaults to PluginManager).
28+
"""
29+
30+
def __init__(
31+
self, project_name: str, plugin_manager_cls: type[PluginManager] | None = None
32+
) -> None:
33+
from ._hooks import HookimplMarker
34+
from ._hooks import HookspecMarker
35+
from ._manager import PluginManager as DefaultPluginManager
36+
37+
#: The project name used across all components.
38+
self.project_name: Final = project_name
39+
#: The PluginManager class for creating new instances.
40+
self._plugin_manager_cls: Final = plugin_manager_cls or DefaultPluginManager
41+
42+
# Create marker instances (these are stateless decorators, safe to share)
43+
#: Hook specification marker for decorating hook specification functions.
44+
self.hookspec: Final = HookspecMarker(self)
45+
#: Hook implementation marker for decorating hook implementation functions.
46+
self.hookimpl: Final = HookimplMarker(self)
47+
48+
def create_plugin_manager(self) -> PluginManager:
49+
"""Create a new PluginManager instance for this project.
50+
51+
Each call returns a fresh, independent PluginManager instance
52+
configured with this project's name and using the specified
53+
PluginManager class (if provided during initialization).
54+
55+
:returns: New PluginManager instance.
56+
"""
57+
return self._plugin_manager_cls(self)
58+
59+
def __repr__(self) -> str:
60+
return f"ProjectSpec(project_name={self.project_name!r})"

0 commit comments

Comments
 (0)