pluginkit 0.2.0__tar.gz → 0.3.0__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.0}/PKG-INFO +15 -14
- {pluginkit-0.2.0 → pluginkit-0.3.0}/README.md +12 -9
- {pluginkit-0.2.0 → pluginkit-0.3.0}/pyproject.toml +6 -8
- pluginkit-0.3.0/src/pluginkit/__init__.py +77 -0
- {pluginkit-0.2.0 → pluginkit-0.3.0}/src/pluginkit/aio.py +46 -2
- {pluginkit-0.2.0 → pluginkit-0.3.0}/src/pluginkit/manager.py +61 -2
- {pluginkit-0.2.0 → pluginkit-0.3.0}/src/pluginkit/markers.py +53 -20
- pluginkit-0.2.0/src/pluginkit/__init__.py +0 -38
- {pluginkit-0.2.0 → pluginkit-0.3.0}/src/pluginkit/exceptions.py +0 -0
- {pluginkit-0.2.0 → pluginkit-0.3.0}/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.0
|
|
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.0"
|
|
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,77 @@
|
|
|
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
|
+
HookCaller,
|
|
35
|
+
HookImpl,
|
|
36
|
+
HookRelay,
|
|
37
|
+
PipelineCaller,
|
|
38
|
+
PluginManager,
|
|
39
|
+
)
|
|
40
|
+
from pluginkit.markers import (
|
|
41
|
+
CollectingSpec,
|
|
42
|
+
FirstResultSpec,
|
|
43
|
+
HookimplMarker,
|
|
44
|
+
HookimplOpts,
|
|
45
|
+
HookspecMarker,
|
|
46
|
+
HookspecOpts,
|
|
47
|
+
PipelineSpec,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
__version__ = version("pluginkit")
|
|
52
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree without an install
|
|
53
|
+
__version__ = "0.0.0+unknown"
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"AsyncCollectingCaller",
|
|
57
|
+
"AsyncFirstResultCaller",
|
|
58
|
+
"AsyncHookCaller",
|
|
59
|
+
"AsyncPipelineCaller",
|
|
60
|
+
"AsyncPluginManager",
|
|
61
|
+
"CollectingCaller",
|
|
62
|
+
"CollectingSpec",
|
|
63
|
+
"FirstResultCaller",
|
|
64
|
+
"FirstResultSpec",
|
|
65
|
+
"HookCaller",
|
|
66
|
+
"HookImpl",
|
|
67
|
+
"HookRelay",
|
|
68
|
+
"HookimplMarker",
|
|
69
|
+
"HookimplOpts",
|
|
70
|
+
"HookspecMarker",
|
|
71
|
+
"HookspecOpts",
|
|
72
|
+
"PipelineCaller",
|
|
73
|
+
"PipelineSpec",
|
|
74
|
+
"PluginManager",
|
|
75
|
+
"PluginValidationError",
|
|
76
|
+
"__version__",
|
|
77
|
+
]
|
|
@@ -15,10 +15,16 @@ 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
|
+
HookimplOpts,
|
|
25
|
+
HookspecOpts,
|
|
26
|
+
PipelineSpec,
|
|
27
|
+
)
|
|
22
28
|
|
|
23
29
|
|
|
24
30
|
class AsyncHookCaller(HookCaller):
|
|
@@ -107,6 +113,32 @@ class AsyncHookCaller(HookCaller):
|
|
|
107
113
|
return result
|
|
108
114
|
|
|
109
115
|
|
|
116
|
+
# Async typed views returned by AsyncPluginManager.caller(); never instantiated
|
|
117
|
+
# (the runtime object is an AsyncHookCaller). Awaiting a call yields the mode type.
|
|
118
|
+
class AsyncCollectingCaller[**P, R](AsyncHookCaller):
|
|
119
|
+
"""A collecting async hook's typed caller: `await` a call to get `list[R]`."""
|
|
120
|
+
|
|
121
|
+
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> list[R]:
|
|
122
|
+
"""Await the collecting hook, returning each impl's result as `list[R]`."""
|
|
123
|
+
raise NotImplementedError # pragma: no cover - the runtime object is an AsyncHookCaller
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class AsyncFirstResultCaller[**P, R](AsyncHookCaller):
|
|
127
|
+
"""A firstresult async hook's typed caller: `await` a call to get `R | None`."""
|
|
128
|
+
|
|
129
|
+
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R | None:
|
|
130
|
+
"""Await the firstresult hook, returning the first non-None `R` or `None`."""
|
|
131
|
+
raise NotImplementedError # pragma: no cover - the runtime object is an AsyncHookCaller
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class AsyncPipelineCaller[**P, R](AsyncHookCaller):
|
|
135
|
+
"""A pipeline async hook's typed caller: `await` a call to get `R`."""
|
|
136
|
+
|
|
137
|
+
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
138
|
+
"""Await the pipeline hook, returning the threaded value `R`."""
|
|
139
|
+
raise NotImplementedError # pragma: no cover - the runtime object is an AsyncHookCaller
|
|
140
|
+
|
|
141
|
+
|
|
110
142
|
class AsyncPluginManager(PluginManager):
|
|
111
143
|
"""A PluginManager whose hooks are awaited; impls may be coroutine functions."""
|
|
112
144
|
|
|
@@ -114,6 +146,18 @@ class AsyncPluginManager(PluginManager):
|
|
|
114
146
|
"""Build an AsyncHookCaller instead of the synchronous one."""
|
|
115
147
|
return AsyncHookCaller(name=name, spec=spec, params=params)
|
|
116
148
|
|
|
149
|
+
@overload # type: ignore[override] # async manager returns awaitable callers
|
|
150
|
+
def caller[**P, R](self, spec: FirstResultSpec[P, R]) -> AsyncFirstResultCaller[P, R]: ...
|
|
151
|
+
@overload
|
|
152
|
+
def caller[**P, R](self, spec: PipelineSpec[P, R]) -> AsyncPipelineCaller[P, R]: ...
|
|
153
|
+
@overload
|
|
154
|
+
def caller[**P, R](self, spec: CollectingSpec[P, R]) -> AsyncCollectingCaller[P, R]: ...
|
|
155
|
+
def caller( # pyright: ignore[reportIncompatibleMethodOverride] # async returns awaitable callers
|
|
156
|
+
self, spec: object
|
|
157
|
+
) -> HookCaller:
|
|
158
|
+
"""Return the typed async caller for a `@hookspec`-decorated spec function."""
|
|
159
|
+
return self._caller(spec)
|
|
160
|
+
|
|
117
161
|
|
|
118
162
|
async def _maybe_await(value: Any) -> Any:
|
|
119
163
|
"""Await a value if it is awaitable, otherwise return it unchanged."""
|
|
@@ -23,10 +23,16 @@ 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, 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
|
+
HookimplOpts,
|
|
33
|
+
HookspecOpts,
|
|
34
|
+
PipelineSpec,
|
|
35
|
+
)
|
|
30
36
|
|
|
31
37
|
# Sentinel distinguishing "no result yet" from a legitimate None result.
|
|
32
38
|
_UNSET: Any = object()
|
|
@@ -274,6 +280,34 @@ class HookCaller:
|
|
|
274
280
|
return f"<HookCaller {self.name!r} impls={len(self._impls)}>"
|
|
275
281
|
|
|
276
282
|
|
|
283
|
+
# Typed views returned by PluginManager.caller(). The runtime object is always a
|
|
284
|
+
# plain HookCaller; these subclasses are never instantiated - they exist only to
|
|
285
|
+
# refine the static return type of a call per dispatch mode, deriving the impl
|
|
286
|
+
# ParamSpec P and return R from the branded spec.
|
|
287
|
+
class CollectingCaller[**P, R](HookCaller):
|
|
288
|
+
"""A collecting hook's typed caller: a call returns `list[R]`."""
|
|
289
|
+
|
|
290
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> list[R]:
|
|
291
|
+
"""Call the collecting hook, returning each impl's result as `list[R]`."""
|
|
292
|
+
raise NotImplementedError # pragma: no cover - the runtime object is a HookCaller
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class FirstResultCaller[**P, R](HookCaller):
|
|
296
|
+
"""A firstresult hook's typed caller: a call returns `R | None`."""
|
|
297
|
+
|
|
298
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R | None:
|
|
299
|
+
"""Call the firstresult hook, returning the first non-None `R` or `None`."""
|
|
300
|
+
raise NotImplementedError # pragma: no cover - the runtime object is a HookCaller
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class PipelineCaller[**P, R](HookCaller):
|
|
304
|
+
"""A pipeline hook's typed caller: a call returns `R`."""
|
|
305
|
+
|
|
306
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
307
|
+
"""Call the pipeline hook, returning the threaded value `R`."""
|
|
308
|
+
raise NotImplementedError # pragma: no cover - the runtime object is a HookCaller
|
|
309
|
+
|
|
310
|
+
|
|
277
311
|
class HookRelay:
|
|
278
312
|
"""Attribute-style access to hook callers, e.g. pm.hook.add_ingredients(...)."""
|
|
279
313
|
|
|
@@ -330,6 +364,31 @@ class PluginManager:
|
|
|
330
364
|
"""Build the caller for a spec; overridden by AsyncPluginManager."""
|
|
331
365
|
return HookCaller(name=name, spec=spec, params=params)
|
|
332
366
|
|
|
367
|
+
@overload
|
|
368
|
+
def caller[**P, R](self, spec: FirstResultSpec[P, R]) -> FirstResultCaller[P, R]: ...
|
|
369
|
+
@overload
|
|
370
|
+
def caller[**P, R](self, spec: PipelineSpec[P, R]) -> PipelineCaller[P, R]: ...
|
|
371
|
+
@overload
|
|
372
|
+
def caller[**P, R](self, spec: CollectingSpec[P, R]) -> CollectingCaller[P, R]: ...
|
|
373
|
+
def caller(self, spec: object) -> HookCaller:
|
|
374
|
+
"""Return the typed caller for a `@hookspec`-decorated spec function.
|
|
375
|
+
|
|
376
|
+
The result is a plain `HookCaller`, but its static type carries the spec's
|
|
377
|
+
dispatch mode, so a call returns `list[R]` (collecting), `R | None`
|
|
378
|
+
(firstresult), or `R` (pipeline) - derived from the spec, not asserted.
|
|
379
|
+
"""
|
|
380
|
+
return self._caller(spec)
|
|
381
|
+
|
|
382
|
+
def _caller(self, spec: object) -> HookCaller:
|
|
383
|
+
"""Resolve a spec function to its registered caller (shared by subclasses)."""
|
|
384
|
+
name = getattr(spec, "__name__", None)
|
|
385
|
+
if not isinstance(name, str):
|
|
386
|
+
raise TypeError("caller() expects a @hookspec-decorated function")
|
|
387
|
+
found = self.hook._get_caller(name)
|
|
388
|
+
if found is None:
|
|
389
|
+
raise PluginValidationError(self.project_name, f"unknown hook spec {name!r}; call add_hookspecs() first")
|
|
390
|
+
return found
|
|
391
|
+
|
|
333
392
|
@staticmethod
|
|
334
393
|
def _validate_spec(name: str, spec: HookspecOpts, params: tuple[str, ...]) -> None:
|
|
335
394
|
"""Reject contradictory or impossible spec option combinations."""
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
"""Decorator markers that tag functions as hook specs or hook implementations.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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).
|
|
6
13
|
"""
|
|
7
14
|
|
|
8
15
|
from collections.abc import Callable
|
|
9
16
|
from dataclasses import dataclass
|
|
10
|
-
from typing import Any,
|
|
11
|
-
|
|
12
|
-
F = TypeVar("F", bound=Callable[..., Any])
|
|
17
|
+
from typing import Any, Literal, overload
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
@dataclass(frozen=True, slots=True)
|
|
@@ -32,6 +37,30 @@ class HookimplOpts:
|
|
|
32
37
|
specname: str | None = None
|
|
33
38
|
|
|
34
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
|
+
|
|
35
64
|
class HookspecMarker:
|
|
36
65
|
"""Creates the @hookspec decorator bound to a project name."""
|
|
37
66
|
|
|
@@ -41,24 +70,30 @@ class HookspecMarker:
|
|
|
41
70
|
self.attribute = f"{project_name}_spec"
|
|
42
71
|
|
|
43
72
|
@overload
|
|
44
|
-
def __call__(self, function:
|
|
45
|
-
|
|
73
|
+
def __call__[**P, R](self, function: Callable[P, R]) -> CollectingSpec[P, R]: ...
|
|
46
74
|
@overload
|
|
47
|
-
def __call__(
|
|
48
|
-
self, function: None = ..., *, firstresult:
|
|
49
|
-
) -> Callable[[
|
|
50
|
-
|
|
75
|
+
def __call__[**P, R](
|
|
76
|
+
self, function: None = ..., *, firstresult: Literal[True], historic: bool = ...
|
|
77
|
+
) -> Callable[[Callable[P, R]], FirstResultSpec[P, R]]: ...
|
|
78
|
+
@overload
|
|
79
|
+
def __call__[**P, R](
|
|
80
|
+
self, function: None = ..., *, pipeline: Literal[True], historic: bool = ...
|
|
81
|
+
) -> Callable[[Callable[P, R]], PipelineSpec[P, R]]: ...
|
|
82
|
+
@overload
|
|
83
|
+
def __call__[**P, R](
|
|
84
|
+
self, function: None = ..., *, historic: bool = ...
|
|
85
|
+
) -> Callable[[Callable[P, R]], CollectingSpec[P, R]]: ...
|
|
51
86
|
def __call__(
|
|
52
87
|
self,
|
|
53
|
-
function:
|
|
88
|
+
function: Callable[..., Any] | None = None,
|
|
54
89
|
*,
|
|
55
90
|
firstresult: bool = False,
|
|
56
91
|
historic: bool = False,
|
|
57
92
|
pipeline: bool = False,
|
|
58
|
-
) ->
|
|
93
|
+
) -> Any:
|
|
59
94
|
"""Stamp HookspecOpts onto the function; supports bare and called forms."""
|
|
60
95
|
|
|
61
|
-
def mark(func:
|
|
96
|
+
def mark(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
62
97
|
setattr(func, self.attribute, HookspecOpts(firstresult=firstresult, historic=historic, pipeline=pipeline))
|
|
63
98
|
return func
|
|
64
99
|
|
|
@@ -74,10 +109,9 @@ class HookimplMarker:
|
|
|
74
109
|
self.attribute = f"{project_name}_impl"
|
|
75
110
|
|
|
76
111
|
@overload
|
|
77
|
-
def __call__(self, function: F) -> F: ...
|
|
78
|
-
|
|
112
|
+
def __call__[F: Callable[..., Any]](self, function: F) -> F: ...
|
|
79
113
|
@overload
|
|
80
|
-
def __call__(
|
|
114
|
+
def __call__[F: Callable[..., Any]](
|
|
81
115
|
self,
|
|
82
116
|
function: None = ...,
|
|
83
117
|
*,
|
|
@@ -87,8 +121,7 @@ class HookimplMarker:
|
|
|
87
121
|
optionalhook: bool = ...,
|
|
88
122
|
specname: str | None = ...,
|
|
89
123
|
) -> Callable[[F], F]: ...
|
|
90
|
-
|
|
91
|
-
def __call__(
|
|
124
|
+
def __call__[F: Callable[..., Any]](
|
|
92
125
|
self,
|
|
93
126
|
function: F | None = None,
|
|
94
127
|
*,
|
|
@@ -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
|
-
]
|
|
File without changes
|
|
File without changes
|