pluginkit 0.4.3__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.3 → pluginkit-0.4.4}/PKG-INFO +1 -1
- {pluginkit-0.4.3 → pluginkit-0.4.4}/pyproject.toml +1 -1
- {pluginkit-0.4.3 → pluginkit-0.4.4}/src/pluginkit/aio.py +8 -3
- {pluginkit-0.4.3 → pluginkit-0.4.4}/src/pluginkit/manager.py +39 -7
- {pluginkit-0.4.3 → pluginkit-0.4.4}/LICENSE +0 -0
- {pluginkit-0.4.3 → pluginkit-0.4.4}/README.md +0 -0
- {pluginkit-0.4.3 → pluginkit-0.4.4}/src/pluginkit/__init__.py +0 -0
- {pluginkit-0.4.3 → pluginkit-0.4.4}/src/pluginkit/exceptions.py +0 -0
- {pluginkit-0.4.3 → pluginkit-0.4.4}/src/pluginkit/markers.py +0 -0
- {pluginkit-0.4.3 → 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)}")
|
|
@@ -407,6 +438,7 @@ class PluginManager:
|
|
|
407
438
|
if not isinstance(spec, ExtensionPointOpts):
|
|
408
439
|
continue
|
|
409
440
|
signature = inspect.signature(member)
|
|
441
|
+
arity = _positional_arity(signature, "extension point", member_name)
|
|
410
442
|
params = tuple(signature.parameters)
|
|
411
443
|
defaults = {
|
|
412
444
|
name: parameter.default
|
|
@@ -419,13 +451,13 @@ class PluginManager:
|
|
|
419
451
|
f"re-adding it would drop the implementations already wired to it"
|
|
420
452
|
)
|
|
421
453
|
self._validate_spec(member_name, spec, params)
|
|
422
|
-
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))
|
|
423
455
|
|
|
424
456
|
def _make_caller(
|
|
425
|
-
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
|
|
426
458
|
) -> HookCaller:
|
|
427
459
|
"""Build the caller for a spec; overridden by AsyncPluginManager."""
|
|
428
|
-
return HookCaller(name=name, spec=spec, params=params, defaults=defaults)
|
|
460
|
+
return HookCaller(name=name, spec=spec, params=params, defaults=defaults, positional_arity=arity)
|
|
429
461
|
|
|
430
462
|
@overload
|
|
431
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
|