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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pluginkit
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pluginkit"
3
- version = "0.4.3"
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 instead of the synchronous one."""
151
- return AsyncHookCaller(name=name, spec=spec, params=params, defaults=defaults)
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
- params = tuple(inspect.signature(function).parameters)
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) > len(self.params):
172
- raise TypeError(f"hook {self.name!r} takes at most {len(self.params)} positional argument(s)")
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