anydi 0.63.0__tar.gz → 0.65.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.63.0 → anydi-0.65.0}/PKG-INFO +3 -2
- {anydi-0.63.0 → anydi-0.65.0}/README.md +2 -1
- {anydi-0.63.0 → anydi-0.65.0}/anydi/_container.py +9 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/_resolver.py +34 -16
- {anydi-0.63.0 → anydi-0.65.0}/anydi/ext/fastapi.py +17 -8
- anydi-0.65.0/anydi/ext/starlette/middleware.py +49 -0
- {anydi-0.63.0 → anydi-0.65.0}/pyproject.toml +2 -1
- anydi-0.63.0/anydi/ext/starlette/middleware.py +0 -23
- {anydi-0.63.0 → anydi-0.65.0}/anydi/__init__.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/_async_lock.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/_context.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/_decorators.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/_injector.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/_marker.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/_module.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/_provider.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/_scanner.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/_types.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/ext/__init__.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/ext/django/__init__.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/ext/faststream.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/ext/pydantic_settings.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/ext/pytest_plugin.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/ext/starlette/__init__.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/ext/typer.py +0 -0
- {anydi-0.63.0 → anydi-0.65.0}/anydi/py.typed +0 -0
- {anydi-0.63.0 → anydi-0.65.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.65.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
|
|
|
@@ -7,6 +7,7 @@ Modern, lightweight Dependency Injection library using type annotations.
|
|
|
7
7
|
[](https://github.com/antonrh/anydi/actions/workflows/ci.yml)
|
|
8
8
|
[](https://codecov.io/gh/antonrh/anydi)
|
|
9
9
|
[](https://anydi.readthedocs.io/en/latest/)
|
|
10
|
+
[](https://codspeed.io/antonrh/anydi?utm_source=badge)
|
|
10
11
|
|
|
11
12
|
</div>
|
|
12
13
|
|
|
@@ -194,7 +195,7 @@ Configure Django (`settings.py`):
|
|
|
194
195
|
|
|
195
196
|
```python
|
|
196
197
|
INSTALLED_APPS = [
|
|
197
|
-
|
|
198
|
+
...,
|
|
198
199
|
"anydi_django",
|
|
199
200
|
]
|
|
200
201
|
|
|
@@ -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)
|
|
@@ -549,6 +553,11 @@ class Container:
|
|
|
549
553
|
)
|
|
550
554
|
|
|
551
555
|
self._set_provider(provider)
|
|
556
|
+
|
|
557
|
+
# Clear cached resolvers when overriding
|
|
558
|
+
if override:
|
|
559
|
+
self._resolver.clear_caches()
|
|
560
|
+
|
|
552
561
|
return provider
|
|
553
562
|
|
|
554
563
|
def _validate_provider_scope(
|
|
@@ -62,6 +62,13 @@ class Resolver:
|
|
|
62
62
|
def add_unresolved(self, interface: Any) -> None:
|
|
63
63
|
self._unresolved_interfaces.add(interface)
|
|
64
64
|
|
|
65
|
+
def clear_caches(self) -> None:
|
|
66
|
+
"""Clear all cached resolvers."""
|
|
67
|
+
self._cache.clear()
|
|
68
|
+
self._async_cache.clear()
|
|
69
|
+
self._override_cache.clear()
|
|
70
|
+
self._async_override_cache.clear()
|
|
71
|
+
|
|
65
72
|
def get_cached(self, interface: Any, *, is_async: bool) -> CompiledResolver | None:
|
|
66
73
|
"""Get cached resolver if it exists."""
|
|
67
74
|
if self.override_mode:
|
|
@@ -83,9 +90,14 @@ class Resolver:
|
|
|
83
90
|
return cache[provider.interface]
|
|
84
91
|
|
|
85
92
|
# Recursively compile dependencies first
|
|
86
|
-
for
|
|
87
|
-
if
|
|
88
|
-
|
|
93
|
+
for param in provider.parameters:
|
|
94
|
+
if param.provider is not None:
|
|
95
|
+
# Look up the current provider to handle overrides
|
|
96
|
+
current_provider = self._container.providers.get(param.annotation)
|
|
97
|
+
if current_provider is not None:
|
|
98
|
+
self.compile(current_provider, is_async=is_async)
|
|
99
|
+
else:
|
|
100
|
+
self.compile(param.provider, is_async=is_async)
|
|
89
101
|
|
|
90
102
|
# Compile the resolver and creator functions
|
|
91
103
|
compiled = self._compile_resolver(
|
|
@@ -155,23 +167,29 @@ class Resolver:
|
|
|
155
167
|
else (self._async_cache if is_async else self._cache)
|
|
156
168
|
)
|
|
157
169
|
|
|
158
|
-
for idx,
|
|
159
|
-
param_annotations[idx] =
|
|
160
|
-
param_defaults[idx] =
|
|
161
|
-
param_has_default[idx] =
|
|
162
|
-
param_names[idx] =
|
|
163
|
-
param_shared_scopes[idx] =
|
|
164
|
-
|
|
165
|
-
if
|
|
166
|
-
|
|
170
|
+
for idx, param in enumerate(provider.parameters):
|
|
171
|
+
param_annotations[idx] = param.annotation
|
|
172
|
+
param_defaults[idx] = param.default
|
|
173
|
+
param_has_default[idx] = param.has_default
|
|
174
|
+
param_names[idx] = param.name
|
|
175
|
+
param_shared_scopes[idx] = param.shared_scope
|
|
176
|
+
|
|
177
|
+
if param.provider is not None:
|
|
178
|
+
# Look up the current provider from the container to handle overrides
|
|
179
|
+
current_provider = self._container.providers.get(param.annotation)
|
|
180
|
+
if current_provider is not None:
|
|
181
|
+
compiled = cache.get(current_provider.interface)
|
|
182
|
+
else:
|
|
183
|
+
# Fallback to the original provider if not in container
|
|
184
|
+
compiled = cache.get(param.provider.interface)
|
|
167
185
|
if compiled is None:
|
|
168
|
-
compiled = self.compile(
|
|
169
|
-
cache[
|
|
186
|
+
compiled = self.compile(param.provider, is_async=is_async)
|
|
187
|
+
cache[param.provider.interface] = compiled
|
|
170
188
|
param_resolvers[idx] = compiled.resolve
|
|
171
189
|
|
|
172
190
|
unresolved_message = (
|
|
173
|
-
f"You are attempting to get the parameter `{
|
|
174
|
-
f"annotation `{type_repr(
|
|
191
|
+
f"You are attempting to get the parameter `{param.name}` with the "
|
|
192
|
+
f"annotation `{type_repr(param.annotation)}` as a dependency into "
|
|
175
193
|
f"`{type_repr(provider.call)}` which is not registered or set in the "
|
|
176
194
|
"scoped context."
|
|
177
195
|
)
|
|
@@ -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)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Starlette middleware for AnyDI scoped contexts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import AsyncExitStack
|
|
6
|
+
|
|
7
|
+
from starlette.requests import Request
|
|
8
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
9
|
+
from starlette.websockets import WebSocket
|
|
10
|
+
|
|
11
|
+
from anydi import Container
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RequestScopedMiddleware:
|
|
15
|
+
"""ASGI middleware for managing request-scoped AnyDI context."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, app: ASGIApp, container: Container) -> None:
|
|
18
|
+
self.app = app
|
|
19
|
+
self.container = container
|
|
20
|
+
|
|
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
|
[project]
|
|
2
2
|
name = "anydi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.65.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"
|
|
@@ -55,6 +55,7 @@ dev = [
|
|
|
55
55
|
"pytest>=8.4.0,<9",
|
|
56
56
|
"pytest-cov>=7.0.0",
|
|
57
57
|
"pytest-mock>=3.14.1",
|
|
58
|
+
"pytest-codspeed>=4.2.0",
|
|
58
59
|
"starlette>=0.37.2",
|
|
59
60
|
"fastapi>=0.100.0",
|
|
60
61
|
"httpx>=0.26.0",
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""Starlette RequestScopedMiddleware."""
|
|
2
|
-
|
|
3
|
-
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
4
|
-
from starlette.requests import Request
|
|
5
|
-
from starlette.responses import Response
|
|
6
|
-
from starlette.types import ASGIApp
|
|
7
|
-
|
|
8
|
-
from anydi import Container
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class RequestScopedMiddleware(BaseHTTPMiddleware):
|
|
12
|
-
"""Starlette middleware for managing request-scoped AnyDI context."""
|
|
13
|
-
|
|
14
|
-
def __init__(self, app: ASGIApp, container: Container) -> None:
|
|
15
|
-
super().__init__(app)
|
|
16
|
-
self.container = container
|
|
17
|
-
|
|
18
|
-
async def dispatch(
|
|
19
|
-
self, request: Request, call_next: RequestResponseEndpoint
|
|
20
|
-
) -> Response:
|
|
21
|
-
async with self.container.arequest_context() as context:
|
|
22
|
-
context.set(Request, request)
|
|
23
|
-
return await call_next(request)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|