pluginkit 0.4.2__tar.gz → 0.4.4__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.4.2 → pluginkit-0.4.4}/PKG-INFO +1 -1
- {pluginkit-0.4.2 → pluginkit-0.4.4}/pyproject.toml +1 -1
- {pluginkit-0.4.2 → pluginkit-0.4.4}/src/pluginkit/aio.py +8 -3
- {pluginkit-0.4.2 → pluginkit-0.4.4}/src/pluginkit/manager.py +47 -8
- {pluginkit-0.4.2 → pluginkit-0.4.4}/LICENSE +0 -0
- {pluginkit-0.4.2 → pluginkit-0.4.4}/README.md +0 -0
- {pluginkit-0.4.2 → pluginkit-0.4.4}/src/pluginkit/__init__.py +0 -0
- {pluginkit-0.4.2 → pluginkit-0.4.4}/src/pluginkit/exceptions.py +0 -0
- {pluginkit-0.4.2 → pluginkit-0.4.4}/src/pluginkit/markers.py +0 -0
- {pluginkit-0.4.2 → pluginkit-0.4.4}/src/pluginkit/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pluginkit"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.4"
|
|
4
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" }]
|
|
@@ -145,10 +145,15 @@ class AsyncPluginManager(PluginManager):
|
|
|
145
145
|
"""A PluginManager whose hooks are awaited; impls may be coroutine functions."""
|
|
146
146
|
|
|
147
147
|
def _make_caller(
|
|
148
|
-
self, name: str, spec: ExtensionPointOpts, params: tuple[str, ...], defaults: dict[str, Any]
|
|
148
|
+
self, name: str, spec: ExtensionPointOpts, params: tuple[str, ...], defaults: dict[str, Any], arity: int
|
|
149
149
|
) -> HookCaller:
|
|
150
|
-
"""Build an AsyncHookCaller
|
|
151
|
-
|
|
150
|
+
"""Build an AsyncHookCaller; historic extension points are not supported async-side."""
|
|
151
|
+
if spec.historic:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"historic extension point {name!r} is not supported by AsyncPluginManager "
|
|
154
|
+
f"(historic replay requires synchronous dispatch)"
|
|
155
|
+
)
|
|
156
|
+
return AsyncHookCaller(name=name, spec=spec, params=params, defaults=defaults, positional_arity=arity)
|
|
152
157
|
|
|
153
158
|
@overload # type: ignore[override] # async manager returns awaitable callers
|
|
154
159
|
def caller[**P, R](self, spec: FirstResultSpec[P, R]) -> AsyncFirstResultCaller[P, R]: ...
|
|
@@ -38,6 +38,30 @@ from pluginkit.markers import (
|
|
|
38
38
|
# Sentinel distinguishing "no result yet" from a legitimate None result.
|
|
39
39
|
_UNSET: Any = object()
|
|
40
40
|
|
|
41
|
+
# pluginkit dispatches by keyword (each impl receives the subset of named arguments it
|
|
42
|
+
# declares), so every parameter must be addressable by name.
|
|
43
|
+
_KEYWORD_DISPATCHABLE = frozenset({inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _positional_arity(signature: inspect.Signature, label: str, name: str) -> int:
|
|
47
|
+
"""Validate that every parameter can be passed by keyword and return the positional arity.
|
|
48
|
+
|
|
49
|
+
Rejects positional-only, `*args`, and `**kwargs` parameters (none are addressable by a
|
|
50
|
+
fixed name), and returns how many leading parameters may *also* be passed positionally -
|
|
51
|
+
so a positional call binds exactly the arguments the ParamSpec allows positionally.
|
|
52
|
+
"""
|
|
53
|
+
arity = 0
|
|
54
|
+
for param_name, parameter in signature.parameters.items():
|
|
55
|
+
if parameter.kind not in _KEYWORD_DISPATCHABLE:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"{label} {name!r} parameter {param_name!r} is {parameter.kind.description}; "
|
|
58
|
+
f"pluginkit dispatches by keyword, so only positional-or-keyword and keyword-only "
|
|
59
|
+
f"parameters are supported"
|
|
60
|
+
)
|
|
61
|
+
if parameter.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
|
62
|
+
arity += 1
|
|
63
|
+
return arity
|
|
64
|
+
|
|
41
65
|
|
|
42
66
|
@dataclass(slots=True)
|
|
43
67
|
class HookImpl:
|
|
@@ -55,7 +79,9 @@ class HookImpl:
|
|
|
55
79
|
@classmethod
|
|
56
80
|
def from_function(cls, plugin_name: str, function: Callable[..., Any], opts: ExtensionOpts) -> Self:
|
|
57
81
|
"""Build an impl, recording which keyword arguments the function declares."""
|
|
58
|
-
|
|
82
|
+
signature = inspect.signature(function)
|
|
83
|
+
_positional_arity(signature, "implementation", getattr(function, "__qualname__", plugin_name))
|
|
84
|
+
params = tuple(signature.parameters)
|
|
59
85
|
return cls(plugin_name=plugin_name, function=function, opts=opts, accepts=frozenset(params), params=params)
|
|
60
86
|
|
|
61
87
|
def call(self, kwargs: dict[str, Any]) -> Any:
|
|
@@ -93,6 +119,9 @@ class HookCaller:
|
|
|
93
119
|
# branded caller's ParamSpec makes them optional); they are filled in at call
|
|
94
120
|
# time so the type checker and the runtime agree.
|
|
95
121
|
defaults: dict[str, Any] = field(default_factory=dict)
|
|
122
|
+
# How many leading params may be passed positionally (the rest are keyword-only);
|
|
123
|
+
# -1 means "derive as all params" for callers built directly without kind info.
|
|
124
|
+
positional_arity: int = -1
|
|
96
125
|
_impls: list[HookImpl] = field(default_factory=list)
|
|
97
126
|
_wrappers: list[HookImpl] = field(default_factory=list)
|
|
98
127
|
_nonwrappers: list[HookImpl] = field(default_factory=list)
|
|
@@ -102,6 +131,8 @@ class HookCaller:
|
|
|
102
131
|
"""Derive the argument-name set from the ordered parameters when given."""
|
|
103
132
|
if self.params and not self.argnames:
|
|
104
133
|
self.argnames = frozenset(self.params)
|
|
134
|
+
if self.positional_arity < 0:
|
|
135
|
+
self.positional_arity = len(self.params)
|
|
105
136
|
|
|
106
137
|
def check_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
107
138
|
"""Fill any omitted defaulted args, then validate the call against the spec.
|
|
@@ -168,9 +199,9 @@ class HookCaller:
|
|
|
168
199
|
"""
|
|
169
200
|
if not args:
|
|
170
201
|
return kwargs
|
|
171
|
-
if len(args) >
|
|
172
|
-
raise TypeError(f"hook {self.name!r} takes at most {
|
|
173
|
-
positional = dict(zip(self.params, args, strict=False))
|
|
202
|
+
if len(args) > self.positional_arity:
|
|
203
|
+
raise TypeError(f"hook {self.name!r} takes at most {self.positional_arity} positional argument(s)")
|
|
204
|
+
positional = dict(zip(self.params[: self.positional_arity], args, strict=False))
|
|
174
205
|
clash = positional.keys() & kwargs.keys()
|
|
175
206
|
if clash:
|
|
176
207
|
raise TypeError(f"hook {self.name!r} got multiple values for {sorted(clash)}")
|
|
@@ -203,7 +234,9 @@ class HookCaller:
|
|
|
203
234
|
if not self.spec.historic:
|
|
204
235
|
raise TypeError(f"hook {self.name!r} is not historic")
|
|
205
236
|
kwargs = self.check_arguments(kwargs)
|
|
206
|
-
|
|
237
|
+
# Snapshot the event so a later mutation of the caller's dict can't change
|
|
238
|
+
# what plugins registered after this call replay.
|
|
239
|
+
self._history.append((dict(kwargs), result_callback))
|
|
207
240
|
for outcome in self._collect(kwargs):
|
|
208
241
|
if result_callback is not None:
|
|
209
242
|
result_callback(outcome)
|
|
@@ -405,20 +438,26 @@ class PluginManager:
|
|
|
405
438
|
if not isinstance(spec, ExtensionPointOpts):
|
|
406
439
|
continue
|
|
407
440
|
signature = inspect.signature(member)
|
|
441
|
+
arity = _positional_arity(signature, "extension point", member_name)
|
|
408
442
|
params = tuple(signature.parameters)
|
|
409
443
|
defaults = {
|
|
410
444
|
name: parameter.default
|
|
411
445
|
for name, parameter in signature.parameters.items()
|
|
412
446
|
if parameter.default is not inspect.Parameter.empty
|
|
413
447
|
}
|
|
448
|
+
if self.hook._get_caller(member_name) is not None:
|
|
449
|
+
raise ValueError(
|
|
450
|
+
f"extension point {member_name!r} is already registered; "
|
|
451
|
+
f"re-adding it would drop the implementations already wired to it"
|
|
452
|
+
)
|
|
414
453
|
self._validate_spec(member_name, spec, params)
|
|
415
|
-
self.hook._add_caller(self._make_caller(member_name, spec, params, defaults))
|
|
454
|
+
self.hook._add_caller(self._make_caller(member_name, spec, params, defaults, arity))
|
|
416
455
|
|
|
417
456
|
def _make_caller(
|
|
418
|
-
self, name: str, spec: ExtensionPointOpts, params: tuple[str, ...], defaults: dict[str, Any]
|
|
457
|
+
self, name: str, spec: ExtensionPointOpts, params: tuple[str, ...], defaults: dict[str, Any], arity: int
|
|
419
458
|
) -> HookCaller:
|
|
420
459
|
"""Build the caller for a spec; overridden by AsyncPluginManager."""
|
|
421
|
-
return HookCaller(name=name, spec=spec, params=params, defaults=defaults)
|
|
460
|
+
return HookCaller(name=name, spec=spec, params=params, defaults=defaults, positional_arity=arity)
|
|
422
461
|
|
|
423
462
|
@overload
|
|
424
463
|
def caller[**P, R](self, spec: FirstResultSpec[P, R]) -> FirstResultCaller[P, R]: ...
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|