anydi 0.62.0__py3-none-any.whl → 0.64.0__py3-none-any.whl
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/_container.py +35 -1
- anydi/_resolver.py +9 -5
- anydi/ext/fastapi.py +17 -8
- anydi/ext/starlette/middleware.py +39 -13
- {anydi-0.62.0.dist-info → anydi-0.64.0.dist-info}/METADATA +3 -2
- {anydi-0.62.0.dist-info → anydi-0.64.0.dist-info}/RECORD +8 -8
- {anydi-0.62.0.dist-info → anydi-0.64.0.dist-info}/WHEEL +0 -0
- {anydi-0.62.0.dist-info → anydi-0.64.0.dist-info}/entry_points.txt +0 -0
anydi/_container.py
CHANGED
|
@@ -272,6 +272,10 @@ class Container:
|
|
|
272
272
|
# Register the scope
|
|
273
273
|
self._scopes[scope] = tuple({scope, "singleton"} | set(parents))
|
|
274
274
|
|
|
275
|
+
def has_scope(self, scope: str) -> bool:
|
|
276
|
+
"""Check if a scope is registered."""
|
|
277
|
+
return scope in self._scopes
|
|
278
|
+
|
|
275
279
|
def get_context_scopes(self, scopes: set[Scope] | None = None) -> list[str]: # noqa: C901
|
|
276
280
|
"""Return scopes that require context management in dependency order."""
|
|
277
281
|
# Build execution order: singleton -> request -> custom (by depth)
|
|
@@ -431,6 +435,10 @@ class Container:
|
|
|
431
435
|
parameters: list[ProviderParameter] = []
|
|
432
436
|
scope_provider: dict[Scope, Provider] = {}
|
|
433
437
|
|
|
438
|
+
# Precompute constant checks
|
|
439
|
+
is_scoped = scope not in ("singleton", "transient")
|
|
440
|
+
has_defaults = defaults is not None
|
|
441
|
+
|
|
434
442
|
for parameter in signature.parameters.values():
|
|
435
443
|
if parameter.annotation is inspect.Parameter.empty:
|
|
436
444
|
raise TypeError(
|
|
@@ -450,10 +458,32 @@ class Container:
|
|
|
450
458
|
)
|
|
451
459
|
has_default = default is not NOT_SET
|
|
452
460
|
|
|
461
|
+
# Check if provider exists before attempting to register (for scoped only)
|
|
462
|
+
was_auto_registered = (
|
|
463
|
+
is_scoped and parameter.annotation not in self._providers
|
|
464
|
+
)
|
|
465
|
+
|
|
453
466
|
try:
|
|
454
467
|
sub_provider = self._get_or_register_provider(parameter.annotation)
|
|
455
468
|
except LookupError as exc:
|
|
456
|
-
if parameter.name in defaults
|
|
469
|
+
if (has_defaults and parameter.name in defaults) or has_default:
|
|
470
|
+
continue
|
|
471
|
+
# For request/custom scopes, allow unregistered dependencies
|
|
472
|
+
# They might be provided via context.set()
|
|
473
|
+
if is_scoped:
|
|
474
|
+
# Add to unresolved list to provide better error messages
|
|
475
|
+
# and prevent infinite recursion
|
|
476
|
+
self._resolver.add_unresolved(parameter.annotation)
|
|
477
|
+
parameters.append(
|
|
478
|
+
ProviderParameter(
|
|
479
|
+
name=parameter.name,
|
|
480
|
+
annotation=parameter.annotation,
|
|
481
|
+
default=default,
|
|
482
|
+
has_default=has_default,
|
|
483
|
+
provider=None, # Will check context at runtime
|
|
484
|
+
shared_scope=True, # Same scope, check context
|
|
485
|
+
)
|
|
486
|
+
)
|
|
457
487
|
continue
|
|
458
488
|
unresolved_parameter = parameter
|
|
459
489
|
unresolved_exc = exc
|
|
@@ -463,6 +493,10 @@ class Container:
|
|
|
463
493
|
if sub_provider.scope not in scope_provider:
|
|
464
494
|
scope_provider[sub_provider.scope] = sub_provider
|
|
465
495
|
|
|
496
|
+
# If provider was auto-registered and has same scope, mark as unresolved
|
|
497
|
+
if was_auto_registered and sub_provider.scope == scope:
|
|
498
|
+
self._resolver.add_unresolved(parameter.annotation)
|
|
499
|
+
|
|
466
500
|
parameters.append(
|
|
467
501
|
ProviderParameter(
|
|
468
502
|
name=parameter.name,
|
anydi/_resolver.py
CHANGED
|
@@ -228,11 +228,15 @@ class Resolver:
|
|
|
228
228
|
)
|
|
229
229
|
create_lines.append(f" arg_{idx} = defaults['{name}']")
|
|
230
230
|
create_lines.append(" else:")
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
231
|
+
# Direct dict access for shared scope params (avoids method call)
|
|
232
|
+
if param_shared_scopes[idx]:
|
|
233
|
+
create_lines.append(
|
|
234
|
+
f" cached = (context._instances.get("
|
|
235
|
+
f"_param_annotations[{idx}], NOT_SET_) "
|
|
236
|
+
f"if context is not None else NOT_SET_)"
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
create_lines.append(" cached = NOT_SET_")
|
|
236
240
|
create_lines.append(" if cached is NOT_SET_:")
|
|
237
241
|
create_lines.append(
|
|
238
242
|
f" if _param_annotations[{idx}] in "
|
anydi/ext/fastapi.py
CHANGED
|
@@ -8,8 +8,8 @@ from typing import Annotated, Any, cast
|
|
|
8
8
|
|
|
9
9
|
from fastapi import Depends, FastAPI, params
|
|
10
10
|
from fastapi.dependencies.models import Dependant
|
|
11
|
-
from fastapi.routing import APIRoute
|
|
12
|
-
from starlette.requests import
|
|
11
|
+
from fastapi.routing import APIRoute, APIWebSocketRoute
|
|
12
|
+
from starlette.requests import HTTPConnection
|
|
13
13
|
|
|
14
14
|
from anydi import Container, Inject
|
|
15
15
|
from anydi._marker import Marker, extend_marker
|
|
@@ -19,9 +19,8 @@ from .starlette.middleware import RequestScopedMiddleware
|
|
|
19
19
|
__all__ = ["install", "get_container", "Inject", "RequestScopedMiddleware"]
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def get_container(
|
|
23
|
-
|
|
24
|
-
return cast(Container, request.app.state.container)
|
|
22
|
+
def get_container(connection: HTTPConnection) -> Container:
|
|
23
|
+
return cast(Container, connection.app.state.container)
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
class FastAPIMarker(params.Depends, Marker):
|
|
@@ -39,7 +38,9 @@ class FastAPIMarker(params.Depends, Marker):
|
|
|
39
38
|
return await container.aresolve(self.interface)
|
|
40
39
|
|
|
41
40
|
|
|
42
|
-
# Configure Inject() and Provide[T] to use FastAPI-specific marker
|
|
41
|
+
# Configure Inject() and Provide[T] to use FastAPI-specific marker at import time
|
|
42
|
+
# This is also called in install() to ensure it's set correctly even if other
|
|
43
|
+
# extensions have overwritten it
|
|
43
44
|
extend_marker(FastAPIMarker)
|
|
44
45
|
|
|
45
46
|
|
|
@@ -51,7 +52,9 @@ def _iter_dependencies(dependant: Dependant) -> Iterator[Dependant]:
|
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
def _validate_route_dependencies(
|
|
54
|
-
route: APIRoute
|
|
55
|
+
route: APIRoute | APIWebSocketRoute,
|
|
56
|
+
container: Container,
|
|
57
|
+
patched: set[tuple[Any, ...]],
|
|
55
58
|
) -> None:
|
|
56
59
|
for dependant in _iter_dependencies(route.dependant):
|
|
57
60
|
if dependant.cache_key in patched:
|
|
@@ -71,8 +74,14 @@ def _validate_route_dependencies(
|
|
|
71
74
|
def install(app: FastAPI, container: Container) -> None:
|
|
72
75
|
"""Install AnyDI into a FastAPI application."""
|
|
73
76
|
app.state.container = container # noqa
|
|
77
|
+
|
|
78
|
+
# Register websocket scope with request as parent if not already registered
|
|
79
|
+
if not container.has_scope("websocket"):
|
|
80
|
+
container.register_scope("websocket", parents=["request"])
|
|
81
|
+
|
|
82
|
+
# Validate routes (both HTTP and WebSocket)
|
|
74
83
|
patched: set[tuple[Any, ...]] = set()
|
|
75
84
|
for route in app.routes:
|
|
76
|
-
if not isinstance(route, APIRoute):
|
|
85
|
+
if not isinstance(route, APIRoute | APIWebSocketRoute):
|
|
77
86
|
continue
|
|
78
87
|
_validate_route_dependencies(route, container, patched)
|
|
@@ -1,23 +1,49 @@
|
|
|
1
|
-
"""Starlette
|
|
1
|
+
"""Starlette middleware for AnyDI scoped contexts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import AsyncExitStack
|
|
2
6
|
|
|
3
|
-
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
4
7
|
from starlette.requests import Request
|
|
5
|
-
from starlette.
|
|
6
|
-
from starlette.
|
|
8
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
9
|
+
from starlette.websockets import WebSocket
|
|
7
10
|
|
|
8
11
|
from anydi import Container
|
|
9
12
|
|
|
10
13
|
|
|
11
|
-
class RequestScopedMiddleware
|
|
12
|
-
"""
|
|
14
|
+
class RequestScopedMiddleware:
|
|
15
|
+
"""ASGI middleware for managing request-scoped AnyDI context."""
|
|
13
16
|
|
|
14
17
|
def __init__(self, app: ASGIApp, container: Container) -> None:
|
|
15
|
-
|
|
18
|
+
self.app = app
|
|
16
19
|
self.container = container
|
|
17
20
|
|
|
18
|
-
async def
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
22
|
+
# Only handle HTTP requests or WebSocket connections
|
|
23
|
+
if scope["type"] not in {"http", "websocket"}:
|
|
24
|
+
await self.app(scope, receive, send)
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
async with AsyncExitStack() as stack:
|
|
28
|
+
# Create request context first (parent scope)
|
|
29
|
+
request_context = await stack.enter_async_context(
|
|
30
|
+
self.container.arequest_context()
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# For WebSocket connections, create websocket context (child scope)
|
|
34
|
+
websocket_context = None
|
|
35
|
+
if scope["type"] == "websocket" and self.container.has_scope("websocket"):
|
|
36
|
+
websocket_context = await stack.enter_async_context(
|
|
37
|
+
self.container.ascoped_context("websocket")
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if scope["type"] == "http":
|
|
41
|
+
request = Request(scope, receive=receive, send=send)
|
|
42
|
+
request_context.set(Request, request)
|
|
43
|
+
else:
|
|
44
|
+
websocket = WebSocket(scope, receive=receive, send=send)
|
|
45
|
+
request_context.set(WebSocket, websocket)
|
|
46
|
+
if websocket_context is not None:
|
|
47
|
+
websocket_context.set(WebSocket, websocket)
|
|
48
|
+
|
|
49
|
+
await self.app(scope, receive, send)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anydi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.64.0
|
|
4
4
|
Summary: Dependency Injection library
|
|
5
5
|
Keywords: dependency injection,dependencies,di,async,asyncio,application
|
|
6
6
|
Author: Anton Ruhlov
|
|
@@ -42,6 +42,7 @@ Modern, lightweight Dependency Injection library using type annotations.
|
|
|
42
42
|
[](https://github.com/antonrh/anydi/actions/workflows/ci.yml)
|
|
43
43
|
[](https://codecov.io/gh/antonrh/anydi)
|
|
44
44
|
[](https://anydi.readthedocs.io/en/latest/)
|
|
45
|
+
[](https://codspeed.io/antonrh/anydi?utm_source=badge)
|
|
45
46
|
|
|
46
47
|
</div>
|
|
47
48
|
|
|
@@ -229,7 +230,7 @@ Configure Django (`settings.py`):
|
|
|
229
230
|
|
|
230
231
|
```python
|
|
231
232
|
INSTALLED_APPS = [
|
|
232
|
-
|
|
233
|
+
...,
|
|
233
234
|
"anydi_django",
|
|
234
235
|
]
|
|
235
236
|
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
anydi/__init__.py,sha256=KFX8OthKXwBuYDPCV61t-044DpJ88tAOzIxeUWRC5OA,633
|
|
2
2
|
anydi/_async_lock.py,sha256=3dwZr0KthXFYha0XKMyXf8jMmGb1lYoNC0O5w29V9ic,1104
|
|
3
|
-
anydi/_container.py,sha256=
|
|
3
|
+
anydi/_container.py,sha256=VbC5WqVH78TtaPx3JbSyZP8gZA4NbPNuwEiJXZSDSks,29561
|
|
4
4
|
anydi/_context.py,sha256=-9QqeMWo9OpZVXZxZCQgIsswggl3Ch7lgx1KiFX_ezc,3752
|
|
5
5
|
anydi/_decorators.py,sha256=J3W261ZAG7q4XKm4tbAv1wsWr9ysx9_5MUbUvSJB_MQ,2809
|
|
6
6
|
anydi/_injector.py,sha256=1Ux71DhGxu3dLwPJP8gU73olI0pcZ3_tVaVzwKH7100,4411
|
|
7
7
|
anydi/_marker.py,sha256=xVydjGdkxd_DqqwttnJZRkQbhpCTE9OnrhFmFJMlgvI,3415
|
|
8
8
|
anydi/_module.py,sha256=2kN5uEXLd2Dsc58gz5IWK43wJewr_QgIVGSO3iWp798,2609
|
|
9
9
|
anydi/_provider.py,sha256=OV1WFHTYv7W2U0XDk_Kql1r551Vhq8o-pUV5ep1HQcU,1574
|
|
10
|
-
anydi/_resolver.py,sha256=
|
|
10
|
+
anydi/_resolver.py,sha256=fh8pB6gDZ-jWXh_q2QLM--g9tWZ0Dyx8MPjGcLVyiOw,30428
|
|
11
11
|
anydi/_scanner.py,sha256=rbRkHzyd2zMu7AFLffN6_tZJcMaW9gy7E-lVdHLHYrs,4294
|
|
12
12
|
anydi/_types.py,sha256=lsShY_-_CM2EFajeknAYXvLl-rHfopBT8udnK5_BtS4,1161
|
|
13
13
|
anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
anydi/ext/django/__init__.py,sha256=Ve8lncLU9dPY_Vjt4zihPgsSxwAtFHACn0XvBM5JG8k,367
|
|
15
|
-
anydi/ext/fastapi.py,sha256=
|
|
15
|
+
anydi/ext/fastapi.py,sha256=px6gdRVIE8-thDy93Zcsd_xJF3H09n1nn7FKKemdiig,2958
|
|
16
16
|
anydi/ext/faststream.py,sha256=yszUfSbo3vJ2tr9PNC2GR-uX1XgRSXGH2lvLNoAXkc4,2738
|
|
17
17
|
anydi/ext/pydantic_settings.py,sha256=jVJZ1wPaPpsxdNPlJj9yq282ebqLZ9tckWpZ0eIwWLg,1533
|
|
18
18
|
anydi/ext/pytest_plugin.py,sha256=M54DkA-KxD9GqLnXdoCyn-Qur2c44MB6d0AgJuYCZ5w,16171
|
|
19
19
|
anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
anydi/ext/starlette/middleware.py,sha256=
|
|
20
|
+
anydi/ext/starlette/middleware.py,sha256=n_JJ7BcG2Mg2M5HwM_SBboxZ-mnnD6WWJn4khq7Bgbs,1860
|
|
21
21
|
anydi/ext/typer.py,sha256=z-sDd3jZMPTE2CyEuJ0f9uIJB43FjoLWbjpnkOvqSKA,6236
|
|
22
22
|
anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
anydi/testing.py,sha256=cHg3mMScZbEep9smRqSNQ81BZMQOkyugHe8TvKdPnEg,1347
|
|
24
|
-
anydi-0.
|
|
25
|
-
anydi-0.
|
|
26
|
-
anydi-0.
|
|
27
|
-
anydi-0.
|
|
24
|
+
anydi-0.64.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
25
|
+
anydi-0.64.0.dist-info/entry_points.txt,sha256=AgOcQYM5KyS4D37QcYb00tiid0QA-pD1VrjHHq4QAps,44
|
|
26
|
+
anydi-0.64.0.dist-info/METADATA,sha256=0Ic_f7pbgpkUG3Mp11D1uSJ8VJ98vfPkY6x3_-_w5rU,8142
|
|
27
|
+
anydi-0.64.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|