anydi 0.57.0__tar.gz → 0.59.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.
- {anydi-0.57.0 → anydi-0.59.0}/PKG-INFO +24 -4
- {anydi-0.57.0 → anydi-0.59.0}/README.md +23 -3
- {anydi-0.57.0 → anydi-0.59.0}/anydi/_container.py +124 -53
- {anydi-0.57.0 → anydi-0.59.0}/anydi/_resolver.py +51 -24
- {anydi-0.57.0 → anydi-0.59.0}/anydi/_types.py +1 -1
- {anydi-0.57.0 → anydi-0.59.0}/anydi/ext/fastapi.py +2 -2
- anydi-0.59.0/anydi/ext/faststream.py +74 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/ext/starlette/middleware.py +1 -1
- {anydi-0.57.0 → anydi-0.59.0}/pyproject.toml +2 -2
- anydi-0.57.0/anydi/ext/faststream.py +0 -52
- {anydi-0.57.0 → anydi-0.59.0}/anydi/__init__.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/_async_lock.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/_context.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/_decorators.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/_injector.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/_module.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/_provider.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/_scanner.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/ext/__init__.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/ext/django/__init__.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/ext/pydantic_settings.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/ext/pytest_plugin.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/ext/starlette/__init__.py +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/py.typed +0 -0
- {anydi-0.57.0 → anydi-0.59.0}/anydi/testing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anydi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.59.0
|
|
4
4
|
Summary: Dependency Injection library
|
|
5
5
|
Keywords: dependency injection,dependencies,di,async,asyncio,application
|
|
6
6
|
Author: Anton Ruhlov
|
|
@@ -58,9 +58,9 @@ The key features are:
|
|
|
58
58
|
|
|
59
59
|
* **Type-safe**: Dependency resolution is driven by type hints.
|
|
60
60
|
* **Async-ready**: Works the same for sync and async providers or injections.
|
|
61
|
-
* **Scoped**: Built-in singleton, transient, and request
|
|
61
|
+
* **Scoped**: Built-in singleton, transient, and request scopes, plus custom scopes.
|
|
62
62
|
* **Simple**: Small surface area keeps boilerplate low.
|
|
63
|
-
* **Fast**:
|
|
63
|
+
* **Fast**: Has minimal overhead and resolves dependencies quickly.
|
|
64
64
|
* **Named**: `Annotated[...]` makes multiple bindings per type simple.
|
|
65
65
|
* **Managed**: Providers can open/close resources via context managers.
|
|
66
66
|
* **Modular**: Compose containers or modules for large apps.
|
|
@@ -74,7 +74,7 @@ The key features are:
|
|
|
74
74
|
pip install anydi
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
##
|
|
77
|
+
## Quick Example
|
|
78
78
|
|
|
79
79
|
### Define a Service (`app/services.py`)
|
|
80
80
|
|
|
@@ -264,3 +264,23 @@ urlpatterns = [
|
|
|
264
264
|
path("api/", api.urls),
|
|
265
265
|
]
|
|
266
266
|
```
|
|
267
|
+
|
|
268
|
+
## What's Next?
|
|
269
|
+
|
|
270
|
+
Ready to learn more? Check out these resources:
|
|
271
|
+
|
|
272
|
+
**Core Documentation:**
|
|
273
|
+
- [Core Concepts](https://anydi.readthedocs.io/en/latest/concepts/) - Understand containers, providers, scopes, and dependency injection
|
|
274
|
+
- [Providers](https://anydi.readthedocs.io/en/latest/usage/providers/) - Learn about registration, named providers, and resource management
|
|
275
|
+
- [Scopes](https://anydi.readthedocs.io/en/latest/usage/scopes/) - Master lifecycle management with built-in and custom scopes
|
|
276
|
+
- [Dependency Injection](https://anydi.readthedocs.io/en/latest/usage/injection/) - Explore injection patterns and techniques
|
|
277
|
+
- [Testing](https://anydi.readthedocs.io/en/latest/usage/testing/) - Write testable code with provider overrides
|
|
278
|
+
|
|
279
|
+
**Framework Integrations:**
|
|
280
|
+
- [FastAPI](https://anydi.readthedocs.io/en/latest/extensions/fastapi/) - Build modern APIs with automatic dependency injection
|
|
281
|
+
- [Django](https://anydi.readthedocs.io/en/latest/extensions/django/) - Integrate with Django and Django Ninja
|
|
282
|
+
- [FastStream](https://anydi.readthedocs.io/en/latest/extensions/faststream/) - Message broker applications
|
|
283
|
+
- [Pydantic Settings](https://anydi.readthedocs.io/en/latest/extensions/pydantic_settings/) - Configuration management
|
|
284
|
+
|
|
285
|
+
**Full Documentation:**
|
|
286
|
+
- [Read the Docs](https://anydi.readthedocs.io/) - Complete documentation with examples and guides
|
|
@@ -23,9 +23,9 @@ The key features are:
|
|
|
23
23
|
|
|
24
24
|
* **Type-safe**: Dependency resolution is driven by type hints.
|
|
25
25
|
* **Async-ready**: Works the same for sync and async providers or injections.
|
|
26
|
-
* **Scoped**: Built-in singleton, transient, and request
|
|
26
|
+
* **Scoped**: Built-in singleton, transient, and request scopes, plus custom scopes.
|
|
27
27
|
* **Simple**: Small surface area keeps boilerplate low.
|
|
28
|
-
* **Fast**:
|
|
28
|
+
* **Fast**: Has minimal overhead and resolves dependencies quickly.
|
|
29
29
|
* **Named**: `Annotated[...]` makes multiple bindings per type simple.
|
|
30
30
|
* **Managed**: Providers can open/close resources via context managers.
|
|
31
31
|
* **Modular**: Compose containers or modules for large apps.
|
|
@@ -39,7 +39,7 @@ The key features are:
|
|
|
39
39
|
pip install anydi
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## Quick Example
|
|
43
43
|
|
|
44
44
|
### Define a Service (`app/services.py`)
|
|
45
45
|
|
|
@@ -229,3 +229,23 @@ urlpatterns = [
|
|
|
229
229
|
path("api/", api.urls),
|
|
230
230
|
]
|
|
231
231
|
```
|
|
232
|
+
|
|
233
|
+
## What's Next?
|
|
234
|
+
|
|
235
|
+
Ready to learn more? Check out these resources:
|
|
236
|
+
|
|
237
|
+
**Core Documentation:**
|
|
238
|
+
- [Core Concepts](https://anydi.readthedocs.io/en/latest/concepts/) - Understand containers, providers, scopes, and dependency injection
|
|
239
|
+
- [Providers](https://anydi.readthedocs.io/en/latest/usage/providers/) - Learn about registration, named providers, and resource management
|
|
240
|
+
- [Scopes](https://anydi.readthedocs.io/en/latest/usage/scopes/) - Master lifecycle management with built-in and custom scopes
|
|
241
|
+
- [Dependency Injection](https://anydi.readthedocs.io/en/latest/usage/injection/) - Explore injection patterns and techniques
|
|
242
|
+
- [Testing](https://anydi.readthedocs.io/en/latest/usage/testing/) - Write testable code with provider overrides
|
|
243
|
+
|
|
244
|
+
**Framework Integrations:**
|
|
245
|
+
- [FastAPI](https://anydi.readthedocs.io/en/latest/extensions/fastapi/) - Build modern APIs with automatic dependency injection
|
|
246
|
+
- [Django](https://anydi.readthedocs.io/en/latest/extensions/django/) - Integrate with Django and Django Ninja
|
|
247
|
+
- [FastStream](https://anydi.readthedocs.io/en/latest/extensions/faststream/) - Message broker applications
|
|
248
|
+
- [Pydantic Settings](https://anydi.readthedocs.io/en/latest/extensions/pydantic_settings/) - Configuration management
|
|
249
|
+
|
|
250
|
+
**Full Documentation:**
|
|
251
|
+
- [Read the Docs](https://anydi.readthedocs.io/) - Complete documentation with examples and guides
|
|
@@ -9,7 +9,7 @@ import logging
|
|
|
9
9
|
import types
|
|
10
10
|
import uuid
|
|
11
11
|
from collections import defaultdict
|
|
12
|
-
from collections.abc import AsyncIterator, Callable, Iterable, Iterator
|
|
12
|
+
from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Sequence
|
|
13
13
|
from contextvars import ContextVar
|
|
14
14
|
from typing import Any, TypeVar, get_args, get_origin, overload
|
|
15
15
|
|
|
@@ -22,24 +22,11 @@ from ._module import ModuleDef, ModuleRegistrar
|
|
|
22
22
|
from ._provider import Provider, ProviderDef, ProviderKind, ProviderParameter
|
|
23
23
|
from ._resolver import Resolver
|
|
24
24
|
from ._scanner import PackageOrIterable, Scanner
|
|
25
|
-
from ._types import
|
|
26
|
-
NOT_SET,
|
|
27
|
-
Event,
|
|
28
|
-
Scope,
|
|
29
|
-
is_event_type,
|
|
30
|
-
is_iterator_type,
|
|
31
|
-
is_none_type,
|
|
32
|
-
)
|
|
25
|
+
from ._types import NOT_SET, Event, Scope, is_event_type, is_iterator_type, is_none_type
|
|
33
26
|
|
|
34
27
|
T = TypeVar("T", bound=Any)
|
|
35
28
|
P = ParamSpec("P")
|
|
36
29
|
|
|
37
|
-
ALLOWED_SCOPES: dict[Scope, list[Scope]] = {
|
|
38
|
-
"singleton": ["singleton"],
|
|
39
|
-
"request": ["request", "singleton"],
|
|
40
|
-
"transient": ["transient", "request", "singleton"],
|
|
41
|
-
}
|
|
42
|
-
|
|
43
30
|
|
|
44
31
|
class Container:
|
|
45
32
|
"""AnyDI is a dependency injection container."""
|
|
@@ -53,11 +40,14 @@ class Container:
|
|
|
53
40
|
) -> None:
|
|
54
41
|
self._providers: dict[Any, Provider] = {}
|
|
55
42
|
self._logger = logger or logging.getLogger(__name__)
|
|
43
|
+
self._scopes: dict[str, Sequence[str]] = {
|
|
44
|
+
"transient": ("transient", "singleton"),
|
|
45
|
+
"singleton": ("singleton",),
|
|
46
|
+
}
|
|
47
|
+
|
|
56
48
|
self._resources: dict[str, list[Any]] = defaultdict(list)
|
|
57
49
|
self._singleton_context = InstanceContext()
|
|
58
|
-
self.
|
|
59
|
-
"request_context", default=None
|
|
60
|
-
)
|
|
50
|
+
self._scoped_context: dict[str, ContextVar[InstanceContext]] = {}
|
|
61
51
|
|
|
62
52
|
# Components
|
|
63
53
|
self._resolver = Resolver(self)
|
|
@@ -65,6 +55,9 @@ class Container:
|
|
|
65
55
|
self._modules = ModuleRegistrar(self)
|
|
66
56
|
self._scanner = Scanner(self)
|
|
67
57
|
|
|
58
|
+
# Register default scopes
|
|
59
|
+
self.register_scope("request")
|
|
60
|
+
|
|
68
61
|
# Register providers
|
|
69
62
|
providers = providers or []
|
|
70
63
|
for provider in providers:
|
|
@@ -141,54 +134,128 @@ class Container:
|
|
|
141
134
|
await self._singleton_context.aclose()
|
|
142
135
|
|
|
143
136
|
@contextlib.contextmanager
|
|
144
|
-
def
|
|
137
|
+
def scoped_context(self, scope: str) -> Iterator[InstanceContext]:
|
|
145
138
|
"""Obtain a context manager for the request-scoped context."""
|
|
146
|
-
|
|
139
|
+
context_var = self._get_scoped_context_var(scope)
|
|
147
140
|
|
|
148
|
-
|
|
141
|
+
# Check if context already exists (re-entering same scope)
|
|
142
|
+
context = context_var.get(None)
|
|
143
|
+
if context is not None:
|
|
144
|
+
# Reuse existing context, don't create a new one
|
|
145
|
+
yield context
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
# Create new context
|
|
149
|
+
context = InstanceContext()
|
|
150
|
+
token = context_var.set(context)
|
|
149
151
|
|
|
150
152
|
# Resolve all request resources
|
|
151
|
-
for interface in self._resources.get(
|
|
153
|
+
for interface in self._resources.get(scope, []):
|
|
152
154
|
if not is_event_type(interface):
|
|
153
155
|
continue
|
|
154
156
|
self.resolve(interface)
|
|
155
157
|
|
|
156
158
|
with context:
|
|
157
159
|
yield context
|
|
158
|
-
|
|
160
|
+
context_var.reset(token)
|
|
159
161
|
|
|
160
162
|
@contextlib.asynccontextmanager
|
|
161
|
-
async def
|
|
162
|
-
"""Obtain
|
|
163
|
-
|
|
163
|
+
async def ascoped_context(self, scope: str) -> AsyncIterator[InstanceContext]:
|
|
164
|
+
"""Obtain a context manager for the specified scoped context."""
|
|
165
|
+
context_var = self._get_scoped_context_var(scope)
|
|
166
|
+
|
|
167
|
+
# Check if context already exists (re-entering same scope)
|
|
168
|
+
context = context_var.get(None)
|
|
169
|
+
if context is not None:
|
|
170
|
+
# Reuse existing context, don't create a new one
|
|
171
|
+
yield context
|
|
172
|
+
return
|
|
164
173
|
|
|
165
|
-
|
|
174
|
+
# Create new context
|
|
175
|
+
context = InstanceContext()
|
|
176
|
+
token = context_var.set(context)
|
|
166
177
|
|
|
167
|
-
|
|
178
|
+
# Resolve all request resources
|
|
179
|
+
for interface in self._resources.get(scope, []):
|
|
168
180
|
if not is_event_type(interface):
|
|
169
181
|
continue
|
|
170
182
|
await self.aresolve(interface)
|
|
171
183
|
|
|
172
184
|
async with context:
|
|
173
185
|
yield context
|
|
174
|
-
|
|
186
|
+
context_var.reset(token)
|
|
187
|
+
|
|
188
|
+
@contextlib.contextmanager
|
|
189
|
+
def request_context(self) -> Iterator[InstanceContext]:
|
|
190
|
+
"""Obtain a context manager for the request-scoped context."""
|
|
191
|
+
with self.scoped_context("request") as context:
|
|
192
|
+
yield context
|
|
193
|
+
|
|
194
|
+
@contextlib.asynccontextmanager
|
|
195
|
+
async def arequest_context(self) -> AsyncIterator[InstanceContext]:
|
|
196
|
+
"""Obtain an async context manager for the request-scoped context."""
|
|
197
|
+
async with self.ascoped_context("request") as context:
|
|
198
|
+
yield context
|
|
175
199
|
|
|
176
|
-
def
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
200
|
+
def _get_scoped_context(self, scope: str) -> InstanceContext:
|
|
201
|
+
scoped_context_var = self._get_scoped_context_var(scope)
|
|
202
|
+
try:
|
|
203
|
+
scoped_context = scoped_context_var.get()
|
|
204
|
+
except LookupError as exc:
|
|
180
205
|
raise LookupError(
|
|
181
|
-
"The
|
|
182
|
-
"the
|
|
206
|
+
f"The {scope} context has not been started. Please ensure that "
|
|
207
|
+
f"the {scope} context is properly initialized before attempting "
|
|
183
208
|
"to use it."
|
|
209
|
+
) from exc
|
|
210
|
+
return scoped_context
|
|
211
|
+
|
|
212
|
+
def _get_scoped_context_var(self, scope: str) -> ContextVar[InstanceContext]:
|
|
213
|
+
"""Get the context variable for the specified scope."""
|
|
214
|
+
# Validate that scope is registered and not reserved
|
|
215
|
+
if scope in ("transient", "singleton"):
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"Cannot get context variable for reserved scope `{scope}`."
|
|
184
218
|
)
|
|
185
|
-
|
|
219
|
+
if scope not in self._scopes:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"Cannot get context variable for not registered scope `{scope}`. "
|
|
222
|
+
f"Please register the scope first using register_scope()."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if scope not in self._scoped_context:
|
|
226
|
+
self._scoped_context[scope] = ContextVar(f"{scope}_context")
|
|
227
|
+
return self._scoped_context[scope]
|
|
186
228
|
|
|
187
229
|
def _get_instance_context(self, scope: Scope) -> InstanceContext:
|
|
188
230
|
"""Get the instance context for the specified scope."""
|
|
189
231
|
if scope == "singleton":
|
|
190
232
|
return self._singleton_context
|
|
191
|
-
return self.
|
|
233
|
+
return self._get_scoped_context(scope)
|
|
234
|
+
|
|
235
|
+
# == Scopes == #
|
|
236
|
+
|
|
237
|
+
def register_scope(
|
|
238
|
+
self, scope: str, *, parents: Sequence[str] | None = None
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Register a new scope with the specified parents."""
|
|
241
|
+
# Check if the scope is reserved
|
|
242
|
+
if scope in ("transient", "singleton"):
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"The scope `{scope}` is reserved and cannot be overridden."
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Check if the scope is already registered
|
|
248
|
+
if scope in self._scopes:
|
|
249
|
+
raise ValueError(f"The scope `{scope}` is already registered.")
|
|
250
|
+
|
|
251
|
+
# Validate parents
|
|
252
|
+
parents = parents or []
|
|
253
|
+
for parent in parents:
|
|
254
|
+
if parent not in self._scopes:
|
|
255
|
+
raise ValueError(f"The parent scope `{parent}` is not registered.")
|
|
256
|
+
|
|
257
|
+
# Register the scope
|
|
258
|
+
self._scopes[scope] = tuple({scope, "singleton"} | set(parents))
|
|
192
259
|
|
|
193
260
|
# == Provider Registry ==
|
|
194
261
|
|
|
@@ -300,7 +367,7 @@ class Container:
|
|
|
300
367
|
unresolved_parameter = None
|
|
301
368
|
unresolved_exc: LookupError | None = None
|
|
302
369
|
parameters: list[ProviderParameter] = []
|
|
303
|
-
|
|
370
|
+
scope_provider: dict[Scope, Provider] = {}
|
|
304
371
|
|
|
305
372
|
for parameter in signature.parameters.values():
|
|
306
373
|
if parameter.annotation is inspect.Parameter.empty:
|
|
@@ -331,8 +398,8 @@ class Container:
|
|
|
331
398
|
continue
|
|
332
399
|
|
|
333
400
|
# Store first provider for each scope
|
|
334
|
-
if sub_provider.scope not in
|
|
335
|
-
|
|
401
|
+
if sub_provider.scope not in scope_provider:
|
|
402
|
+
scope_provider[sub_provider.scope] = sub_provider
|
|
336
403
|
|
|
337
404
|
parameters.append(
|
|
338
405
|
ProviderParameter(
|
|
@@ -345,6 +412,18 @@ class Container:
|
|
|
345
412
|
)
|
|
346
413
|
)
|
|
347
414
|
|
|
415
|
+
# Check scope compatibility
|
|
416
|
+
# Transient scope can use any scoped dependencies
|
|
417
|
+
if scope != "transient":
|
|
418
|
+
for sub_provider in scope_provider.values():
|
|
419
|
+
if sub_provider.scope not in self._scopes.get(scope, []):
|
|
420
|
+
raise ValueError(
|
|
421
|
+
f"The provider `{name}` with a `{scope}` scope "
|
|
422
|
+
f"cannot depend on `{sub_provider}` with a "
|
|
423
|
+
f"`{sub_provider.scope}` scope. Please ensure all "
|
|
424
|
+
"providers are registered with matching scopes."
|
|
425
|
+
)
|
|
426
|
+
|
|
348
427
|
# Check for unresolved parameters
|
|
349
428
|
if unresolved_parameter:
|
|
350
429
|
if scope not in ("singleton", "transient"):
|
|
@@ -358,15 +437,6 @@ class Container:
|
|
|
358
437
|
f"attempting to use it."
|
|
359
438
|
) from unresolved_exc
|
|
360
439
|
|
|
361
|
-
# Check scope compatibility
|
|
362
|
-
for sub_provider in scopes.values():
|
|
363
|
-
if sub_provider.scope not in ALLOWED_SCOPES.get(scope, []):
|
|
364
|
-
raise ValueError(
|
|
365
|
-
f"The provider `{name}` with a `{scope}` scope cannot "
|
|
366
|
-
f"depend on `{sub_provider}` with a `{sub_provider.scope}` scope. "
|
|
367
|
-
"Please ensure all providers are registered with matching scopes."
|
|
368
|
-
)
|
|
369
|
-
|
|
370
440
|
is_coroutine = kind == ProviderKind.COROUTINE
|
|
371
441
|
is_generator = kind == ProviderKind.GENERATOR
|
|
372
442
|
is_async_generator = kind == ProviderKind.ASYNC_GENERATOR
|
|
@@ -389,13 +459,14 @@ class Container:
|
|
|
389
459
|
self._set_provider(provider)
|
|
390
460
|
return provider
|
|
391
461
|
|
|
392
|
-
|
|
393
|
-
|
|
462
|
+
def _validate_provider_scope(
|
|
463
|
+
self, scope: Scope, name: str, is_resource: bool
|
|
464
|
+
) -> None:
|
|
394
465
|
"""Validate the provider scope."""
|
|
395
|
-
if scope not in
|
|
466
|
+
if scope not in self._scopes:
|
|
396
467
|
raise ValueError(
|
|
397
468
|
f"The provider `{name}` scope is invalid. Only the following "
|
|
398
|
-
f"scopes are supported: {', '.join(
|
|
469
|
+
f"scopes are supported: {', '.join(self._scopes.keys())}. "
|
|
399
470
|
"Please use one of the supported scopes when registering a provider."
|
|
400
471
|
)
|
|
401
472
|
if scope == "transient" and is_resource:
|
|
@@ -453,7 +453,7 @@ class Resolver:
|
|
|
453
453
|
create_lines.append(" context.enter(inst)")
|
|
454
454
|
|
|
455
455
|
create_lines.append(" if context is not None and store:")
|
|
456
|
-
create_lines.append(" context.
|
|
456
|
+
create_lines.append(" context._instances[_interface] = inst")
|
|
457
457
|
|
|
458
458
|
# Wrap instance if in override mode (only for override version)
|
|
459
459
|
if with_override:
|
|
@@ -470,18 +470,28 @@ class Resolver:
|
|
|
470
470
|
resolver_lines.append("def _resolver(container, context=None):")
|
|
471
471
|
|
|
472
472
|
# Only define NOT_SET_ if we actually need it
|
|
473
|
-
needs_not_set = scope
|
|
473
|
+
needs_not_set = scope != "transient"
|
|
474
474
|
if needs_not_set:
|
|
475
475
|
resolver_lines.append(" NOT_SET_ = _NOT_SET")
|
|
476
476
|
|
|
477
477
|
if scope == "singleton":
|
|
478
478
|
resolver_lines.append(" if context is None:")
|
|
479
479
|
resolver_lines.append(" context = container._singleton_context")
|
|
480
|
-
elif scope == "
|
|
481
|
-
resolver_lines.append(" if context is None:")
|
|
482
|
-
resolver_lines.append(" context = container._get_request_context()")
|
|
483
|
-
else:
|
|
480
|
+
elif scope == "transient":
|
|
484
481
|
resolver_lines.append(" context = None")
|
|
482
|
+
else:
|
|
483
|
+
# Custom scopes (including "request")
|
|
484
|
+
# Inline context retrieval to avoid method call overhead
|
|
485
|
+
resolver_lines.append(" if context is None:")
|
|
486
|
+
resolver_lines.append(" try:")
|
|
487
|
+
resolver_lines.append(" context = _scoped_context_var.get()")
|
|
488
|
+
resolver_lines.append(" except LookupError:")
|
|
489
|
+
resolver_lines.append(
|
|
490
|
+
f" raise LookupError("
|
|
491
|
+
f"'The {scope} context has not been started. "
|
|
492
|
+
f"Please ensure that the {scope} context is properly initialized "
|
|
493
|
+
f"before attempting to use it.')"
|
|
494
|
+
)
|
|
485
495
|
|
|
486
496
|
if scope == "singleton":
|
|
487
497
|
if with_override:
|
|
@@ -507,33 +517,36 @@ class Resolver:
|
|
|
507
517
|
store=True,
|
|
508
518
|
indent=" ",
|
|
509
519
|
)
|
|
510
|
-
elif scope == "
|
|
520
|
+
elif scope == "transient":
|
|
521
|
+
# Transient scope
|
|
511
522
|
if with_override:
|
|
512
|
-
self._add_override_check(resolver_lines)
|
|
513
|
-
|
|
514
|
-
# Fast path: check cached instance
|
|
515
|
-
resolver_lines.append(" inst = context.get(_interface)")
|
|
516
|
-
resolver_lines.append(" if inst is not NOT_SET_:")
|
|
517
|
-
resolver_lines.append(" return inst")
|
|
523
|
+
self._add_override_check(resolver_lines, include_not_set=True)
|
|
518
524
|
|
|
519
525
|
self._add_create_call(
|
|
520
526
|
resolver_lines,
|
|
521
527
|
is_async=is_async,
|
|
522
528
|
with_override=with_override,
|
|
523
|
-
context="
|
|
524
|
-
store=
|
|
529
|
+
context="",
|
|
530
|
+
store=False,
|
|
525
531
|
)
|
|
526
532
|
else:
|
|
527
|
-
#
|
|
533
|
+
# Custom scopes (including "request")
|
|
528
534
|
if with_override:
|
|
529
|
-
self._add_override_check(resolver_lines
|
|
535
|
+
self._add_override_check(resolver_lines)
|
|
536
|
+
|
|
537
|
+
# Fast path: check cached instance (inline dict access for speed)
|
|
538
|
+
resolver_lines.append(
|
|
539
|
+
" inst = context._instances.get(_interface, NOT_SET_)"
|
|
540
|
+
)
|
|
541
|
+
resolver_lines.append(" if inst is not NOT_SET_:")
|
|
542
|
+
resolver_lines.append(" return inst")
|
|
530
543
|
|
|
531
544
|
self._add_create_call(
|
|
532
545
|
resolver_lines,
|
|
533
546
|
is_async=is_async,
|
|
534
547
|
with_override=with_override,
|
|
535
|
-
context="",
|
|
536
|
-
store=
|
|
548
|
+
context="context",
|
|
549
|
+
store=True,
|
|
537
550
|
)
|
|
538
551
|
|
|
539
552
|
create_resolver_lines: list[str] = []
|
|
@@ -552,18 +565,26 @@ class Resolver:
|
|
|
552
565
|
|
|
553
566
|
if scope == "singleton":
|
|
554
567
|
create_resolver_lines.append(" context = container._singleton_context")
|
|
555
|
-
elif scope == "
|
|
568
|
+
elif scope == "transient":
|
|
569
|
+
create_resolver_lines.append(" context = None")
|
|
570
|
+
else:
|
|
571
|
+
# Custom scopes (including "request")
|
|
572
|
+
# Inline context retrieval to avoid method call overhead
|
|
573
|
+
create_resolver_lines.append(" try:")
|
|
574
|
+
create_resolver_lines.append(" context = _scoped_context_var.get()")
|
|
575
|
+
create_resolver_lines.append(" except LookupError:")
|
|
556
576
|
create_resolver_lines.append(
|
|
557
|
-
"
|
|
577
|
+
f" raise LookupError("
|
|
578
|
+
f"'The {scope} context has not been started. "
|
|
579
|
+
f"Please ensure that the {scope} context is properly initialized "
|
|
580
|
+
f"before attempting to use it.')"
|
|
558
581
|
)
|
|
559
|
-
else:
|
|
560
|
-
create_resolver_lines.append(" context = None")
|
|
561
582
|
|
|
562
583
|
if with_override:
|
|
563
584
|
self._add_override_check(create_resolver_lines, include_not_set=True)
|
|
564
585
|
|
|
565
586
|
# Determine context for create call
|
|
566
|
-
context_arg = "context" if scope
|
|
587
|
+
context_arg = "context" if scope != "transient" else ""
|
|
567
588
|
|
|
568
589
|
self._add_create_call(
|
|
569
590
|
create_resolver_lines,
|
|
@@ -603,6 +624,12 @@ class Resolver:
|
|
|
603
624
|
"resolver": self,
|
|
604
625
|
}
|
|
605
626
|
|
|
627
|
+
# For custom scopes, cache the ContextVar to avoid dictionary lookups
|
|
628
|
+
if scope not in ("singleton", "transient"):
|
|
629
|
+
ns["_scoped_context_var"] = self._container._get_scoped_context_var( # type: ignore[reportPrivateUsage]
|
|
630
|
+
scope
|
|
631
|
+
)
|
|
632
|
+
|
|
606
633
|
# Add async-specific namespace entries
|
|
607
634
|
if is_async:
|
|
608
635
|
ns["_asynccontextmanager"] = contextlib.asynccontextmanager
|
|
@@ -11,8 +11,8 @@ from fastapi.dependencies.models import Dependant
|
|
|
11
11
|
from fastapi.routing import APIRoute
|
|
12
12
|
from starlette.requests import Request
|
|
13
13
|
|
|
14
|
-
from anydi import Container
|
|
15
|
-
from anydi._types import
|
|
14
|
+
from anydi import Container, Inject
|
|
15
|
+
from anydi._types import ProvideMarker, set_provide_factory
|
|
16
16
|
|
|
17
17
|
from .starlette.middleware import RequestScopedMiddleware
|
|
18
18
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""AnyDI FastStream extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
8
|
+
|
|
9
|
+
from fast_depends.dependencies import Dependant
|
|
10
|
+
from faststream import BaseMiddleware, ContextRepo, StreamMessage
|
|
11
|
+
|
|
12
|
+
from anydi import Container
|
|
13
|
+
from anydi._types import Inject, ProvideMarker, set_provide_factory
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from faststream._internal.basic_types import AsyncFuncAny
|
|
17
|
+
from faststream._internal.broker import BrokerUsecase
|
|
18
|
+
from faststream._internal.types import AnyMsg
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"install",
|
|
22
|
+
"get_container",
|
|
23
|
+
"get_container_from_context",
|
|
24
|
+
"Inject",
|
|
25
|
+
"RequestScopedMiddleware",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_container(broker: BrokerUsecase[Any, Any]) -> Container:
|
|
30
|
+
"""Get the AnyDI container from a FastStream broker."""
|
|
31
|
+
return cast(Container, getattr(broker, "_container")) # noqa
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_container_from_context(context: ContextRepo) -> Container:
|
|
35
|
+
return get_container(context.broker)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RequestScopedMiddleware(BaseMiddleware):
|
|
39
|
+
@cached_property
|
|
40
|
+
def container(self) -> Container:
|
|
41
|
+
return get_container_from_context(self.context)
|
|
42
|
+
|
|
43
|
+
async def consume_scope(
|
|
44
|
+
self, call_next: AsyncFuncAny, msg: StreamMessage[AnyMsg]
|
|
45
|
+
) -> Any:
|
|
46
|
+
async with self.container.arequest_context():
|
|
47
|
+
return await call_next(msg)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _ProvideMarker(Dependant, ProvideMarker):
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
super().__init__(self._dependency, use_cache=True, cast=True, cast_result=True)
|
|
53
|
+
ProvideMarker.__init__(self)
|
|
54
|
+
|
|
55
|
+
async def _dependency(self, context: ContextRepo) -> Any:
|
|
56
|
+
container = get_container_from_context(context)
|
|
57
|
+
return await container.aresolve(self.interface)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Configure Inject() and Provide[T] to use FastStream-specific marker
|
|
61
|
+
set_provide_factory(_ProvideMarker)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_broker_handlers(broker: BrokerUsecase[Any, Any]) -> list[Any]:
|
|
65
|
+
return [subscriber.calls[0].handler for subscriber in broker.subscribers]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def install(broker: BrokerUsecase[Any, Any], container: Container) -> None:
|
|
69
|
+
"""Install AnyDI into a FastStream broker."""
|
|
70
|
+
broker._container = container # type: ignore
|
|
71
|
+
for handler in _get_broker_handlers(broker):
|
|
72
|
+
call = handler._original_call # noqa
|
|
73
|
+
for parameter in inspect.signature(call, eval_str=True).parameters.values():
|
|
74
|
+
container.validate_injected_parameter(parameter, call=call)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "anydi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.59.0"
|
|
4
4
|
description = "Dependency Injection library"
|
|
5
5
|
authors = [{ name = "Anton Ruhlov", email = "antonruhlov@gmail.com" }]
|
|
6
6
|
requires-python = ">=3.10.0, <3.15"
|
|
@@ -58,7 +58,7 @@ dev = [
|
|
|
58
58
|
"starlette>=0.37.2",
|
|
59
59
|
"fastapi>=0.100.0",
|
|
60
60
|
"httpx>=0.26.0",
|
|
61
|
-
"faststream>=0.
|
|
61
|
+
"faststream>=0.6,<0.7",
|
|
62
62
|
"redis>=5.0.4,<6",
|
|
63
63
|
"pydantic-settings>=2.4.0,<3",
|
|
64
64
|
]
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"""AnyDI FastStream extension."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import inspect
|
|
6
|
-
from typing import Any, cast
|
|
7
|
-
|
|
8
|
-
from fast_depends.dependencies import Depends
|
|
9
|
-
from faststream import ContextRepo
|
|
10
|
-
from faststream.broker.core.usecase import BrokerUsecase
|
|
11
|
-
|
|
12
|
-
from anydi import Container
|
|
13
|
-
from anydi._types import Inject, ProvideMarker, set_provide_factory
|
|
14
|
-
|
|
15
|
-
__all__ = ["install", "get_container", "Inject"]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def get_container(broker: BrokerUsecase[Any, Any]) -> Container:
|
|
19
|
-
"""Get the AnyDI container from a FastStream broker."""
|
|
20
|
-
return cast(Container, getattr(broker, "_container")) # noqa
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class _ProvideMarker(Depends, ProvideMarker):
|
|
24
|
-
def __init__(self) -> None:
|
|
25
|
-
super().__init__(dependency=self._dependency, use_cache=True, cast=True)
|
|
26
|
-
ProvideMarker.__init__(self)
|
|
27
|
-
|
|
28
|
-
async def _dependency(self, context: ContextRepo) -> Any:
|
|
29
|
-
container = get_container(context.get("broker"))
|
|
30
|
-
return await container.aresolve(self.interface)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# Configure Inject() and Provide[T] to use FastStream-specific marker
|
|
34
|
-
set_provide_factory(_ProvideMarker)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _get_broker_handlers(broker: BrokerUsecase[Any, Any]) -> list[Any]:
|
|
38
|
-
if (handlers := getattr(broker, "handlers", None)) is not None:
|
|
39
|
-
return [handler.calls[0][0] for handler in handlers.values()]
|
|
40
|
-
return [
|
|
41
|
-
subscriber.calls[0].handler
|
|
42
|
-
for subscriber in broker._subscribers.values() # noqa
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def install(broker: BrokerUsecase[Any, Any], container: Container) -> None:
|
|
47
|
-
"""Install AnyDI into a FastStream broker."""
|
|
48
|
-
broker._container = container # type: ignore
|
|
49
|
-
for handler in _get_broker_handlers(broker):
|
|
50
|
-
call = handler._original_call # noqa
|
|
51
|
-
for parameter in inspect.signature(call, eval_str=True).parameters.values():
|
|
52
|
-
container.validate_injected_parameter(parameter, call=call)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|