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 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,4 @@
1
+ from fast_depends.exceptions import FastDependsError as WiremeError
2
+ from fast_depends.exceptions import ValidationError
3
+
4
+ __all__ = ("ValidationError", "WiremeError")
@@ -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