pluginkit 0.2.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pluginkit-0.2.0 → pluginkit-0.3.1}/PKG-INFO +15 -14
- {pluginkit-0.2.0 → pluginkit-0.3.1}/README.md +12 -9
- {pluginkit-0.2.0 → pluginkit-0.3.1}/pyproject.toml +6 -8
- pluginkit-0.3.1/src/pluginkit/__init__.py +81 -0
- {pluginkit-0.2.0 → pluginkit-0.3.1}/src/pluginkit/aio.py +57 -9
- {pluginkit-0.2.0 → pluginkit-0.3.1}/src/pluginkit/manager.py +138 -24
- pluginkit-0.3.1/src/pluginkit/markers.py +162 -0
- pluginkit-0.2.0/src/pluginkit/__init__.py +0 -38
- pluginkit-0.2.0/src/pluginkit/markers.py +0 -117
- {pluginkit-0.2.0 → pluginkit-0.3.1}/src/pluginkit/exceptions.py +0 -0
- {pluginkit-0.2.0 → pluginkit-0.3.1}/src/pluginkit/py.typed +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pluginkit
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: A
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: A strictly-typed, generics-first plugin framework for Python 3.13: hooks with derived return types
|
|
5
5
|
Keywords: plugins,hooks,protocol,entry-points
|
|
6
6
|
Author: Morten Hansen
|
|
7
7
|
Author-email: Morten Hansen <morten@winterop.com>
|
|
@@ -9,12 +9,10 @@ License: MIT
|
|
|
9
9
|
Classifier: Development Status :: 4 - Beta
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
13
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
14
|
Classifier: Typing :: Typed
|
|
17
|
-
Requires-Python: >=3.
|
|
15
|
+
Requires-Python: >=3.13
|
|
18
16
|
Project-URL: Homepage, https://github.com/winterop-com/pluginkit
|
|
19
17
|
Project-URL: Repository, https://github.com/winterop-com/pluginkit
|
|
20
18
|
Project-URL: Issues, https://github.com/winterop-com/pluginkit/issues
|
|
@@ -29,21 +27,22 @@ Description-Content-Type: text/markdown
|
|
|
29
27
|
[](https://winterop-com.github.io/pluginkit/)
|
|
30
28
|
[](LICENSE)
|
|
31
29
|
|
|
32
|
-
A small, **
|
|
33
|
-
specifications, let plugins implement them,
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
A small, **strictly-typed**, generics-first plugin framework for **Python 3.13+**.
|
|
31
|
+
Declare hook specifications, let plugins implement them, discover plugins via entry
|
|
32
|
+
points - and, unlike untyped hook systems, get the **right return type for every
|
|
33
|
+
call**, derived from the spec and checked by your type checker.
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
`
|
|
39
|
-
|
|
35
|
+
`pm.caller(spec)` hands back a caller whose result type matches the dispatch mode -
|
|
36
|
+
`list[R]` for collecting, `R | None` for firstresult, `R` for pipeline - with no
|
|
37
|
+
hand-annotations and no drift. Zero runtime dependencies, a `py.typed` marker, and a
|
|
38
|
+
few readable files.
|
|
40
39
|
|
|
41
40
|
```bash
|
|
42
41
|
pip install pluginkit # or: uv add pluginkit
|
|
43
42
|
```
|
|
44
43
|
|
|
45
44
|
```python
|
|
46
|
-
from pluginkit import
|
|
45
|
+
from pluginkit import HookimplMarker, HookspecMarker, PluginManager
|
|
47
46
|
|
|
48
47
|
hookspec = HookspecMarker("greeter")
|
|
49
48
|
hookimpl = HookimplMarker("greeter")
|
|
@@ -65,7 +64,9 @@ class Casual:
|
|
|
65
64
|
pm = PluginManager("greeter")
|
|
66
65
|
pm.add_hookspecs(Specs)
|
|
67
66
|
pm.register(Casual(), name="casual")
|
|
68
|
-
|
|
67
|
+
|
|
68
|
+
greetings = pm.caller(Specs.greeting)(name="Ada") # typed list[str] - derived, not asserted
|
|
69
|
+
print(greetings) # ['hey Ada!']
|
|
69
70
|
```
|
|
70
71
|
|
|
71
72
|
## What it supports
|
|
@@ -6,21 +6,22 @@
|
|
|
6
6
|
[](https://winterop-com.github.io/pluginkit/)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
A small, **
|
|
10
|
-
specifications, let plugins implement them,
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
A small, **strictly-typed**, generics-first plugin framework for **Python 3.13+**.
|
|
10
|
+
Declare hook specifications, let plugins implement them, discover plugins via entry
|
|
11
|
+
points - and, unlike untyped hook systems, get the **right return type for every
|
|
12
|
+
call**, derived from the spec and checked by your type checker.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
`
|
|
16
|
-
|
|
14
|
+
`pm.caller(spec)` hands back a caller whose result type matches the dispatch mode -
|
|
15
|
+
`list[R]` for collecting, `R | None` for firstresult, `R` for pipeline - with no
|
|
16
|
+
hand-annotations and no drift. Zero runtime dependencies, a `py.typed` marker, and a
|
|
17
|
+
few readable files.
|
|
17
18
|
|
|
18
19
|
```bash
|
|
19
20
|
pip install pluginkit # or: uv add pluginkit
|
|
20
21
|
```
|
|
21
22
|
|
|
22
23
|
```python
|
|
23
|
-
from pluginkit import
|
|
24
|
+
from pluginkit import HookimplMarker, HookspecMarker, PluginManager
|
|
24
25
|
|
|
25
26
|
hookspec = HookspecMarker("greeter")
|
|
26
27
|
hookimpl = HookimplMarker("greeter")
|
|
@@ -42,7 +43,9 @@ class Casual:
|
|
|
42
43
|
pm = PluginManager("greeter")
|
|
43
44
|
pm.add_hookspecs(Specs)
|
|
44
45
|
pm.register(Casual(), name="casual")
|
|
45
|
-
|
|
46
|
+
|
|
47
|
+
greetings = pm.caller(Specs.greeting)(name="Ada") # typed list[str] - derived, not asserted
|
|
48
|
+
print(greetings) # ['hey Ada!']
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
## What it supports
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pluginkit"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "A
|
|
3
|
+
version = "0.3.1"
|
|
4
|
+
description = "A strictly-typed, generics-first plugin framework for Python 3.13: hooks with derived return types"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Morten Hansen", email = "morten@winterop.com" }]
|
|
7
7
|
license = { text = "MIT" }
|
|
8
|
-
requires-python = ">=3.
|
|
8
|
+
requires-python = ">=3.13"
|
|
9
9
|
keywords = ["plugins", "hooks", "protocol", "entry-points"]
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Development Status :: 4 - Beta",
|
|
12
12
|
"Intended Audience :: Developers",
|
|
13
13
|
"License :: OSI Approved :: MIT License",
|
|
14
|
-
"Programming Language :: Python :: 3.11",
|
|
15
|
-
"Programming Language :: Python :: 3.12",
|
|
16
14
|
"Programming Language :: Python :: 3.13",
|
|
17
15
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
16
|
"Typing :: Typed",
|
|
@@ -56,7 +54,7 @@ pluginkit-tour = { workspace = true }
|
|
|
56
54
|
smoothie-extra = { workspace = true }
|
|
57
55
|
|
|
58
56
|
[tool.ruff]
|
|
59
|
-
target-version = "
|
|
57
|
+
target-version = "py313"
|
|
60
58
|
line-length = 120
|
|
61
59
|
|
|
62
60
|
[tool.ruff.lint]
|
|
@@ -85,7 +83,7 @@ pythonpath = ["examples/recipes", "examples/integrations"]
|
|
|
85
83
|
norecursedirs = [".git", ".venv", "__pycache__"]
|
|
86
84
|
|
|
87
85
|
[tool.mypy]
|
|
88
|
-
python_version = "3.
|
|
86
|
+
python_version = "3.13"
|
|
89
87
|
warn_return_any = true
|
|
90
88
|
warn_unused_configs = true
|
|
91
89
|
disallow_untyped_defs = true
|
|
@@ -110,7 +108,7 @@ disable_error_code = ["empty-body"]
|
|
|
110
108
|
include = ["src", "tests", "examples/recipes", "examples/integrations", "examples/tour/src"]
|
|
111
109
|
extraPaths = ["examples/recipes", "examples/integrations", "examples/tour/src"]
|
|
112
110
|
exclude = ["**/.venv"]
|
|
113
|
-
pythonVersion = "3.
|
|
111
|
+
pythonVersion = "3.13"
|
|
114
112
|
typeCheckingMode = "strict"
|
|
115
113
|
useLibraryCodeForTypes = true
|
|
116
114
|
reportPrivateUsage = false
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""pluginkit: a small, strictly-typed, generics-first plugin framework for Python 3.13+.
|
|
2
|
+
|
|
3
|
+
Unlike untyped hook systems, pluginkit derives a hook call's return type from its
|
|
4
|
+
spec: ``pm.caller(spec)`` hands back a caller whose result is ``list[R]``
|
|
5
|
+
(collecting), ``R | None`` (firstresult), or ``R`` (pipeline) - checked, not asserted.
|
|
6
|
+
|
|
7
|
+
Public API:
|
|
8
|
+
|
|
9
|
+
- :class:`HookspecMarker` / :class:`HookimplMarker` - decorators that declare hook
|
|
10
|
+
specifications and implementations. ``@hookspec`` brands the spec by dispatch mode.
|
|
11
|
+
- :class:`HookspecOpts` / :class:`HookimplOpts` - the option records the markers stamp.
|
|
12
|
+
- :class:`PluginManager` - registers plugins and dispatches hook calls; ``caller(spec)``
|
|
13
|
+
returns a typed caller.
|
|
14
|
+
- :class:`CollectingSpec` / :class:`FirstResultSpec` / :class:`PipelineSpec` - branded
|
|
15
|
+
spec types, and :class:`CollectingCaller` / :class:`FirstResultCaller` /
|
|
16
|
+
:class:`PipelineCaller` (and the ``Async*`` variants) - the typed callers.
|
|
17
|
+
- :class:`HookRelay` / :class:`HookCaller` / :class:`HookImpl` - the dispatch internals.
|
|
18
|
+
- :class:`PluginValidationError` - raised when a plugin is invalid.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
22
|
+
|
|
23
|
+
from pluginkit.aio import (
|
|
24
|
+
AsyncCollectingCaller,
|
|
25
|
+
AsyncFirstResultCaller,
|
|
26
|
+
AsyncHookCaller,
|
|
27
|
+
AsyncPipelineCaller,
|
|
28
|
+
AsyncPluginManager,
|
|
29
|
+
)
|
|
30
|
+
from pluginkit.exceptions import PluginValidationError
|
|
31
|
+
from pluginkit.manager import (
|
|
32
|
+
CollectingCaller,
|
|
33
|
+
FirstResultCaller,
|
|
34
|
+
HistoricCaller,
|
|
35
|
+
HookCaller,
|
|
36
|
+
HookImpl,
|
|
37
|
+
HookRelay,
|
|
38
|
+
PipelineCaller,
|
|
39
|
+
PluginManager,
|
|
40
|
+
)
|
|
41
|
+
from pluginkit.markers import (
|
|
42
|
+
CollectingSpec,
|
|
43
|
+
FirstResultSpec,
|
|
44
|
+
HistoricSpec,
|
|
45
|
+
HookimplMarker,
|
|
46
|
+
HookimplOpts,
|
|
47
|
+
HookspecMarker,
|
|
48
|
+
HookspecOpts,
|
|
49
|
+
PipelineSpec,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
__version__ = version("pluginkit")
|
|
54
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree without an install
|
|
55
|
+
__version__ = "0.0.0+unknown"
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"AsyncCollectingCaller",
|
|
59
|
+
"AsyncFirstResultCaller",
|
|
60
|
+
"AsyncHookCaller",
|
|
61
|
+
"AsyncPipelineCaller",
|
|
62
|
+
"AsyncPluginManager",
|
|
63
|
+
"CollectingCaller",
|
|
64
|
+
"CollectingSpec",
|
|
65
|
+
"FirstResultCaller",
|
|
66
|
+
"FirstResultSpec",
|
|
67
|
+
"HistoricCaller",
|
|
68
|
+
"HistoricSpec",
|
|
69
|
+
"HookCaller",
|
|
70
|
+
"HookImpl",
|
|
71
|
+
"HookRelay",
|
|
72
|
+
"HookimplMarker",
|
|
73
|
+
"HookimplOpts",
|
|
74
|
+
"HookspecMarker",
|
|
75
|
+
"HookspecOpts",
|
|
76
|
+
"PipelineCaller",
|
|
77
|
+
"PipelineSpec",
|
|
78
|
+
"PluginManager",
|
|
79
|
+
"PluginValidationError",
|
|
80
|
+
"__version__",
|
|
81
|
+
]
|
|
@@ -15,10 +15,15 @@ a wrapper must transform the result.
|
|
|
15
15
|
import inspect
|
|
16
16
|
from collections.abc import Callable
|
|
17
17
|
from types import AsyncGeneratorType
|
|
18
|
-
from typing import Any
|
|
18
|
+
from typing import Any, overload
|
|
19
19
|
|
|
20
20
|
from pluginkit.manager import _UNSET, HookCaller, HookImpl, PluginManager
|
|
21
|
-
from pluginkit.markers import
|
|
21
|
+
from pluginkit.markers import (
|
|
22
|
+
CollectingSpec,
|
|
23
|
+
FirstResultSpec,
|
|
24
|
+
HookspecOpts,
|
|
25
|
+
PipelineSpec,
|
|
26
|
+
)
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
class AsyncHookCaller(HookCaller):
|
|
@@ -28,16 +33,17 @@ class AsyncHookCaller(HookCaller):
|
|
|
28
33
|
"""Await the hook: a list, a single value (firstresult), or the threaded value (pipeline)."""
|
|
29
34
|
if self.spec.historic:
|
|
30
35
|
raise TypeError(f"historic hook {self.name!r} must be called via call_historic()")
|
|
31
|
-
self.check_arguments(kwargs)
|
|
36
|
+
kwargs = self.check_arguments(kwargs)
|
|
32
37
|
return await self._execute_async(kwargs, self._nonwrappers)
|
|
33
38
|
|
|
34
39
|
async def call_extra(self, functions: list[Callable[..., Any]], kwargs: dict[str, Any]) -> Any:
|
|
35
40
|
"""Await the hook with extra one-off implementations that are not registered."""
|
|
36
41
|
if self.spec.historic:
|
|
37
42
|
raise TypeError(f"historic hook {self.name!r} must be called via call_historic()")
|
|
38
|
-
self.check_arguments(kwargs)
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
kwargs = self.check_arguments(kwargs)
|
|
44
|
+
combined = sorted(
|
|
45
|
+
[*self._nonwrappers, *self._prepare_extra(functions)], key=lambda candidate: candidate.order_key
|
|
46
|
+
)
|
|
41
47
|
return await self._execute_async(kwargs, combined)
|
|
42
48
|
|
|
43
49
|
def call_historic(self, kwargs: dict[str, Any], result_callback: Callable[[Any], None] | None = None) -> None:
|
|
@@ -100,19 +106,61 @@ class AsyncHookCaller(HookCaller):
|
|
|
100
106
|
except BaseException as new_exc: # noqa: BLE001 - propagate the wrapper's error onward
|
|
101
107
|
exc = new_exc
|
|
102
108
|
else:
|
|
109
|
+
# Double yield: capture the error but keep unwinding the remaining
|
|
110
|
+
# wrappers so their teardown still runs; raised after the loop.
|
|
103
111
|
await generator.aclose()
|
|
104
|
-
|
|
112
|
+
exc = RuntimeError(f"async wrapper for {self.name!r} must yield exactly once")
|
|
105
113
|
if exc is not None:
|
|
106
114
|
raise exc
|
|
107
115
|
return result
|
|
108
116
|
|
|
109
117
|
|
|
118
|
+
# Async typed views returned by AsyncPluginManager.caller(); never instantiated
|
|
119
|
+
# (the runtime object is an AsyncHookCaller). Awaiting a call yields the mode type.
|
|
120
|
+
class AsyncCollectingCaller[**P, R](AsyncHookCaller):
|
|
121
|
+
"""A collecting async hook's typed caller: `await` a call to get `list[R]`."""
|
|
122
|
+
|
|
123
|
+
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> list[R]:
|
|
124
|
+
"""Await the collecting hook, returning each impl's result as `list[R]`."""
|
|
125
|
+
raise NotImplementedError # pragma: no cover - the runtime object is an AsyncHookCaller
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class AsyncFirstResultCaller[**P, R](AsyncHookCaller):
|
|
129
|
+
"""A firstresult async hook's typed caller: `await` a call to get `R | None`."""
|
|
130
|
+
|
|
131
|
+
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R | None:
|
|
132
|
+
"""Await the firstresult hook, returning the first non-None `R` or `None`."""
|
|
133
|
+
raise NotImplementedError # pragma: no cover - the runtime object is an AsyncHookCaller
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class AsyncPipelineCaller[**P, R](AsyncHookCaller):
|
|
137
|
+
"""A pipeline async hook's typed caller: `await` a call to get `R`."""
|
|
138
|
+
|
|
139
|
+
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
140
|
+
"""Await the pipeline hook, returning the threaded value `R`."""
|
|
141
|
+
raise NotImplementedError # pragma: no cover - the runtime object is an AsyncHookCaller
|
|
142
|
+
|
|
143
|
+
|
|
110
144
|
class AsyncPluginManager(PluginManager):
|
|
111
145
|
"""A PluginManager whose hooks are awaited; impls may be coroutine functions."""
|
|
112
146
|
|
|
113
|
-
def _make_caller(
|
|
147
|
+
def _make_caller(
|
|
148
|
+
self, name: str, spec: HookspecOpts, params: tuple[str, ...], defaults: dict[str, Any]
|
|
149
|
+
) -> HookCaller:
|
|
114
150
|
"""Build an AsyncHookCaller instead of the synchronous one."""
|
|
115
|
-
return AsyncHookCaller(name=name, spec=spec, params=params)
|
|
151
|
+
return AsyncHookCaller(name=name, spec=spec, params=params, defaults=defaults)
|
|
152
|
+
|
|
153
|
+
@overload # type: ignore[override] # async manager returns awaitable callers
|
|
154
|
+
def caller[**P, R](self, spec: FirstResultSpec[P, R]) -> AsyncFirstResultCaller[P, R]: ...
|
|
155
|
+
@overload
|
|
156
|
+
def caller[**P, R](self, spec: PipelineSpec[P, R]) -> AsyncPipelineCaller[P, R]: ...
|
|
157
|
+
@overload
|
|
158
|
+
def caller[**P, R](self, spec: CollectingSpec[P, R]) -> AsyncCollectingCaller[P, R]: ...
|
|
159
|
+
def caller( # pyright: ignore[reportIncompatibleMethodOverride] # async returns awaitable callers
|
|
160
|
+
self, spec: object
|
|
161
|
+
) -> HookCaller:
|
|
162
|
+
"""Return the typed async caller for a `@hookspec`-decorated spec function."""
|
|
163
|
+
return self._caller(spec)
|
|
116
164
|
|
|
117
165
|
|
|
118
166
|
async def _maybe_await(value: Any) -> Any:
|
|
@@ -23,10 +23,17 @@ from collections.abc import Callable, Generator, Iterator
|
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
24
|
from importlib.metadata import entry_points
|
|
25
25
|
from types import GeneratorType
|
|
26
|
-
from typing import Any, Self
|
|
26
|
+
from typing import Any, NoReturn, Self, overload
|
|
27
27
|
|
|
28
28
|
from pluginkit.exceptions import PluginValidationError
|
|
29
|
-
from pluginkit.markers import
|
|
29
|
+
from pluginkit.markers import (
|
|
30
|
+
CollectingSpec,
|
|
31
|
+
FirstResultSpec,
|
|
32
|
+
HistoricSpec,
|
|
33
|
+
HookimplOpts,
|
|
34
|
+
HookspecOpts,
|
|
35
|
+
PipelineSpec,
|
|
36
|
+
)
|
|
30
37
|
|
|
31
38
|
# Sentinel distinguishing "no result yet" from a legitimate None result.
|
|
32
39
|
_UNSET: Any = object()
|
|
@@ -82,6 +89,10 @@ class HookCaller:
|
|
|
82
89
|
spec: HookspecOpts
|
|
83
90
|
params: tuple[str, ...] = ()
|
|
84
91
|
argnames: frozenset[str] = frozenset()
|
|
92
|
+
# Default values for spec params that declare one. A call may omit these (the
|
|
93
|
+
# branded caller's ParamSpec makes them optional); they are filled in at call
|
|
94
|
+
# time so the type checker and the runtime agree.
|
|
95
|
+
defaults: dict[str, Any] = field(default_factory=dict)
|
|
85
96
|
_impls: list[HookImpl] = field(default_factory=list)
|
|
86
97
|
_wrappers: list[HookImpl] = field(default_factory=list)
|
|
87
98
|
_nonwrappers: list[HookImpl] = field(default_factory=list)
|
|
@@ -92,11 +103,18 @@ class HookCaller:
|
|
|
92
103
|
if self.params and not self.argnames:
|
|
93
104
|
self.argnames = frozenset(self.params)
|
|
94
105
|
|
|
95
|
-
def check_arguments(self, kwargs: dict[str, Any]) ->
|
|
96
|
-
"""
|
|
106
|
+
def check_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
107
|
+
"""Fill any omitted defaulted args, then validate the call against the spec.
|
|
108
|
+
|
|
109
|
+
Returns the completed kwargs (defaults filled). Spec params with a default
|
|
110
|
+
are optional at the call site; required params and unknown args are still
|
|
111
|
+
rejected.
|
|
112
|
+
"""
|
|
113
|
+
if self.defaults:
|
|
114
|
+
kwargs = {**self.defaults, **kwargs}
|
|
97
115
|
# dict_keys compares as a set against the frozenset without allocating one.
|
|
98
116
|
if kwargs.keys() == self.argnames:
|
|
99
|
-
return
|
|
117
|
+
return kwargs
|
|
100
118
|
provided = frozenset(kwargs)
|
|
101
119
|
problems: list[str] = []
|
|
102
120
|
missing = self.argnames - provided
|
|
@@ -130,11 +148,23 @@ class HookCaller:
|
|
|
130
148
|
"""Return whether the named plugin contributes any impl to this hook."""
|
|
131
149
|
return any(impl.plugin_name == plugin_name for impl in self._impls)
|
|
132
150
|
|
|
151
|
+
def _prepare_extra(self, functions: list[Callable[..., Any]]) -> list[HookImpl]:
|
|
152
|
+
"""Build one-off impls for call_extra, validating their args against the spec."""
|
|
153
|
+
extra: list[HookImpl] = []
|
|
154
|
+
for function in functions:
|
|
155
|
+
impl = HookImpl.from_function("<call_extra>", function, HookimplOpts())
|
|
156
|
+
unknown = impl.accepts - self.argnames
|
|
157
|
+
if unknown:
|
|
158
|
+
raise TypeError(f"call_extra impl for {self.name!r} declares unknown argument(s) {sorted(unknown)}")
|
|
159
|
+
impl.passthrough = impl.accepts == self.argnames
|
|
160
|
+
extra.append(impl)
|
|
161
|
+
return extra
|
|
162
|
+
|
|
133
163
|
def __call__(self, **kwargs: Any) -> Any:
|
|
134
164
|
"""Call the hook: a list, a single value (firstresult), or the threaded value (pipeline)."""
|
|
135
165
|
if self.spec.historic:
|
|
136
166
|
raise TypeError(f"historic hook {self.name!r} must be called via call_historic()")
|
|
137
|
-
self.check_arguments(kwargs)
|
|
167
|
+
kwargs = self.check_arguments(kwargs)
|
|
138
168
|
return self._execute(kwargs, self._nonwrappers)
|
|
139
169
|
|
|
140
170
|
def call_extra(self, functions: list[Callable[..., Any]], kwargs: dict[str, Any]) -> Any:
|
|
@@ -146,23 +176,17 @@ class HookCaller:
|
|
|
146
176
|
"""
|
|
147
177
|
if self.spec.historic:
|
|
148
178
|
raise TypeError(f"historic hook {self.name!r} must be called via call_historic()")
|
|
149
|
-
self.check_arguments(kwargs)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
unknown = impl.accepts - self.argnames
|
|
154
|
-
if unknown:
|
|
155
|
-
raise TypeError(f"call_extra impl for {self.name!r} declares unknown argument(s) {sorted(unknown)}")
|
|
156
|
-
impl.passthrough = impl.accepts == self.argnames
|
|
157
|
-
extra.append(impl)
|
|
158
|
-
combined = sorted([*self._nonwrappers, *extra], key=lambda candidate: candidate.order_key)
|
|
179
|
+
kwargs = self.check_arguments(kwargs)
|
|
180
|
+
combined = sorted(
|
|
181
|
+
[*self._nonwrappers, *self._prepare_extra(functions)], key=lambda candidate: candidate.order_key
|
|
182
|
+
)
|
|
159
183
|
return self._execute(kwargs, combined)
|
|
160
184
|
|
|
161
185
|
def call_historic(self, kwargs: dict[str, Any], result_callback: Callable[[Any], None] | None = None) -> None:
|
|
162
186
|
"""Call a historic hook now and remember it for plugins registered later."""
|
|
163
187
|
if not self.spec.historic:
|
|
164
188
|
raise TypeError(f"hook {self.name!r} is not historic")
|
|
165
|
-
self.check_arguments(kwargs)
|
|
189
|
+
kwargs = self.check_arguments(kwargs)
|
|
166
190
|
self._history.append((kwargs, result_callback))
|
|
167
191
|
for outcome in self._collect(kwargs):
|
|
168
192
|
if result_callback is not None:
|
|
@@ -259,8 +283,10 @@ class HookCaller:
|
|
|
259
283
|
exc = new_exc
|
|
260
284
|
else:
|
|
261
285
|
# The generator yielded a second time, violating the one-yield contract.
|
|
286
|
+
# Capture the error but keep unwinding so the remaining wrappers still
|
|
287
|
+
# tear down; the error propagates through them and is raised at the end.
|
|
262
288
|
generator.close()
|
|
263
|
-
|
|
289
|
+
exc = RuntimeError(f"wrapper for {self.name!r} must yield exactly once")
|
|
264
290
|
if exc is not None:
|
|
265
291
|
raise exc
|
|
266
292
|
return result
|
|
@@ -274,6 +300,46 @@ class HookCaller:
|
|
|
274
300
|
return f"<HookCaller {self.name!r} impls={len(self._impls)}>"
|
|
275
301
|
|
|
276
302
|
|
|
303
|
+
# Typed views returned by PluginManager.caller(). The runtime object is always a
|
|
304
|
+
# plain HookCaller; these subclasses are never instantiated - they exist only to
|
|
305
|
+
# refine the static return type of a call per dispatch mode, deriving the impl
|
|
306
|
+
# ParamSpec P and return R from the branded spec.
|
|
307
|
+
class CollectingCaller[**P, R](HookCaller):
|
|
308
|
+
"""A collecting hook's typed caller: a call returns `list[R]`."""
|
|
309
|
+
|
|
310
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> list[R]:
|
|
311
|
+
"""Call the collecting hook, returning each impl's result as `list[R]`."""
|
|
312
|
+
raise NotImplementedError # pragma: no cover - the runtime object is a HookCaller
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class FirstResultCaller[**P, R](HookCaller):
|
|
316
|
+
"""A firstresult hook's typed caller: a call returns `R | None`."""
|
|
317
|
+
|
|
318
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R | None:
|
|
319
|
+
"""Call the firstresult hook, returning the first non-None `R` or `None`."""
|
|
320
|
+
raise NotImplementedError # pragma: no cover - the runtime object is a HookCaller
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class PipelineCaller[**P, R](HookCaller):
|
|
324
|
+
"""A pipeline hook's typed caller: a call returns `R`."""
|
|
325
|
+
|
|
326
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
327
|
+
"""Call the pipeline hook, returning the threaded value `R`."""
|
|
328
|
+
raise NotImplementedError # pragma: no cover - the runtime object is a HookCaller
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class HistoricCaller[**P, R](HookCaller):
|
|
332
|
+
"""A historic hook's typed caller. Replay it with `call_historic({...})`.
|
|
333
|
+
|
|
334
|
+
Calling it directly raises - historic hooks have no plain call form - so the
|
|
335
|
+
typed `__call__` is `NoReturn` rather than a value it never produces.
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> NoReturn:
|
|
339
|
+
"""Historic hooks cannot be called directly; use `call_historic`."""
|
|
340
|
+
raise NotImplementedError # pragma: no cover - the runtime object is a HookCaller
|
|
341
|
+
|
|
342
|
+
|
|
277
343
|
class HookRelay:
|
|
278
344
|
"""Attribute-style access to hook callers, e.g. pm.hook.add_ingredients(...)."""
|
|
279
345
|
|
|
@@ -322,13 +388,48 @@ class PluginManager:
|
|
|
322
388
|
spec = getattr(member, self._spec_attribute, None)
|
|
323
389
|
if not isinstance(spec, HookspecOpts):
|
|
324
390
|
continue
|
|
325
|
-
|
|
391
|
+
signature = inspect.signature(member)
|
|
392
|
+
params = tuple(signature.parameters)
|
|
393
|
+
defaults = {
|
|
394
|
+
name: parameter.default
|
|
395
|
+
for name, parameter in signature.parameters.items()
|
|
396
|
+
if parameter.default is not inspect.Parameter.empty
|
|
397
|
+
}
|
|
326
398
|
self._validate_spec(member_name, spec, params)
|
|
327
|
-
self.hook._add_caller(self._make_caller(member_name, spec, params))
|
|
399
|
+
self.hook._add_caller(self._make_caller(member_name, spec, params, defaults))
|
|
328
400
|
|
|
329
|
-
def _make_caller(
|
|
401
|
+
def _make_caller(
|
|
402
|
+
self, name: str, spec: HookspecOpts, params: tuple[str, ...], defaults: dict[str, Any]
|
|
403
|
+
) -> HookCaller:
|
|
330
404
|
"""Build the caller for a spec; overridden by AsyncPluginManager."""
|
|
331
|
-
return HookCaller(name=name, spec=spec, params=params)
|
|
405
|
+
return HookCaller(name=name, spec=spec, params=params, defaults=defaults)
|
|
406
|
+
|
|
407
|
+
@overload
|
|
408
|
+
def caller[**P, R](self, spec: FirstResultSpec[P, R]) -> FirstResultCaller[P, R]: ...
|
|
409
|
+
@overload
|
|
410
|
+
def caller[**P, R](self, spec: PipelineSpec[P, R]) -> PipelineCaller[P, R]: ...
|
|
411
|
+
@overload
|
|
412
|
+
def caller[**P, R](self, spec: HistoricSpec[P, R]) -> HistoricCaller[P, R]: ...
|
|
413
|
+
@overload
|
|
414
|
+
def caller[**P, R](self, spec: CollectingSpec[P, R]) -> CollectingCaller[P, R]: ...
|
|
415
|
+
def caller(self, spec: object) -> HookCaller:
|
|
416
|
+
"""Return the typed caller for a `@hookspec`-decorated spec function.
|
|
417
|
+
|
|
418
|
+
The result is a plain `HookCaller`, but its static type carries the spec's
|
|
419
|
+
dispatch mode, so a call returns `list[R]` (collecting), `R | None`
|
|
420
|
+
(firstresult), or `R` (pipeline) - derived from the spec, not asserted.
|
|
421
|
+
"""
|
|
422
|
+
return self._caller(spec)
|
|
423
|
+
|
|
424
|
+
def _caller(self, spec: object) -> HookCaller:
|
|
425
|
+
"""Resolve a spec function to its registered caller (shared by subclasses)."""
|
|
426
|
+
name = getattr(spec, "__name__", None)
|
|
427
|
+
if not isinstance(name, str):
|
|
428
|
+
raise TypeError("caller() expects a @hookspec-decorated function")
|
|
429
|
+
found = self.hook._get_caller(name)
|
|
430
|
+
if found is None:
|
|
431
|
+
raise PluginValidationError(self.project_name, f"unknown hook spec {name!r}; call add_hookspecs() first")
|
|
432
|
+
return found
|
|
332
433
|
|
|
333
434
|
@staticmethod
|
|
334
435
|
def _validate_spec(name: str, spec: HookspecOpts, params: tuple[str, ...]) -> None:
|
|
@@ -360,8 +461,16 @@ class PluginManager:
|
|
|
360
461
|
|
|
361
462
|
impls = self._collect_impls(plugin_name, plugin)
|
|
362
463
|
self._name2plugin[plugin_name] = plugin
|
|
363
|
-
|
|
364
|
-
caller
|
|
464
|
+
try:
|
|
465
|
+
for caller, impl in impls:
|
|
466
|
+
caller.add_impl(impl)
|
|
467
|
+
except BaseException:
|
|
468
|
+
# add_impl can fail mid-loop (e.g. a historic replay raising). Roll the
|
|
469
|
+
# partial wiring back so registration is all-or-nothing.
|
|
470
|
+
self._name2plugin.pop(plugin_name, None)
|
|
471
|
+
for caller in self.hook._all_callers():
|
|
472
|
+
caller.remove_plugin(plugin_name)
|
|
473
|
+
raise
|
|
365
474
|
return plugin_name
|
|
366
475
|
|
|
367
476
|
def unregister(self, name_or_plugin: str | object) -> object | None:
|
|
@@ -431,6 +540,11 @@ class PluginManager:
|
|
|
431
540
|
|
|
432
541
|
Returns:
|
|
433
542
|
The number of plugins successfully registered.
|
|
543
|
+
|
|
544
|
+
Note:
|
|
545
|
+
With ``ignore_errors=False``, a failure part-way through leaves the
|
|
546
|
+
plugins registered before it registered; this method does not roll back
|
|
547
|
+
across plugins.
|
|
434
548
|
"""
|
|
435
549
|
count = 0
|
|
436
550
|
for entry_point in entry_points(group=group):
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Decorator markers that tag functions as hook specs or hook implementations.
|
|
2
|
+
|
|
3
|
+
A marker stamps a small frozen dataclass of options onto the decorated function
|
|
4
|
+
under a project-namespaced attribute, so the manager can later recognise specs and
|
|
5
|
+
impls by introspection.
|
|
6
|
+
|
|
7
|
+
The `@hookspec` decorator is **typed by dispatch mode**: it returns a branded spec
|
|
8
|
+
type (`CollectingSpec` / `FirstResultSpec` / `PipelineSpec`) that carries the impl
|
|
9
|
+
signature (`P`) and per-impl return type (`R`). `PluginManager.caller(spec)` reads
|
|
10
|
+
that brand to hand back a caller whose result type is exactly right for the mode -
|
|
11
|
+
`list[R]`, `R | None`, or `R`. The brand classes are type-level only; they are never
|
|
12
|
+
instantiated (a spec is a declaration, not a callable you invoke directly).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any, Literal, overload
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class HookspecOpts:
|
|
22
|
+
"""Options attached to a hook specification."""
|
|
23
|
+
|
|
24
|
+
firstresult: bool = False
|
|
25
|
+
historic: bool = False
|
|
26
|
+
pipeline: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class HookimplOpts:
|
|
31
|
+
"""Options attached to a hook implementation."""
|
|
32
|
+
|
|
33
|
+
tryfirst: bool = False
|
|
34
|
+
trylast: bool = False
|
|
35
|
+
wrapper: bool = False
|
|
36
|
+
optionalhook: bool = False
|
|
37
|
+
specname: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CollectingSpec[**P, R]:
|
|
41
|
+
"""A collecting hook spec: a call collects each impl's `R` into `list[R]`."""
|
|
42
|
+
|
|
43
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
44
|
+
"""Specs are declarations; obtain a callable via PluginManager.caller(spec)."""
|
|
45
|
+
raise NotImplementedError("a spec is a declaration; call it via PluginManager.caller(spec)")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FirstResultSpec[**P, R]:
|
|
49
|
+
"""A firstresult hook spec: a call returns the first non-None `R`, or `None`."""
|
|
50
|
+
|
|
51
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
52
|
+
"""Specs are declarations; obtain a callable via PluginManager.caller(spec)."""
|
|
53
|
+
raise NotImplementedError("a spec is a declaration; call it via PluginManager.caller(spec)")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PipelineSpec[**P, R]:
|
|
57
|
+
"""A pipeline hook spec: a call threads `R` through the impls and returns it."""
|
|
58
|
+
|
|
59
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
60
|
+
"""Specs are declarations; obtain a callable via PluginManager.caller(spec)."""
|
|
61
|
+
raise NotImplementedError("a spec is a declaration; call it via PluginManager.caller(spec)")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class HistoricSpec[**P, R]:
|
|
65
|
+
"""A historic hook spec: replayed to late plugins, driven via `call_historic`."""
|
|
66
|
+
|
|
67
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
68
|
+
"""Specs are declarations; obtain a callable via PluginManager.caller(spec)."""
|
|
69
|
+
raise NotImplementedError("a spec is a declaration; call it via PluginManager.caller(spec)")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class HookspecMarker:
|
|
73
|
+
"""Creates the @hookspec decorator bound to a project name."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, project_name: str) -> None:
|
|
76
|
+
"""Bind the marker to a project name used for the stamped attribute."""
|
|
77
|
+
self.project_name = project_name
|
|
78
|
+
self.attribute = f"{project_name}_spec"
|
|
79
|
+
|
|
80
|
+
@overload
|
|
81
|
+
def __call__[**P, R](self, function: Callable[P, R]) -> CollectingSpec[P, R]: ...
|
|
82
|
+
@overload
|
|
83
|
+
def __call__[**P, R](
|
|
84
|
+
self, function: None = ..., *, firstresult: Literal[True], historic: bool = ...
|
|
85
|
+
) -> Callable[[Callable[P, R]], FirstResultSpec[P, R]]: ...
|
|
86
|
+
@overload
|
|
87
|
+
def __call__[**P, R](
|
|
88
|
+
self, function: None = ..., *, pipeline: Literal[True], historic: bool = ...
|
|
89
|
+
) -> Callable[[Callable[P, R]], PipelineSpec[P, R]]: ...
|
|
90
|
+
@overload
|
|
91
|
+
def __call__[**P, R](
|
|
92
|
+
self, function: None = ..., *, historic: Literal[True]
|
|
93
|
+
) -> Callable[[Callable[P, R]], HistoricSpec[P, R]]: ...
|
|
94
|
+
@overload
|
|
95
|
+
def __call__[**P, R](
|
|
96
|
+
self, function: None = ..., *, historic: Literal[False] = ...
|
|
97
|
+
) -> Callable[[Callable[P, R]], CollectingSpec[P, R]]: ...
|
|
98
|
+
def __call__(
|
|
99
|
+
self,
|
|
100
|
+
function: Callable[..., Any] | None = None,
|
|
101
|
+
*,
|
|
102
|
+
firstresult: bool = False,
|
|
103
|
+
historic: bool = False,
|
|
104
|
+
pipeline: bool = False,
|
|
105
|
+
) -> Any:
|
|
106
|
+
"""Stamp HookspecOpts onto the function; supports bare and called forms."""
|
|
107
|
+
|
|
108
|
+
def mark(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
109
|
+
setattr(func, self.attribute, HookspecOpts(firstresult=firstresult, historic=historic, pipeline=pipeline))
|
|
110
|
+
return func
|
|
111
|
+
|
|
112
|
+
return mark(function) if function is not None else mark
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class HookimplMarker:
|
|
116
|
+
"""Creates the @hookimpl decorator bound to a project name."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, project_name: str) -> None:
|
|
119
|
+
"""Bind the marker to a project name used for the stamped attribute."""
|
|
120
|
+
self.project_name = project_name
|
|
121
|
+
self.attribute = f"{project_name}_impl"
|
|
122
|
+
|
|
123
|
+
@overload
|
|
124
|
+
def __call__[F: Callable[..., Any]](self, function: F) -> F: ...
|
|
125
|
+
@overload
|
|
126
|
+
def __call__[F: Callable[..., Any]](
|
|
127
|
+
self,
|
|
128
|
+
function: None = ...,
|
|
129
|
+
*,
|
|
130
|
+
tryfirst: bool = ...,
|
|
131
|
+
trylast: bool = ...,
|
|
132
|
+
wrapper: bool = ...,
|
|
133
|
+
optionalhook: bool = ...,
|
|
134
|
+
specname: str | None = ...,
|
|
135
|
+
) -> Callable[[F], F]: ...
|
|
136
|
+
def __call__[F: Callable[..., Any]](
|
|
137
|
+
self,
|
|
138
|
+
function: F | None = None,
|
|
139
|
+
*,
|
|
140
|
+
tryfirst: bool = False,
|
|
141
|
+
trylast: bool = False,
|
|
142
|
+
wrapper: bool = False,
|
|
143
|
+
optionalhook: bool = False,
|
|
144
|
+
specname: str | None = None,
|
|
145
|
+
) -> F | Callable[[F], F]:
|
|
146
|
+
"""Stamp HookimplOpts onto the function; supports bare and called forms."""
|
|
147
|
+
|
|
148
|
+
def mark(func: F) -> F:
|
|
149
|
+
setattr(
|
|
150
|
+
func,
|
|
151
|
+
self.attribute,
|
|
152
|
+
HookimplOpts(
|
|
153
|
+
tryfirst=tryfirst,
|
|
154
|
+
trylast=trylast,
|
|
155
|
+
wrapper=wrapper,
|
|
156
|
+
optionalhook=optionalhook,
|
|
157
|
+
specname=specname,
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
return func
|
|
161
|
+
|
|
162
|
+
return mark(function) if function is not None else mark
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"""pluginkit: a small, dependency-free, pluggy-style plugin framework.
|
|
2
|
-
|
|
3
|
-
Public API:
|
|
4
|
-
|
|
5
|
-
- :class:`HookspecMarker` / :class:`HookimplMarker` - decorators that declare hook
|
|
6
|
-
specifications and implementations.
|
|
7
|
-
- :class:`HookspecOpts` / :class:`HookimplOpts` - the option records the markers stamp.
|
|
8
|
-
- :class:`PluginManager` - registers plugins and dispatches hook calls.
|
|
9
|
-
- :class:`HookRelay` / :class:`HookCaller` / :class:`HookImpl` - the dispatch internals.
|
|
10
|
-
- :class:`PluginValidationError` - raised when a plugin is invalid.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from pluginkit.aio import AsyncHookCaller, AsyncPluginManager
|
|
14
|
-
from pluginkit.exceptions import PluginValidationError
|
|
15
|
-
from pluginkit.manager import HookCaller, HookImpl, HookRelay, PluginManager
|
|
16
|
-
from pluginkit.markers import (
|
|
17
|
-
HookimplMarker,
|
|
18
|
-
HookimplOpts,
|
|
19
|
-
HookspecMarker,
|
|
20
|
-
HookspecOpts,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
__version__ = "0.1.0"
|
|
24
|
-
|
|
25
|
-
__all__ = [
|
|
26
|
-
"AsyncHookCaller",
|
|
27
|
-
"AsyncPluginManager",
|
|
28
|
-
"HookCaller",
|
|
29
|
-
"HookImpl",
|
|
30
|
-
"HookRelay",
|
|
31
|
-
"HookimplMarker",
|
|
32
|
-
"HookimplOpts",
|
|
33
|
-
"HookspecMarker",
|
|
34
|
-
"HookspecOpts",
|
|
35
|
-
"PluginManager",
|
|
36
|
-
"PluginValidationError",
|
|
37
|
-
"__version__",
|
|
38
|
-
]
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
"""Decorator markers that tag functions as hook specs or hook implementations.
|
|
2
|
-
|
|
3
|
-
Mirrors pluggy's HookspecMarker / HookimplMarker. A marker stamps a small frozen
|
|
4
|
-
dataclass of options onto the decorated function under a project-namespaced
|
|
5
|
-
attribute, so the manager can later recognise specs and impls by introspection.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from collections.abc import Callable
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from typing import Any, TypeVar, overload
|
|
11
|
-
|
|
12
|
-
F = TypeVar("F", bound=Callable[..., Any])
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@dataclass(frozen=True, slots=True)
|
|
16
|
-
class HookspecOpts:
|
|
17
|
-
"""Options attached to a hook specification."""
|
|
18
|
-
|
|
19
|
-
firstresult: bool = False
|
|
20
|
-
historic: bool = False
|
|
21
|
-
pipeline: bool = False
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass(frozen=True, slots=True)
|
|
25
|
-
class HookimplOpts:
|
|
26
|
-
"""Options attached to a hook implementation."""
|
|
27
|
-
|
|
28
|
-
tryfirst: bool = False
|
|
29
|
-
trylast: bool = False
|
|
30
|
-
wrapper: bool = False
|
|
31
|
-
optionalhook: bool = False
|
|
32
|
-
specname: str | None = None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class HookspecMarker:
|
|
36
|
-
"""Creates the @hookspec decorator bound to a project name."""
|
|
37
|
-
|
|
38
|
-
def __init__(self, project_name: str) -> None:
|
|
39
|
-
"""Bind the marker to a project name used for the stamped attribute."""
|
|
40
|
-
self.project_name = project_name
|
|
41
|
-
self.attribute = f"{project_name}_spec"
|
|
42
|
-
|
|
43
|
-
@overload
|
|
44
|
-
def __call__(self, function: F) -> F: ...
|
|
45
|
-
|
|
46
|
-
@overload
|
|
47
|
-
def __call__(
|
|
48
|
-
self, function: None = ..., *, firstresult: bool = ..., historic: bool = ..., pipeline: bool = ...
|
|
49
|
-
) -> Callable[[F], F]: ...
|
|
50
|
-
|
|
51
|
-
def __call__(
|
|
52
|
-
self,
|
|
53
|
-
function: F | None = None,
|
|
54
|
-
*,
|
|
55
|
-
firstresult: bool = False,
|
|
56
|
-
historic: bool = False,
|
|
57
|
-
pipeline: bool = False,
|
|
58
|
-
) -> F | Callable[[F], F]:
|
|
59
|
-
"""Stamp HookspecOpts onto the function; supports bare and called forms."""
|
|
60
|
-
|
|
61
|
-
def mark(func: F) -> F:
|
|
62
|
-
setattr(func, self.attribute, HookspecOpts(firstresult=firstresult, historic=historic, pipeline=pipeline))
|
|
63
|
-
return func
|
|
64
|
-
|
|
65
|
-
return mark(function) if function is not None else mark
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class HookimplMarker:
|
|
69
|
-
"""Creates the @hookimpl decorator bound to a project name."""
|
|
70
|
-
|
|
71
|
-
def __init__(self, project_name: str) -> None:
|
|
72
|
-
"""Bind the marker to a project name used for the stamped attribute."""
|
|
73
|
-
self.project_name = project_name
|
|
74
|
-
self.attribute = f"{project_name}_impl"
|
|
75
|
-
|
|
76
|
-
@overload
|
|
77
|
-
def __call__(self, function: F) -> F: ...
|
|
78
|
-
|
|
79
|
-
@overload
|
|
80
|
-
def __call__(
|
|
81
|
-
self,
|
|
82
|
-
function: None = ...,
|
|
83
|
-
*,
|
|
84
|
-
tryfirst: bool = ...,
|
|
85
|
-
trylast: bool = ...,
|
|
86
|
-
wrapper: bool = ...,
|
|
87
|
-
optionalhook: bool = ...,
|
|
88
|
-
specname: str | None = ...,
|
|
89
|
-
) -> Callable[[F], F]: ...
|
|
90
|
-
|
|
91
|
-
def __call__(
|
|
92
|
-
self,
|
|
93
|
-
function: F | None = None,
|
|
94
|
-
*,
|
|
95
|
-
tryfirst: bool = False,
|
|
96
|
-
trylast: bool = False,
|
|
97
|
-
wrapper: bool = False,
|
|
98
|
-
optionalhook: bool = False,
|
|
99
|
-
specname: str | None = None,
|
|
100
|
-
) -> F | Callable[[F], F]:
|
|
101
|
-
"""Stamp HookimplOpts onto the function; supports bare and called forms."""
|
|
102
|
-
|
|
103
|
-
def mark(func: F) -> F:
|
|
104
|
-
setattr(
|
|
105
|
-
func,
|
|
106
|
-
self.attribute,
|
|
107
|
-
HookimplOpts(
|
|
108
|
-
tryfirst=tryfirst,
|
|
109
|
-
trylast=trylast,
|
|
110
|
-
wrapper=wrapper,
|
|
111
|
-
optionalhook=optionalhook,
|
|
112
|
-
specname=specname,
|
|
113
|
-
),
|
|
114
|
-
)
|
|
115
|
-
return func
|
|
116
|
-
|
|
117
|
-
return mark(function) if function is not None else mark
|
|
File without changes
|
|
File without changes
|