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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pluginkit
3
- Version: 0.2.0
4
- Summary: A small, dependency-free plugin framework: hook specs, entry-point discovery, and sync/async dispatch
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.11
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
  [![Docs](https://img.shields.io/badge/docs-pages-blue.svg)](https://winterop-com.github.io/pluginkit/)
30
28
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
31
29
 
32
- A small, **dependency-free** plugin framework for Python: declare hook
33
- specifications, let plugins implement them, and discover plugins via entry points.
34
- Supports sync and async dispatch, hook ordering, wrappers, pipeline (fold)
35
- dispatch, and historic hooks - in a few readable files.
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
- The library is three files under `src/pluginkit/` (`markers.py`, `manager.py`,
38
- `exceptions.py`), has **zero runtime dependencies** (standard library only), runs on
39
- **Python 3.11+**, and ships a `py.typed` marker.
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 HookspecMarker, HookimplMarker, PluginManager
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
- print(pm.hook.greeting(name="Ada")) # ['hey Ada!']
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
  [![Docs](https://img.shields.io/badge/docs-pages-blue.svg)](https://winterop-com.github.io/pluginkit/)
7
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
8
8
 
9
- A small, **dependency-free** plugin framework for Python: declare hook
10
- specifications, let plugins implement them, and discover plugins via entry points.
11
- Supports sync and async dispatch, hook ordering, wrappers, pipeline (fold)
12
- dispatch, and historic hooks - in a few readable files.
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
- The library is three files under `src/pluginkit/` (`markers.py`, `manager.py`,
15
- `exceptions.py`), has **zero runtime dependencies** (standard library only), runs on
16
- **Python 3.11+**, and ships a `py.typed` marker.
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 HookspecMarker, HookimplMarker, PluginManager
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
- print(pm.hook.greeting(name="Ada")) # ['hey Ada!']
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.2.0"
4
- description = "A small, dependency-free plugin framework: hook specs, entry-point discovery, and sync/async dispatch"
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.11"
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 = "py311"
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.11"
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.11"
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 HookimplOpts, HookspecOpts
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 HookimplOpts, HookspecOpts
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
- 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.
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, TypeVar, overload
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: F) -> F: ...
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: bool = ..., historic: bool = ..., pipeline: bool = ...
49
- ) -> Callable[[F], F]: ...
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: F | None = None,
88
+ function: Callable[..., Any] | None = None,
54
89
  *,
55
90
  firstresult: bool = False,
56
91
  historic: bool = False,
57
92
  pipeline: bool = False,
58
- ) -> F | Callable[[F], F]:
93
+ ) -> Any:
59
94
  """Stamp HookspecOpts onto the function; supports bare and called forms."""
60
95
 
61
- def mark(func: F) -> F:
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
- ]