wireme 0.1.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.
- wireme-0.1.0/LICENSE +21 -0
- wireme-0.1.0/PKG-INFO +401 -0
- wireme-0.1.0/README.md +375 -0
- wireme-0.1.0/pyproject.toml +70 -0
- wireme-0.1.0/src/wireme/__init__.py +13 -0
- wireme-0.1.0/src/wireme/_core.py +11 -0
- wireme-0.1.0/src/wireme/_errors.py +4 -0
- wireme-0.1.0/src/wireme/_impl.py +258 -0
- wireme-0.1.0/src/wireme/py.typed +0 -0
wireme-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohanad Ghali
|
|
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.
|
wireme-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wireme
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tiny, typed dependency injection built on FastDepends.
|
|
5
|
+
Keywords: dependency-injection,di,fast-depends,typing
|
|
6
|
+
Author: Mohanad Ghali
|
|
7
|
+
Author-email: Mohanad Ghali <mghalix@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: fast-depends[pydantic]>=3.0.8,<4
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Project-URL: Homepage, https://github.com/mghalix/wireme
|
|
22
|
+
Project-URL: Repository, https://github.com/mghalix/wireme
|
|
23
|
+
Project-URL: Issues, https://github.com/mghalix/wireme/issues
|
|
24
|
+
Project-URL: Changelog, https://github.com/mghalix/wireme/blob/main/CHANGELOG.md
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# wireme
|
|
28
|
+
|
|
29
|
+
Tiny, typed dependency injection for Python, powered by FastDepends.
|
|
30
|
+
|
|
31
|
+
Wireme keeps dependency injection explicit and small:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from wireme import Wired, override_dependency, wire, wired
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- `@wire` enables dependency resolution for a function or method.
|
|
38
|
+
- `wired(factory)` declares how a dependency is created.
|
|
39
|
+
- `Wired()` marks a reusable `Annotated` dependency as caller-optional.
|
|
40
|
+
- `override_dependency()` temporarily replaces a dependency in tests.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv add wireme
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Wireme requires Python 3.12 or newer.
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from wireme import wire, wired
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Database:
|
|
57
|
+
def write(self, value: str) -> None:
|
|
58
|
+
print(f"writing: {value}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_database() -> Database:
|
|
62
|
+
return Database()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@wire
|
|
66
|
+
def process_text(
|
|
67
|
+
text: str,
|
|
68
|
+
database: Database = wired(get_database),
|
|
69
|
+
) -> None:
|
|
70
|
+
database.write(text)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
process_text("Hello, world")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Callers only provide application inputs. Wireme resolves `database` automatically.
|
|
77
|
+
Injected parameters are also removed from the public runtime signature:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import inspect
|
|
81
|
+
|
|
82
|
+
assert str(inspect.signature(process_text)) == "(text: str) -> None"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Reusable dependencies
|
|
86
|
+
|
|
87
|
+
Use `Annotated` when the same dependency appears in multiple callables:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from typing import Annotated
|
|
91
|
+
|
|
92
|
+
from wireme import Wired, wire, wired
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
type DatabaseDep = Annotated[Database, wired(get_database)]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@wire
|
|
99
|
+
def create_user(
|
|
100
|
+
username: str,
|
|
101
|
+
database: DatabaseDep = Wired(),
|
|
102
|
+
) -> None:
|
|
103
|
+
database.write(username)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
create_user("mo")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`wired(get_database)` stores the dependency declaration in the annotation.
|
|
110
|
+
`Wired()` tells type checkers and call sites that the argument does not need to be
|
|
111
|
+
passed explicitly.
|
|
112
|
+
|
|
113
|
+
## Wiring classes
|
|
114
|
+
|
|
115
|
+
Decorate constructors or methods individually. Do not decorate the class itself.
|
|
116
|
+
|
|
117
|
+
Use constructor injection when the dependency belongs to the object's state:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from typing import Annotated
|
|
121
|
+
|
|
122
|
+
from wireme import Wired, wire, wired
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
type DatabaseDep = Annotated[Database, wired(get_database)]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TextProcessor:
|
|
129
|
+
@wire
|
|
130
|
+
def __init__(self, database: DatabaseDep = Wired()) -> None:
|
|
131
|
+
self._database = database
|
|
132
|
+
|
|
133
|
+
def process(self, text: str) -> None:
|
|
134
|
+
self._database.write(text)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
TextProcessor().process("Hello from constructor injection")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Use method injection when only a specific operation needs the dependency:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
class TextProcessor:
|
|
144
|
+
@wire
|
|
145
|
+
def process(
|
|
146
|
+
self,
|
|
147
|
+
text: str,
|
|
148
|
+
database: DatabaseDep = Wired(),
|
|
149
|
+
) -> None:
|
|
150
|
+
database.write(text)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
TextProcessor().process("Hello from method injection")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Constructor injection is useful for dependencies shared across multiple methods.
|
|
157
|
+
Method injection keeps the object stateless when only one operation needs the
|
|
158
|
+
dependency.
|
|
159
|
+
|
|
160
|
+
See [examples/classes.py](https://github.com/mghalix/wireme/blob/main/examples/classes.py) for a complete runnable example.
|
|
161
|
+
|
|
162
|
+
## Explicit values take precedence
|
|
163
|
+
|
|
164
|
+
A caller may still provide a dependency explicitly. The passed value wins over
|
|
165
|
+
injection:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
class TestDatabase(Database):
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
create_user("mo", TestDatabase())
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
This is useful for one-off composition. For test suites, prefer
|
|
176
|
+
`override_dependency()` so every nested dependency sees the replacement.
|
|
177
|
+
|
|
178
|
+
## Test overrides
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from wireme import override_dependency
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_test_database() -> Database:
|
|
185
|
+
return TestDatabase()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
with override_dependency(get_database, get_test_database):
|
|
189
|
+
create_user("mo")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Overrides are restored when the context exits, including after exceptions.
|
|
193
|
+
Nested overrides restore the previous outer override correctly.
|
|
194
|
+
|
|
195
|
+
The provider is shared at process level. Use overrides for isolated tests and
|
|
196
|
+
application setup, not concurrent request-level mutation.
|
|
197
|
+
|
|
198
|
+
## Nested dependencies and caching
|
|
199
|
+
|
|
200
|
+
Factories can depend on other factories:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from wireme import wire, wired
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_settings() -> Settings:
|
|
207
|
+
return Settings()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_database(
|
|
211
|
+
settings: Settings = wired(get_settings),
|
|
212
|
+
) -> Database:
|
|
213
|
+
return Database(settings.database_url)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@wire
|
|
217
|
+
def operation(database: Database = wired(get_database)) -> None:
|
|
218
|
+
...
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Dependencies are cached once per wired call by default. Disable caching for a
|
|
222
|
+
specific declaration when every use must create a new value:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
value: Token = wired(create_token, use_cache=False)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Async and resource dependencies
|
|
229
|
+
|
|
230
|
+
Async factories are supported:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
async def get_client() -> Client:
|
|
234
|
+
return await Client.connect()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@wire
|
|
238
|
+
async def fetch_user(
|
|
239
|
+
user_id: str,
|
|
240
|
+
client: Client = wired(get_client),
|
|
241
|
+
) -> User:
|
|
242
|
+
return await client.fetch_user(user_id)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Generator and async-generator factories can own resource cleanup:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from collections.abc import AsyncIterator
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
async def get_client() -> AsyncIterator[Client]:
|
|
252
|
+
client = await Client.connect()
|
|
253
|
+
try:
|
|
254
|
+
yield client
|
|
255
|
+
finally:
|
|
256
|
+
await client.close()
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Cleanup runs after the wired callable finishes.
|
|
260
|
+
|
|
261
|
+
## Protocol dependencies
|
|
262
|
+
|
|
263
|
+
A protocol can describe the dependency interface. When runtime validation is
|
|
264
|
+
active, make the protocol runtime-checkable:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
from typing import Annotated, Protocol, runtime_checkable
|
|
268
|
+
|
|
269
|
+
from wireme import Wired, wire, wired
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@runtime_checkable
|
|
273
|
+
class DatabaseLike(Protocol):
|
|
274
|
+
def write(self, value: str) -> None: ...
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
type DatabaseDep = Annotated[DatabaseLike, wired(get_database)]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@wire
|
|
281
|
+
def process(
|
|
282
|
+
value: str,
|
|
283
|
+
database: DatabaseDep = Wired(),
|
|
284
|
+
) -> None:
|
|
285
|
+
database.write(value)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Build integrations on top of Wireme
|
|
289
|
+
|
|
290
|
+
Wireme can be the small DI primitive behind a project-specific API. For example,
|
|
291
|
+
a service or plugin registry can expose `wired_service(ServiceType)` while still
|
|
292
|
+
using Wireme for resolution, typing, caching, and overrides.
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
import functools
|
|
296
|
+
|
|
297
|
+
from collections.abc import Callable
|
|
298
|
+
from typing import cast
|
|
299
|
+
|
|
300
|
+
from wireme import WiremeError, wired
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class ServiceUnavailableError(WiremeError):
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
_services: dict[type[object], object] = {}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@functools.cache
|
|
311
|
+
def require_service[T](service_type: type[T]) -> Callable[[], T]:
|
|
312
|
+
def dependency() -> T:
|
|
313
|
+
try:
|
|
314
|
+
service = _services[service_type]
|
|
315
|
+
except KeyError as error:
|
|
316
|
+
raise ServiceUnavailableError(
|
|
317
|
+
f"{service_type.__name__} is not registered."
|
|
318
|
+
) from error
|
|
319
|
+
|
|
320
|
+
return cast(T, service)
|
|
321
|
+
|
|
322
|
+
return dependency
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def wired_service[T](service_type: type[T]) -> T:
|
|
326
|
+
return wired(require_service(service_type))
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Caching the generated factory gives it stable identity, which is important when
|
|
330
|
+
using `override_dependency()`.
|
|
331
|
+
|
|
332
|
+
See [`examples/custom_integration.py`](https://github.com/mghalix/wireme/blob/main/examples/custom_integration.py) for a
|
|
333
|
+
complete runnable example.
|
|
334
|
+
|
|
335
|
+
## Errors
|
|
336
|
+
|
|
337
|
+
Wireme exposes:
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
from wireme import ValidationError, WiremeError
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
- `WiremeError` is the base error exposed by Wireme.
|
|
344
|
+
- `ValidationError` represents dependency input or result validation failures.
|
|
345
|
+
|
|
346
|
+
Project-specific DI errors may inherit from `WiremeError`.
|
|
347
|
+
|
|
348
|
+
## Ruff configuration
|
|
349
|
+
|
|
350
|
+
Ruff's `B008` rule normally rejects function calls in defaults. Tell Ruff that
|
|
351
|
+
`Wired()` is an immutable marker:
|
|
352
|
+
|
|
353
|
+
```toml
|
|
354
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
355
|
+
extend-immutable-calls = ["wireme.Wired"]
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Declarations using `wired(factory)` may also need your project's normal DI rule
|
|
359
|
+
configuration, depending on which Ruff rules are enabled.
|
|
360
|
+
|
|
361
|
+
## Public API
|
|
362
|
+
|
|
363
|
+
| Name | Purpose |
|
|
364
|
+
| --------------------- | --------------------------------------------------------------- |
|
|
365
|
+
| `wire` | Decorate a function or method and enable dependency resolution. |
|
|
366
|
+
| `wired` | Declare a dependency factory and its resolution options. |
|
|
367
|
+
| `Wired` | Mark an `Annotated` dependency as caller-optional. |
|
|
368
|
+
| `override_dependency` | Temporarily replace a factory, with nested restoration. |
|
|
369
|
+
| `WiremeError` | Base error exposed by Wireme. |
|
|
370
|
+
| `ValidationError` | Dependency validation error. |
|
|
371
|
+
|
|
372
|
+
## Examples
|
|
373
|
+
|
|
374
|
+
- [`examples/basic.py`](https://github.com/mghalix/wireme/blob/main/examples/basic.py) covers direct and reusable dependencies.
|
|
375
|
+
- [`examples/classes.py`](https://github.com/mghalix/wireme/blob/main/examples/classes.py) covers constructor and method injection.
|
|
376
|
+
- [`examples/overrides.py`](https://github.com/mghalix/wireme/blob/main/examples/overrides.py) covers isolated test overrides.
|
|
377
|
+
- [`examples/protocols.py`](https://github.com/mghalix/wireme/blob/main/examples/protocols.py) covers interface-based dependencies.
|
|
378
|
+
- [`examples/resources.py`](https://github.com/mghalix/wireme/blob/main/examples/resources.py) covers async resource cleanup.
|
|
379
|
+
- [`examples/custom_integration.py`](https://github.com/mghalix/wireme/blob/main/examples/custom_integration.py) builds a typed service registry integration.
|
|
380
|
+
|
|
381
|
+
Run any example with:
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
uv run python examples/basic.py
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Why Wireme instead of importing FastDepends directly?
|
|
388
|
+
|
|
389
|
+
FastDepends provides the resolution engine. Wireme provides a deliberately small,
|
|
390
|
+
opinionated facade with:
|
|
391
|
+
|
|
392
|
+
- A cohesive `wire`, `wired`, and `Wired` vocabulary.
|
|
393
|
+
- Strong return typing for sync, async, generator, and async-generator factories.
|
|
394
|
+
- Reusable `Annotated` dependencies.
|
|
395
|
+
- Injected parameters hidden from public runtime signatures.
|
|
396
|
+
- Nested-safe dependency overrides.
|
|
397
|
+
- A minimal backend-independent public namespace.
|
|
398
|
+
|
|
399
|
+
## License
|
|
400
|
+
|
|
401
|
+
MIT
|
wireme-0.1.0/README.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# wireme
|
|
2
|
+
|
|
3
|
+
Tiny, typed dependency injection for Python, powered by FastDepends.
|
|
4
|
+
|
|
5
|
+
Wireme keeps dependency injection explicit and small:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from wireme import Wired, override_dependency, wire, wired
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- `@wire` enables dependency resolution for a function or method.
|
|
12
|
+
- `wired(factory)` declares how a dependency is created.
|
|
13
|
+
- `Wired()` marks a reusable `Annotated` dependency as caller-optional.
|
|
14
|
+
- `override_dependency()` temporarily replaces a dependency in tests.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv add wireme
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Wireme requires Python 3.12 or newer.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from wireme import wire, wired
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Database:
|
|
31
|
+
def write(self, value: str) -> None:
|
|
32
|
+
print(f"writing: {value}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_database() -> Database:
|
|
36
|
+
return Database()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@wire
|
|
40
|
+
def process_text(
|
|
41
|
+
text: str,
|
|
42
|
+
database: Database = wired(get_database),
|
|
43
|
+
) -> None:
|
|
44
|
+
database.write(text)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
process_text("Hello, world")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Callers only provide application inputs. Wireme resolves `database` automatically.
|
|
51
|
+
Injected parameters are also removed from the public runtime signature:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import inspect
|
|
55
|
+
|
|
56
|
+
assert str(inspect.signature(process_text)) == "(text: str) -> None"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Reusable dependencies
|
|
60
|
+
|
|
61
|
+
Use `Annotated` when the same dependency appears in multiple callables:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from typing import Annotated
|
|
65
|
+
|
|
66
|
+
from wireme import Wired, wire, wired
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
type DatabaseDep = Annotated[Database, wired(get_database)]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@wire
|
|
73
|
+
def create_user(
|
|
74
|
+
username: str,
|
|
75
|
+
database: DatabaseDep = Wired(),
|
|
76
|
+
) -> None:
|
|
77
|
+
database.write(username)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
create_user("mo")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`wired(get_database)` stores the dependency declaration in the annotation.
|
|
84
|
+
`Wired()` tells type checkers and call sites that the argument does not need to be
|
|
85
|
+
passed explicitly.
|
|
86
|
+
|
|
87
|
+
## Wiring classes
|
|
88
|
+
|
|
89
|
+
Decorate constructors or methods individually. Do not decorate the class itself.
|
|
90
|
+
|
|
91
|
+
Use constructor injection when the dependency belongs to the object's state:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from typing import Annotated
|
|
95
|
+
|
|
96
|
+
from wireme import Wired, wire, wired
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
type DatabaseDep = Annotated[Database, wired(get_database)]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TextProcessor:
|
|
103
|
+
@wire
|
|
104
|
+
def __init__(self, database: DatabaseDep = Wired()) -> None:
|
|
105
|
+
self._database = database
|
|
106
|
+
|
|
107
|
+
def process(self, text: str) -> None:
|
|
108
|
+
self._database.write(text)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
TextProcessor().process("Hello from constructor injection")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Use method injection when only a specific operation needs the dependency:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
class TextProcessor:
|
|
118
|
+
@wire
|
|
119
|
+
def process(
|
|
120
|
+
self,
|
|
121
|
+
text: str,
|
|
122
|
+
database: DatabaseDep = Wired(),
|
|
123
|
+
) -> None:
|
|
124
|
+
database.write(text)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
TextProcessor().process("Hello from method injection")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Constructor injection is useful for dependencies shared across multiple methods.
|
|
131
|
+
Method injection keeps the object stateless when only one operation needs the
|
|
132
|
+
dependency.
|
|
133
|
+
|
|
134
|
+
See [examples/classes.py](https://github.com/mghalix/wireme/blob/main/examples/classes.py) for a complete runnable example.
|
|
135
|
+
|
|
136
|
+
## Explicit values take precedence
|
|
137
|
+
|
|
138
|
+
A caller may still provide a dependency explicitly. The passed value wins over
|
|
139
|
+
injection:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
class TestDatabase(Database):
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
create_user("mo", TestDatabase())
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This is useful for one-off composition. For test suites, prefer
|
|
150
|
+
`override_dependency()` so every nested dependency sees the replacement.
|
|
151
|
+
|
|
152
|
+
## Test overrides
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from wireme import override_dependency
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_test_database() -> Database:
|
|
159
|
+
return TestDatabase()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
with override_dependency(get_database, get_test_database):
|
|
163
|
+
create_user("mo")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Overrides are restored when the context exits, including after exceptions.
|
|
167
|
+
Nested overrides restore the previous outer override correctly.
|
|
168
|
+
|
|
169
|
+
The provider is shared at process level. Use overrides for isolated tests and
|
|
170
|
+
application setup, not concurrent request-level mutation.
|
|
171
|
+
|
|
172
|
+
## Nested dependencies and caching
|
|
173
|
+
|
|
174
|
+
Factories can depend on other factories:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from wireme import wire, wired
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_settings() -> Settings:
|
|
181
|
+
return Settings()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_database(
|
|
185
|
+
settings: Settings = wired(get_settings),
|
|
186
|
+
) -> Database:
|
|
187
|
+
return Database(settings.database_url)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@wire
|
|
191
|
+
def operation(database: Database = wired(get_database)) -> None:
|
|
192
|
+
...
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Dependencies are cached once per wired call by default. Disable caching for a
|
|
196
|
+
specific declaration when every use must create a new value:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
value: Token = wired(create_token, use_cache=False)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Async and resource dependencies
|
|
203
|
+
|
|
204
|
+
Async factories are supported:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
async def get_client() -> Client:
|
|
208
|
+
return await Client.connect()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@wire
|
|
212
|
+
async def fetch_user(
|
|
213
|
+
user_id: str,
|
|
214
|
+
client: Client = wired(get_client),
|
|
215
|
+
) -> User:
|
|
216
|
+
return await client.fetch_user(user_id)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Generator and async-generator factories can own resource cleanup:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from collections.abc import AsyncIterator
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def get_client() -> AsyncIterator[Client]:
|
|
226
|
+
client = await Client.connect()
|
|
227
|
+
try:
|
|
228
|
+
yield client
|
|
229
|
+
finally:
|
|
230
|
+
await client.close()
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Cleanup runs after the wired callable finishes.
|
|
234
|
+
|
|
235
|
+
## Protocol dependencies
|
|
236
|
+
|
|
237
|
+
A protocol can describe the dependency interface. When runtime validation is
|
|
238
|
+
active, make the protocol runtime-checkable:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from typing import Annotated, Protocol, runtime_checkable
|
|
242
|
+
|
|
243
|
+
from wireme import Wired, wire, wired
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@runtime_checkable
|
|
247
|
+
class DatabaseLike(Protocol):
|
|
248
|
+
def write(self, value: str) -> None: ...
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
type DatabaseDep = Annotated[DatabaseLike, wired(get_database)]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@wire
|
|
255
|
+
def process(
|
|
256
|
+
value: str,
|
|
257
|
+
database: DatabaseDep = Wired(),
|
|
258
|
+
) -> None:
|
|
259
|
+
database.write(value)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Build integrations on top of Wireme
|
|
263
|
+
|
|
264
|
+
Wireme can be the small DI primitive behind a project-specific API. For example,
|
|
265
|
+
a service or plugin registry can expose `wired_service(ServiceType)` while still
|
|
266
|
+
using Wireme for resolution, typing, caching, and overrides.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
import functools
|
|
270
|
+
|
|
271
|
+
from collections.abc import Callable
|
|
272
|
+
from typing import cast
|
|
273
|
+
|
|
274
|
+
from wireme import WiremeError, wired
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class ServiceUnavailableError(WiremeError):
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
_services: dict[type[object], object] = {}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@functools.cache
|
|
285
|
+
def require_service[T](service_type: type[T]) -> Callable[[], T]:
|
|
286
|
+
def dependency() -> T:
|
|
287
|
+
try:
|
|
288
|
+
service = _services[service_type]
|
|
289
|
+
except KeyError as error:
|
|
290
|
+
raise ServiceUnavailableError(
|
|
291
|
+
f"{service_type.__name__} is not registered."
|
|
292
|
+
) from error
|
|
293
|
+
|
|
294
|
+
return cast(T, service)
|
|
295
|
+
|
|
296
|
+
return dependency
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def wired_service[T](service_type: type[T]) -> T:
|
|
300
|
+
return wired(require_service(service_type))
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Caching the generated factory gives it stable identity, which is important when
|
|
304
|
+
using `override_dependency()`.
|
|
305
|
+
|
|
306
|
+
See [`examples/custom_integration.py`](https://github.com/mghalix/wireme/blob/main/examples/custom_integration.py) for a
|
|
307
|
+
complete runnable example.
|
|
308
|
+
|
|
309
|
+
## Errors
|
|
310
|
+
|
|
311
|
+
Wireme exposes:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
from wireme import ValidationError, WiremeError
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
- `WiremeError` is the base error exposed by Wireme.
|
|
318
|
+
- `ValidationError` represents dependency input or result validation failures.
|
|
319
|
+
|
|
320
|
+
Project-specific DI errors may inherit from `WiremeError`.
|
|
321
|
+
|
|
322
|
+
## Ruff configuration
|
|
323
|
+
|
|
324
|
+
Ruff's `B008` rule normally rejects function calls in defaults. Tell Ruff that
|
|
325
|
+
`Wired()` is an immutable marker:
|
|
326
|
+
|
|
327
|
+
```toml
|
|
328
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
329
|
+
extend-immutable-calls = ["wireme.Wired"]
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Declarations using `wired(factory)` may also need your project's normal DI rule
|
|
333
|
+
configuration, depending on which Ruff rules are enabled.
|
|
334
|
+
|
|
335
|
+
## Public API
|
|
336
|
+
|
|
337
|
+
| Name | Purpose |
|
|
338
|
+
| --------------------- | --------------------------------------------------------------- |
|
|
339
|
+
| `wire` | Decorate a function or method and enable dependency resolution. |
|
|
340
|
+
| `wired` | Declare a dependency factory and its resolution options. |
|
|
341
|
+
| `Wired` | Mark an `Annotated` dependency as caller-optional. |
|
|
342
|
+
| `override_dependency` | Temporarily replace a factory, with nested restoration. |
|
|
343
|
+
| `WiremeError` | Base error exposed by Wireme. |
|
|
344
|
+
| `ValidationError` | Dependency validation error. |
|
|
345
|
+
|
|
346
|
+
## Examples
|
|
347
|
+
|
|
348
|
+
- [`examples/basic.py`](https://github.com/mghalix/wireme/blob/main/examples/basic.py) covers direct and reusable dependencies.
|
|
349
|
+
- [`examples/classes.py`](https://github.com/mghalix/wireme/blob/main/examples/classes.py) covers constructor and method injection.
|
|
350
|
+
- [`examples/overrides.py`](https://github.com/mghalix/wireme/blob/main/examples/overrides.py) covers isolated test overrides.
|
|
351
|
+
- [`examples/protocols.py`](https://github.com/mghalix/wireme/blob/main/examples/protocols.py) covers interface-based dependencies.
|
|
352
|
+
- [`examples/resources.py`](https://github.com/mghalix/wireme/blob/main/examples/resources.py) covers async resource cleanup.
|
|
353
|
+
- [`examples/custom_integration.py`](https://github.com/mghalix/wireme/blob/main/examples/custom_integration.py) builds a typed service registry integration.
|
|
354
|
+
|
|
355
|
+
Run any example with:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
uv run python examples/basic.py
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Why Wireme instead of importing FastDepends directly?
|
|
362
|
+
|
|
363
|
+
FastDepends provides the resolution engine. Wireme provides a deliberately small,
|
|
364
|
+
opinionated facade with:
|
|
365
|
+
|
|
366
|
+
- A cohesive `wire`, `wired`, and `Wired` vocabulary.
|
|
367
|
+
- Strong return typing for sync, async, generator, and async-generator factories.
|
|
368
|
+
- Reusable `Annotated` dependencies.
|
|
369
|
+
- Injected parameters hidden from public runtime signatures.
|
|
370
|
+
- Nested-safe dependency overrides.
|
|
371
|
+
- A minimal backend-independent public namespace.
|
|
372
|
+
|
|
373
|
+
## License
|
|
374
|
+
|
|
375
|
+
MIT
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wireme"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Tiny, typed dependency injection built on FastDepends."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Mohanad Ghali", email = "mghalix@gmail.com" }]
|
|
10
|
+
keywords = ["dependency-injection", "di", "fast-depends", "typing"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["fast-depends[pydantic]>=3.0.8,<4"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/mghalix/wireme"
|
|
26
|
+
Repository = "https://github.com/mghalix/wireme"
|
|
27
|
+
Issues = "https://github.com/mghalix/wireme/issues"
|
|
28
|
+
Changelog = "https://github.com/mghalix/wireme/blob/main/CHANGELOG.md"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = ["basedpyright", "pytest", "pytest-cov", "ruff", "trio"]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.10.0,<0.11.0"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
target-version = "py312"
|
|
39
|
+
line-length = 88
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["B", "E4", "E7", "E9", "F", "I", "N", "RUF", "S", "SIM", "UP"]
|
|
43
|
+
ignore = ["N802", "S101"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
46
|
+
extend-immutable-calls = ["wireme.Wired", "wireme.wired"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint.per-file-ignores]
|
|
49
|
+
"tests/*" = ["S101"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
addopts = "-ra --strict-config --strict-markers"
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
|
|
55
|
+
[tool.coverage.run]
|
|
56
|
+
branch = true
|
|
57
|
+
source = ["wireme"]
|
|
58
|
+
|
|
59
|
+
[tool.coverage.report]
|
|
60
|
+
fail_under = 100
|
|
61
|
+
show_missing = true
|
|
62
|
+
skip_covered = true
|
|
63
|
+
|
|
64
|
+
[tool.basedpyright]
|
|
65
|
+
include = ["src", "tests"]
|
|
66
|
+
pythonVersion = "3.12"
|
|
67
|
+
typeCheckingMode = "strict"
|
|
68
|
+
reportAny = false
|
|
69
|
+
reportExplicitAny = false
|
|
70
|
+
reportPrivateUsage = false
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Tiny, typed dependency injection built on FastDepends."""
|
|
2
|
+
|
|
3
|
+
from wireme._errors import ValidationError, WiremeError
|
|
4
|
+
from wireme._impl import Wired, override_dependency, wire, wired
|
|
5
|
+
|
|
6
|
+
__all__ = (
|
|
7
|
+
"ValidationError",
|
|
8
|
+
"Wired",
|
|
9
|
+
"WiremeError",
|
|
10
|
+
"override_dependency",
|
|
11
|
+
"wire",
|
|
12
|
+
"wired",
|
|
13
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from fast_depends import Depends as _Depends
|
|
2
|
+
from fast_depends import Provider as _Provider
|
|
3
|
+
from fast_depends import inject as _inject
|
|
4
|
+
from fast_depends.dependencies import Dependant as _Dependant
|
|
5
|
+
|
|
6
|
+
__all__ = (
|
|
7
|
+
"_Dependant",
|
|
8
|
+
"_Depends",
|
|
9
|
+
"_Provider",
|
|
10
|
+
"_inject",
|
|
11
|
+
)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Internal implementation for :mod:`wireme`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import inspect
|
|
7
|
+
import typing
|
|
8
|
+
from collections.abc import (
|
|
9
|
+
AsyncIterator,
|
|
10
|
+
Awaitable,
|
|
11
|
+
Callable,
|
|
12
|
+
Generator,
|
|
13
|
+
Iterator,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from ._core import _Dependant, _Depends, _inject, _Provider
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _HasSignature(typing.Protocol):
|
|
20
|
+
"""Represent a callable with a customizable runtime signature."""
|
|
21
|
+
|
|
22
|
+
__signature__: inspect.Signature
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_MISSING = object()
|
|
26
|
+
_provider = _Provider()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
type _DependencyFactory[R] = (
|
|
30
|
+
Callable[..., Awaitable[R]]
|
|
31
|
+
| Callable[..., AsyncIterator[R]]
|
|
32
|
+
| Callable[..., Iterator[R]]
|
|
33
|
+
| Callable[..., R]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def Wired() -> typing.Any:
|
|
38
|
+
"""Mark an annotated dependency as optional for static type checkers."""
|
|
39
|
+
return ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _resolve_annotation(
|
|
43
|
+
annotation: object,
|
|
44
|
+
/,
|
|
45
|
+
*,
|
|
46
|
+
globalns: dict[str, object],
|
|
47
|
+
localns: dict[str, object],
|
|
48
|
+
) -> object:
|
|
49
|
+
"""Resolve strings and PEP 695 aliases recursively.
|
|
50
|
+
|
|
51
|
+
Unresolved forward references become Any in FastDepends' internal
|
|
52
|
+
signature while remaining unchanged in Wireme's public signature.
|
|
53
|
+
"""
|
|
54
|
+
while True:
|
|
55
|
+
if isinstance(annotation, str):
|
|
56
|
+
try:
|
|
57
|
+
annotation = eval(annotation, globalns, localns) # noqa: S307
|
|
58
|
+
except (AttributeError, NameError, TypeError):
|
|
59
|
+
return typing.Any
|
|
60
|
+
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
if isinstance(annotation, typing.TypeAliasType):
|
|
64
|
+
annotation = annotation.__value__
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
return annotation
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_dependency_parameter(
|
|
71
|
+
parameter: inspect.Parameter,
|
|
72
|
+
/,
|
|
73
|
+
*,
|
|
74
|
+
globalns: dict[str, object],
|
|
75
|
+
localns: dict[str, object],
|
|
76
|
+
) -> bool:
|
|
77
|
+
"""Return whether a parameter is supplied through dependency injection."""
|
|
78
|
+
if isinstance(parameter.default, _Dependant):
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
annotation = _resolve_annotation(
|
|
82
|
+
parameter.annotation,
|
|
83
|
+
globalns=globalns,
|
|
84
|
+
localns=localns,
|
|
85
|
+
)
|
|
86
|
+
if typing.get_origin(annotation) is not typing.Annotated:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
return any(
|
|
90
|
+
isinstance(metadata, _Dependant) for metadata in typing.get_args(annotation)[1:]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def wire[**P, T](func: Callable[P, T], /) -> Callable[P, T]:
|
|
95
|
+
"""Enable dependency injection and hide injected runtime parameters.
|
|
96
|
+
|
|
97
|
+
Apply this decorator to functions and methods, not classes.
|
|
98
|
+
"""
|
|
99
|
+
original_signature = inspect.signature(func)
|
|
100
|
+
|
|
101
|
+
frame = inspect.currentframe()
|
|
102
|
+
try:
|
|
103
|
+
caller_locals = (
|
|
104
|
+
dict(frame.f_back.f_locals)
|
|
105
|
+
if frame is not None and frame.f_back is not None
|
|
106
|
+
else {}
|
|
107
|
+
)
|
|
108
|
+
finally:
|
|
109
|
+
del frame
|
|
110
|
+
|
|
111
|
+
globalns = dict(getattr(func, "__globals__", {}))
|
|
112
|
+
type_parameters = {
|
|
113
|
+
parameter.__name__: parameter
|
|
114
|
+
for parameter in getattr(func, "__type_params__", ())
|
|
115
|
+
}
|
|
116
|
+
caller_locals.update(type_parameters)
|
|
117
|
+
|
|
118
|
+
resolved_parameters = tuple(
|
|
119
|
+
parameter.replace(
|
|
120
|
+
annotation=_resolve_annotation(
|
|
121
|
+
parameter.annotation,
|
|
122
|
+
globalns=globalns,
|
|
123
|
+
localns=caller_locals,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
for parameter in original_signature.parameters.values()
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
resolved_signature = original_signature.replace(
|
|
130
|
+
parameters=resolved_parameters,
|
|
131
|
+
return_annotation=_resolve_annotation(
|
|
132
|
+
original_signature.return_annotation,
|
|
133
|
+
globalns=globalns,
|
|
134
|
+
localns=caller_locals,
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
previous_signature = getattr(func, "__signature__", _MISSING)
|
|
139
|
+
typing.cast("_HasSignature", func).__signature__ = resolved_signature
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
wrapped = _inject(func, dependency_provider=_provider)
|
|
143
|
+
finally:
|
|
144
|
+
if previous_signature is _MISSING:
|
|
145
|
+
delattr(func, "__signature__")
|
|
146
|
+
else:
|
|
147
|
+
typing.cast("_HasSignature", func).__signature__ = typing.cast(
|
|
148
|
+
"inspect.Signature",
|
|
149
|
+
previous_signature,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
public_parameters = tuple(
|
|
153
|
+
original_parameter
|
|
154
|
+
for original_parameter, resolved_parameter in zip(
|
|
155
|
+
original_signature.parameters.values(),
|
|
156
|
+
resolved_signature.parameters.values(),
|
|
157
|
+
strict=True,
|
|
158
|
+
)
|
|
159
|
+
if not _is_dependency_parameter(
|
|
160
|
+
resolved_parameter,
|
|
161
|
+
globalns=globalns,
|
|
162
|
+
localns=caller_locals,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
typing.cast("_HasSignature", wrapped).__signature__ = original_signature.replace(
|
|
167
|
+
parameters=public_parameters
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return wrapped
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@typing.overload
|
|
174
|
+
def wired[**P, R](
|
|
175
|
+
factory: Callable[P, Awaitable[R]],
|
|
176
|
+
/,
|
|
177
|
+
*,
|
|
178
|
+
use_cache: bool = True,
|
|
179
|
+
cast: bool = True,
|
|
180
|
+
cast_result: bool = False,
|
|
181
|
+
) -> R: ...
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@typing.overload
|
|
185
|
+
def wired[**P, R](
|
|
186
|
+
factory: Callable[P, AsyncIterator[R]],
|
|
187
|
+
/,
|
|
188
|
+
*,
|
|
189
|
+
use_cache: bool = True,
|
|
190
|
+
cast: bool = True,
|
|
191
|
+
cast_result: bool = False,
|
|
192
|
+
) -> R: ...
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@typing.overload
|
|
196
|
+
def wired[**P, R](
|
|
197
|
+
factory: Callable[P, Iterator[R]],
|
|
198
|
+
/,
|
|
199
|
+
*,
|
|
200
|
+
use_cache: bool = True,
|
|
201
|
+
cast: bool = True,
|
|
202
|
+
cast_result: bool = False,
|
|
203
|
+
) -> R: ...
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@typing.overload
|
|
207
|
+
def wired[**P, R](
|
|
208
|
+
factory: Callable[P, R],
|
|
209
|
+
/,
|
|
210
|
+
*,
|
|
211
|
+
use_cache: bool = True,
|
|
212
|
+
cast: bool = True,
|
|
213
|
+
cast_result: bool = False,
|
|
214
|
+
) -> R: ...
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def wired(
|
|
218
|
+
factory: Callable[..., object],
|
|
219
|
+
/,
|
|
220
|
+
*,
|
|
221
|
+
use_cache: bool = True,
|
|
222
|
+
cast: bool = True,
|
|
223
|
+
cast_result: bool = False,
|
|
224
|
+
) -> typing.Any:
|
|
225
|
+
"""Declare a dependency resolved by a callable."""
|
|
226
|
+
return _Depends(
|
|
227
|
+
factory,
|
|
228
|
+
use_cache=use_cache,
|
|
229
|
+
cast=cast,
|
|
230
|
+
cast_result=cast_result,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@contextlib.contextmanager
|
|
235
|
+
def override_dependency[R](
|
|
236
|
+
original: _DependencyFactory[R],
|
|
237
|
+
replacement: _DependencyFactory[R],
|
|
238
|
+
/,
|
|
239
|
+
) -> Generator[None, None, None]:
|
|
240
|
+
"""Temporarily replace a dependency factory.
|
|
241
|
+
|
|
242
|
+
Nested overrides are restored correctly. Overrides modify a shared provider,
|
|
243
|
+
so use them for isolated tests and application setup, not concurrent
|
|
244
|
+
request-level mutation.
|
|
245
|
+
"""
|
|
246
|
+
previous_override = _provider.overrides.get(original, _MISSING)
|
|
247
|
+
_provider.override(original, replacement)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
yield
|
|
251
|
+
finally:
|
|
252
|
+
if previous_override is _MISSING:
|
|
253
|
+
_provider.overrides.pop(original, None)
|
|
254
|
+
else:
|
|
255
|
+
_provider.overrides[original] = typing.cast(
|
|
256
|
+
"typing.Any",
|
|
257
|
+
previous_override,
|
|
258
|
+
)
|
|
File without changes
|