pluginkit 0.4.0__tar.gz → 0.4.2__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Morten Hansen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,14 +1,14 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: pluginkit
3
- Version: 0.4.0
3
+ Version: 0.4.2
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
7
7
  Author-email: Morten Hansen <morten@winterop.com>
8
- License: MIT
8
+ License-Expression: MIT
9
+ License-File: LICENSE
9
10
  Classifier: Development Status :: 4 - Beta
10
11
  Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
14
  Classifier: Typing :: Typed
@@ -38,7 +38,7 @@ hand-annotations and no drift. Zero runtime dependencies, a `py.typed` marker, a
38
38
  few readable files.
39
39
 
40
40
  ```bash
41
- pip install pluginkit # or: uv add pluginkit
41
+ uv add pluginkit # or: pip install pluginkit
42
42
  ```
43
43
 
44
44
  ```python
@@ -87,16 +87,17 @@ print(greetings) # ['hey Ada!']
87
87
  ```
88
88
  src/pluginkit/ the library (pure - no demo code)
89
89
  examples/ everything that uses the library (not shipped):
90
- recipes/ standalone single-file scripts, run directly
90
+ cookbook/ worked examples: bite-size scripts + full apps
91
91
  tour/ pluginkit-tour: a guided CLI walkthrough
92
92
  external-plugin/ a separate distribution discovered via entry points
93
93
  docs/ mkdocs + Material documentation
94
- tests/ library, tour, and recipe tests
94
+ tests/ library, tour, and cookbook tests
95
95
  ```
96
96
 
97
- Everything that demonstrates the library lives under `examples/`. The **recipes**
98
- are independent scripts you run on their own; the **tour** is a guided walkthrough
99
- on one host; the **external-plugin** shows cross-package discovery via entry points.
97
+ Everything that demonstrates the library lives under `examples/`. The **cookbook**
98
+ holds standalone examples (from one-mechanism snippets to complete FastAPI/Click/pytest
99
+ apps); the **tour** is a guided walkthrough on one host; the **external-plugin** shows
100
+ cross-package discovery via entry points.
100
101
 
101
102
  ## Use it
102
103
 
@@ -118,14 +119,14 @@ make run DEMO=wrapper # run one
118
119
  uv run pluginkit-tour list
119
120
  ```
120
121
 
121
- **The recipes** apply the library to different realistic domains - see
122
- [`examples/`](examples/README.md):
122
+ **The cookbook** applies the library to realistic domains and frameworks - see
123
+ [`examples/cookbook/`](examples/cookbook/README.md):
123
124
 
124
125
  ```bash
125
- uv run python examples/recipes/report_builder.py
126
- uv run python examples/recipes/notification_router.py
127
- uv run python examples/recipes/validation_rules.py
128
- uv run python examples/recipes/app_lifecycle.py
126
+ uv run python examples/cookbook/report_builder.py
127
+ uv run python examples/cookbook/fastapi_app.py
128
+ uv run python examples/cookbook/cli_app.py --help
129
+ uv run python examples/cookbook/app_lifecycle.py
129
130
  ```
130
131
 
131
132
  ## Documentation
@@ -17,7 +17,7 @@ hand-annotations and no drift. Zero runtime dependencies, a `py.typed` marker, a
17
17
  few readable files.
18
18
 
19
19
  ```bash
20
- pip install pluginkit # or: uv add pluginkit
20
+ uv add pluginkit # or: pip install pluginkit
21
21
  ```
22
22
 
23
23
  ```python
@@ -66,16 +66,17 @@ print(greetings) # ['hey Ada!']
66
66
  ```
67
67
  src/pluginkit/ the library (pure - no demo code)
68
68
  examples/ everything that uses the library (not shipped):
69
- recipes/ standalone single-file scripts, run directly
69
+ cookbook/ worked examples: bite-size scripts + full apps
70
70
  tour/ pluginkit-tour: a guided CLI walkthrough
71
71
  external-plugin/ a separate distribution discovered via entry points
72
72
  docs/ mkdocs + Material documentation
73
- tests/ library, tour, and recipe tests
73
+ tests/ library, tour, and cookbook tests
74
74
  ```
75
75
 
76
- Everything that demonstrates the library lives under `examples/`. The **recipes**
77
- are independent scripts you run on their own; the **tour** is a guided walkthrough
78
- on one host; the **external-plugin** shows cross-package discovery via entry points.
76
+ Everything that demonstrates the library lives under `examples/`. The **cookbook**
77
+ holds standalone examples (from one-mechanism snippets to complete FastAPI/Click/pytest
78
+ apps); the **tour** is a guided walkthrough on one host; the **external-plugin** shows
79
+ cross-package discovery via entry points.
79
80
 
80
81
  ## Use it
81
82
 
@@ -97,14 +98,14 @@ make run DEMO=wrapper # run one
97
98
  uv run pluginkit-tour list
98
99
  ```
99
100
 
100
- **The recipes** apply the library to different realistic domains - see
101
- [`examples/`](examples/README.md):
101
+ **The cookbook** applies the library to realistic domains and frameworks - see
102
+ [`examples/cookbook/`](examples/cookbook/README.md):
102
103
 
103
104
  ```bash
104
- uv run python examples/recipes/report_builder.py
105
- uv run python examples/recipes/notification_router.py
106
- uv run python examples/recipes/validation_rules.py
107
- uv run python examples/recipes/app_lifecycle.py
105
+ uv run python examples/cookbook/report_builder.py
106
+ uv run python examples/cookbook/fastapi_app.py
107
+ uv run python examples/cookbook/cli_app.py --help
108
+ uv run python examples/cookbook/app_lifecycle.py
108
109
  ```
109
110
 
110
111
  ## Documentation
@@ -1,16 +1,16 @@
1
1
  [project]
2
2
  name = "pluginkit"
3
- version = "0.4.0"
3
+ version = "0.4.2"
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" }]
7
- license = { text = "MIT" }
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
8
9
  requires-python = ">=3.13"
9
10
  keywords = ["plugins", "hooks", "protocol", "entry-points"]
10
11
  classifiers = [
11
12
  "Development Status :: 4 - Beta",
12
13
  "Intended Audience :: Developers",
13
- "License :: OSI Approved :: MIT License",
14
14
  "Programming Language :: Python :: 3.13",
15
15
  "Topic :: Software Development :: Libraries :: Python Modules",
16
16
  "Typing :: Typed",
@@ -79,7 +79,7 @@ docstring-code-line-length = "dynamic"
79
79
 
80
80
  [tool.pytest.ini_options]
81
81
  testpaths = ["tests"]
82
- pythonpath = ["examples/recipes", "examples/integrations"]
82
+ pythonpath = ["examples/cookbook"]
83
83
  norecursedirs = [".git", ".venv", "__pycache__"]
84
84
 
85
85
  [tool.mypy]
@@ -91,7 +91,7 @@ check_untyped_defs = true
91
91
  no_implicit_optional = true
92
92
  warn_unused_ignores = true
93
93
  strict_equality = true
94
- mypy_path = ["src", "examples/tour/src", "examples/recipes", "examples/integrations"]
94
+ mypy_path = ["src", "examples/tour/src", "examples/cookbook"]
95
95
 
96
96
  [[tool.mypy.overrides]]
97
97
  module = "tests.*"
@@ -105,8 +105,8 @@ module = "pluginkit_tour.points"
105
105
  disable_error_code = ["empty-body"]
106
106
 
107
107
  [tool.pyright]
108
- include = ["src", "tests", "examples/recipes", "examples/integrations", "examples/tour/src"]
109
- extraPaths = ["examples/recipes", "examples/integrations", "examples/tour/src"]
108
+ include = ["src", "tests", "examples/cookbook", "examples/tour/src"]
109
+ extraPaths = ["examples/cookbook", "examples/tour/src"]
110
110
  exclude = ["**/.venv"]
111
111
  pythonVersion = "3.13"
112
112
  typeCheckingMode = "strict"
@@ -29,11 +29,11 @@ from pluginkit.markers import (
29
29
  class AsyncHookCaller(HookCaller):
30
30
  """A HookCaller whose calls are coroutines that await async implementations."""
31
31
 
32
- async def __call__(self, **kwargs: Any) -> Any:
32
+ async def __call__(self, *args: Any, **kwargs: Any) -> Any:
33
33
  """Await the hook: a list, a single value (firstresult), or the threaded value (pipeline)."""
34
34
  if self.spec.historic:
35
35
  raise TypeError(f"historic hook {self.name!r} must be called via call_historic()")
36
- kwargs = self.check_arguments(kwargs)
36
+ kwargs = self.check_arguments(self._bind(args, kwargs))
37
37
  return await self._execute_async(kwargs, self._nonwrappers)
38
38
 
39
39
  async def call_extra(self, functions: list[Callable[..., Any]], kwargs: dict[str, Any]) -> Any:
@@ -160,11 +160,27 @@ class HookCaller:
160
160
  extra.append(impl)
161
161
  return extra
162
162
 
163
- def __call__(self, **kwargs: Any) -> Any:
163
+ def _bind(self, args: tuple[Any, ...], kwargs: dict[str, Any]) -> dict[str, Any]:
164
+ """Bind positional args to the spec's params (in order) and merge with kwargs.
165
+
166
+ Lets a typed caller be invoked positionally - `caller(value)` as well as
167
+ `caller(name=value)` - matching what the ParamSpec advertises.
168
+ """
169
+ if not args:
170
+ 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))
174
+ clash = positional.keys() & kwargs.keys()
175
+ if clash:
176
+ raise TypeError(f"hook {self.name!r} got multiple values for {sorted(clash)}")
177
+ return {**positional, **kwargs}
178
+
179
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
164
180
  """Call the hook: a list, a single value (firstresult), or the threaded value (pipeline)."""
165
181
  if self.spec.historic:
166
182
  raise TypeError(f"historic hook {self.name!r} must be called via call_historic()")
167
- kwargs = self.check_arguments(kwargs)
183
+ kwargs = self.check_arguments(self._bind(args, kwargs))
168
184
  return self._execute(kwargs, self._nonwrappers)
169
185
 
170
186
  def call_extra(self, functions: list[Callable[..., Any]], kwargs: dict[str, Any]) -> Any: