anydi 0.26.8a0__py3-none-any.whl → 0.27.0a0__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 +32 -24
- anydi/_context.py +13 -36
- anydi/_types.py +0 -11
- anydi/_utils.py +15 -16
- anydi/ext/_utils.py +89 -0
- anydi/ext/fastapi.py +5 -78
- anydi/ext/faststream.py +50 -0
- anydi/ext/pytest_plugin.py +22 -17
- {anydi-0.26.8a0.dist-info → anydi-0.27.0a0.dist-info}/METADATA +2 -1
- {anydi-0.26.8a0.dist-info → anydi-0.27.0a0.dist-info}/RECORD +13 -11
- {anydi-0.26.8a0.dist-info → anydi-0.27.0a0.dist-info}/WHEEL +1 -1
- {anydi-0.26.8a0.dist-info → anydi-0.27.0a0.dist-info}/LICENSE +0 -0
- {anydi-0.26.8a0.dist-info → anydi-0.27.0a0.dist-info}/entry_points.txt +0 -0
anydi/_container.py
CHANGED
|
@@ -11,9 +11,11 @@ from contextvars import ContextVar
|
|
|
11
11
|
from functools import wraps
|
|
12
12
|
from typing import (
|
|
13
13
|
Any,
|
|
14
|
+
AsyncContextManager,
|
|
14
15
|
AsyncIterator,
|
|
15
16
|
Awaitable,
|
|
16
17
|
Callable,
|
|
18
|
+
ContextManager,
|
|
17
19
|
Iterable,
|
|
18
20
|
Iterator,
|
|
19
21
|
Mapping,
|
|
@@ -41,7 +43,7 @@ from ._context import (
|
|
|
41
43
|
from ._logger import logger
|
|
42
44
|
from ._module import Module, ModuleRegistry
|
|
43
45
|
from ._scanner import Scanner
|
|
44
|
-
from ._types import AnyInterface,
|
|
46
|
+
from ._types import AnyInterface, Interface, Provider, Scope, is_marker
|
|
45
47
|
from ._utils import (
|
|
46
48
|
get_full_qualname,
|
|
47
49
|
get_typed_parameters,
|
|
@@ -84,7 +86,7 @@ class Container:
|
|
|
84
86
|
strict: Whether to enable strict mode. Defaults to False.
|
|
85
87
|
"""
|
|
86
88
|
self._providers: dict[type[Any], Provider] = {}
|
|
87
|
-
self.
|
|
89
|
+
self._providers_cache: dict[Scope, list[type[Any]]] = defaultdict(list)
|
|
88
90
|
self._singleton_context = SingletonContext(self)
|
|
89
91
|
self._transient_context = TransientContext(self)
|
|
90
92
|
self._request_context_var: ContextVar[RequestContext | None] = ContextVar(
|
|
@@ -170,7 +172,7 @@ class Container:
|
|
|
170
172
|
|
|
171
173
|
# Create Event type
|
|
172
174
|
if provider.is_resource and (interface is NoneType or interface is None):
|
|
173
|
-
interface = type(f"Event_{uuid.uuid4().hex}", (
|
|
175
|
+
interface = type(f"Event_{uuid.uuid4().hex}", (), {})
|
|
174
176
|
|
|
175
177
|
if interface in self._providers:
|
|
176
178
|
if override:
|
|
@@ -223,7 +225,7 @@ class Container:
|
|
|
223
225
|
scoped_context.delete(interface)
|
|
224
226
|
|
|
225
227
|
# Cleanup provider references
|
|
226
|
-
self.
|
|
228
|
+
self._providers.pop(interface, None)
|
|
227
229
|
|
|
228
230
|
def _get_provider(self, interface: AnyInterface) -> Provider:
|
|
229
231
|
"""Get provider by interface.
|
|
@@ -284,17 +286,7 @@ class Container:
|
|
|
284
286
|
"""
|
|
285
287
|
self._providers[interface] = provider
|
|
286
288
|
if provider.is_resource:
|
|
287
|
-
self.
|
|
288
|
-
|
|
289
|
-
def _delete_provider(self, interface: AnyInterface) -> None:
|
|
290
|
-
"""Delete a provider by interface.
|
|
291
|
-
|
|
292
|
-
Args:
|
|
293
|
-
interface: The interface for which to delete the provider.
|
|
294
|
-
"""
|
|
295
|
-
provider = self._providers.pop(interface, None)
|
|
296
|
-
if provider is not None and provider.is_resource:
|
|
297
|
-
self._resource_cache[provider.scope].remove(interface)
|
|
289
|
+
self._providers_cache[provider.scope].append(interface)
|
|
298
290
|
|
|
299
291
|
def _validate_provider_scope(self, provider: Provider) -> None:
|
|
300
292
|
"""Validate the scope of a provider.
|
|
@@ -429,9 +421,9 @@ class Container:
|
|
|
429
421
|
exc_type: type[BaseException] | None,
|
|
430
422
|
exc_val: BaseException | None,
|
|
431
423
|
exc_tb: types.TracebackType | None,
|
|
432
|
-
) ->
|
|
424
|
+
) -> None:
|
|
433
425
|
"""Exit the singleton context."""
|
|
434
|
-
|
|
426
|
+
self.close()
|
|
435
427
|
|
|
436
428
|
def start(self) -> None:
|
|
437
429
|
"""Start the singleton context."""
|
|
@@ -441,13 +433,20 @@ class Container:
|
|
|
441
433
|
"""Close the singleton context."""
|
|
442
434
|
self._singleton_context.close()
|
|
443
435
|
|
|
444
|
-
|
|
445
|
-
def request_context(self) -> Iterator[None]:
|
|
436
|
+
def request_context(self) -> ContextManager[None]:
|
|
446
437
|
"""Obtain a context manager for the request-scoped context.
|
|
447
438
|
|
|
448
439
|
Returns:
|
|
449
440
|
A context manager for the request-scoped context.
|
|
450
441
|
"""
|
|
442
|
+
return contextlib.contextmanager(self._request_context)()
|
|
443
|
+
|
|
444
|
+
def _request_context(self) -> Iterator[None]:
|
|
445
|
+
"""Internal method that manages the request-scoped context.
|
|
446
|
+
|
|
447
|
+
Yields:
|
|
448
|
+
Yield control to the code block within the request context.
|
|
449
|
+
"""
|
|
451
450
|
context = RequestContext(self)
|
|
452
451
|
token = self._request_context_var.set(context)
|
|
453
452
|
with context:
|
|
@@ -464,25 +463,34 @@ class Container:
|
|
|
464
463
|
exc_type: type[BaseException] | None,
|
|
465
464
|
exc_val: BaseException | None,
|
|
466
465
|
exc_tb: types.TracebackType | None,
|
|
467
|
-
) ->
|
|
466
|
+
) -> None:
|
|
468
467
|
"""Exit the singleton context."""
|
|
469
|
-
|
|
468
|
+
await self.aclose()
|
|
470
469
|
|
|
471
470
|
async def astart(self) -> None:
|
|
472
471
|
"""Start the singleton context asynchronously."""
|
|
473
|
-
|
|
472
|
+
for interface, provider in self._providers.items():
|
|
473
|
+
if provider.scope == "singleton":
|
|
474
|
+
await self.aresolve(interface) # noqa
|
|
474
475
|
|
|
475
476
|
async def aclose(self) -> None:
|
|
476
477
|
"""Close the singleton context asynchronously."""
|
|
477
478
|
await self._singleton_context.aclose()
|
|
478
479
|
|
|
479
|
-
|
|
480
|
-
async def arequest_context(self) -> AsyncIterator[None]:
|
|
480
|
+
def arequest_context(self) -> AsyncContextManager[None]:
|
|
481
481
|
"""Obtain an async context manager for the request-scoped context.
|
|
482
482
|
|
|
483
483
|
Returns:
|
|
484
484
|
An async context manager for the request-scoped context.
|
|
485
485
|
"""
|
|
486
|
+
return contextlib.asynccontextmanager(self._arequest_context)()
|
|
487
|
+
|
|
488
|
+
async def _arequest_context(self) -> AsyncIterator[None]:
|
|
489
|
+
"""Internal method that manages the async request-scoped context.
|
|
490
|
+
|
|
491
|
+
Yields:
|
|
492
|
+
Yield control to the code block within the request context.
|
|
493
|
+
"""
|
|
486
494
|
context = RequestContext(self)
|
|
487
495
|
token = self._request_context_var.set(context)
|
|
488
496
|
async with context:
|
anydi/_context.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast
|
|
|
7
7
|
|
|
8
8
|
from typing_extensions import Self, final
|
|
9
9
|
|
|
10
|
-
from ._types import AnyInterface, Interface, Provider, Scope
|
|
10
|
+
from ._types import AnyInterface, Interface, Provider, Scope
|
|
11
11
|
from ._utils import run_async
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
@@ -243,7 +243,7 @@ class ResourceScopedContext(ScopedContext):
|
|
|
243
243
|
exc_type: type[BaseException] | None,
|
|
244
244
|
exc_val: BaseException | None,
|
|
245
245
|
exc_tb: TracebackType | None,
|
|
246
|
-
) ->
|
|
246
|
+
) -> None:
|
|
247
247
|
"""Exit the context.
|
|
248
248
|
|
|
249
249
|
Args:
|
|
@@ -251,17 +251,17 @@ class ResourceScopedContext(ScopedContext):
|
|
|
251
251
|
exc_val: The exception instance, if any.
|
|
252
252
|
exc_tb: The traceback, if any.
|
|
253
253
|
"""
|
|
254
|
-
|
|
254
|
+
self.close()
|
|
255
|
+
return
|
|
255
256
|
|
|
256
|
-
@abc.abstractmethod
|
|
257
257
|
def start(self) -> None:
|
|
258
258
|
"""Start the scoped context."""
|
|
259
|
-
for interface in self.container.
|
|
259
|
+
for interface in self.container._providers_cache.get(self.scope, []): # noqa
|
|
260
260
|
self.container.resolve(interface)
|
|
261
261
|
|
|
262
262
|
def close(self) -> None:
|
|
263
263
|
"""Close the scoped context."""
|
|
264
|
-
self._stack.
|
|
264
|
+
self._stack.close()
|
|
265
265
|
|
|
266
266
|
async def __aenter__(self) -> Self:
|
|
267
267
|
"""Enter the context asynchronously.
|
|
@@ -277,7 +277,7 @@ class ResourceScopedContext(ScopedContext):
|
|
|
277
277
|
exc_type: type[BaseException] | None,
|
|
278
278
|
exc_val: BaseException | None,
|
|
279
279
|
exc_tb: TracebackType | None,
|
|
280
|
-
) ->
|
|
280
|
+
) -> None:
|
|
281
281
|
"""Exit the context asynchronously.
|
|
282
282
|
|
|
283
283
|
Args:
|
|
@@ -285,17 +285,18 @@ class ResourceScopedContext(ScopedContext):
|
|
|
285
285
|
exc_val: The exception instance, if any.
|
|
286
286
|
exc_tb: The traceback, if any.
|
|
287
287
|
"""
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
) or await self._async_stack.__aexit__(exc_type, exc_val, exc_tb)
|
|
288
|
+
await self.aclose()
|
|
289
|
+
return
|
|
291
290
|
|
|
292
|
-
@abc.abstractmethod
|
|
293
291
|
async def astart(self) -> None:
|
|
294
292
|
"""Start the scoped context asynchronously."""
|
|
293
|
+
for interface in self.container._providers_cache.get(self.scope, []): # noqa
|
|
294
|
+
await self.container.aresolve(interface)
|
|
295
295
|
|
|
296
296
|
async def aclose(self) -> None:
|
|
297
297
|
"""Close the scoped context asynchronously."""
|
|
298
|
-
await self.
|
|
298
|
+
await run_async(self._stack.close)
|
|
299
|
+
await self._async_stack.aclose()
|
|
299
300
|
|
|
300
301
|
|
|
301
302
|
@final
|
|
@@ -304,16 +305,6 @@ class SingletonContext(ResourceScopedContext):
|
|
|
304
305
|
|
|
305
306
|
scope = "singleton"
|
|
306
307
|
|
|
307
|
-
def start(self) -> None:
|
|
308
|
-
"""Start the scoped context."""
|
|
309
|
-
for interface in self.container._resource_cache.get(self.scope, []): # noqa
|
|
310
|
-
self.container.resolve(interface)
|
|
311
|
-
|
|
312
|
-
async def astart(self) -> None:
|
|
313
|
-
"""Start the scoped context asynchronously."""
|
|
314
|
-
for interface in self.container._resource_cache.get(self.scope, []): # noqa
|
|
315
|
-
await self.container.aresolve(interface)
|
|
316
|
-
|
|
317
308
|
|
|
318
309
|
@final
|
|
319
310
|
class RequestContext(ResourceScopedContext):
|
|
@@ -321,20 +312,6 @@ class RequestContext(ResourceScopedContext):
|
|
|
321
312
|
|
|
322
313
|
scope = "request"
|
|
323
314
|
|
|
324
|
-
def start(self) -> None:
|
|
325
|
-
"""Start the scoped context."""
|
|
326
|
-
for interface in self.container._resource_cache.get(self.scope, []): # noqa
|
|
327
|
-
if not is_event_type(interface):
|
|
328
|
-
continue
|
|
329
|
-
self.container.resolve(interface)
|
|
330
|
-
|
|
331
|
-
async def astart(self) -> None:
|
|
332
|
-
"""Start the scoped context asynchronously."""
|
|
333
|
-
for interface in self.container._resource_cache.get(self.scope, []): # noqa
|
|
334
|
-
if not is_event_type(interface):
|
|
335
|
-
continue
|
|
336
|
-
await self.container.aresolve(interface)
|
|
337
|
-
|
|
338
315
|
|
|
339
316
|
@final
|
|
340
317
|
class TransientContext(ScopedContext):
|
anydi/_types.py
CHANGED
|
@@ -30,17 +30,6 @@ def is_marker(obj: Any) -> bool:
|
|
|
30
30
|
return isinstance(obj, Marker)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class Event:
|
|
34
|
-
"""Represents an event object."""
|
|
35
|
-
|
|
36
|
-
__slots__ = ()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def is_event_type(obj: Any) -> bool:
|
|
40
|
-
"""Checks if an object is an event type."""
|
|
41
|
-
return inspect.isclass(obj) and issubclass(obj, Event)
|
|
42
|
-
|
|
43
|
-
|
|
44
33
|
@dataclass(frozen=True)
|
|
45
34
|
class Provider:
|
|
46
35
|
"""Represents a provider object.
|
anydi/_utils.py
CHANGED
|
@@ -6,7 +6,7 @@ import builtins
|
|
|
6
6
|
import functools
|
|
7
7
|
import inspect
|
|
8
8
|
import sys
|
|
9
|
-
from typing import Any, AsyncIterator, Callable, ForwardRef, Iterator, TypeVar
|
|
9
|
+
from typing import Any, AsyncIterator, Callable, ForwardRef, Iterator, TypeVar, cast
|
|
10
10
|
|
|
11
11
|
from typing_extensions import ParamSpec, get_args, get_origin
|
|
12
12
|
|
|
@@ -16,18 +16,19 @@ except ImportError:
|
|
|
16
16
|
anyio = None # type: ignore[assignment]
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
P = ParamSpec("P")
|
|
19
|
+
if sys.version_info < (3, 9): # pragma: nocover
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
|
|
24
|
-
if sys.version_info < (3, 9):
|
|
21
|
+
def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
|
|
25
22
|
return type_._evaluate(globalns, localns) # noqa
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
|
|
24
|
+
else:
|
|
25
|
+
|
|
26
|
+
def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
|
|
27
|
+
return cast(Any, type_)._evaluate(globalns, localns, set()) # noqa
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
P = ParamSpec("P")
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def get_full_qualname(obj: Any) -> str:
|
|
@@ -69,12 +70,10 @@ def get_typed_annotation(
|
|
|
69
70
|
) -> Any:
|
|
70
71
|
"""Get the typed annotation of a parameter."""
|
|
71
72
|
if isinstance(annotation, str):
|
|
72
|
-
if sys.version_info
|
|
73
|
-
annotation = ForwardRef(annotation, module=module, is_class=is_class)
|
|
74
|
-
elif sys.version_info >= (3, 10, 0):
|
|
75
|
-
annotation = ForwardRef(annotation, module=module)
|
|
76
|
-
else:
|
|
73
|
+
if sys.version_info < (3, 9):
|
|
77
74
|
annotation = ForwardRef(annotation)
|
|
75
|
+
else:
|
|
76
|
+
annotation = ForwardRef(annotation, module=module, is_class=is_class)
|
|
78
77
|
annotation = evaluate_forwardref(annotation, globalns, {})
|
|
79
78
|
return annotation
|
|
80
79
|
|
anydi/ext/_utils.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""AnyDI FastAPI extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from typing_extensions import Annotated, get_args, get_origin
|
|
10
|
+
|
|
11
|
+
from anydi import Container
|
|
12
|
+
from anydi._utils import get_full_qualname
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HasInterface:
|
|
18
|
+
_interface: Any = None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def interface(self) -> Any:
|
|
22
|
+
if self._interface is None:
|
|
23
|
+
raise TypeError("Interface is not set.")
|
|
24
|
+
return self._interface
|
|
25
|
+
|
|
26
|
+
@interface.setter
|
|
27
|
+
def interface(self, interface: Any) -> None:
|
|
28
|
+
self._interface = interface
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def patch_annotated_parameter(parameter: inspect.Parameter) -> inspect.Parameter:
|
|
32
|
+
"""Patch an annotated parameter to resolve the default value."""
|
|
33
|
+
if not (
|
|
34
|
+
get_origin(parameter.annotation) is Annotated
|
|
35
|
+
and parameter.default is parameter.empty
|
|
36
|
+
):
|
|
37
|
+
return parameter
|
|
38
|
+
|
|
39
|
+
tp_origin, *tp_metadata = get_args(parameter.annotation)
|
|
40
|
+
default = tp_metadata[-1]
|
|
41
|
+
|
|
42
|
+
if not isinstance(default, HasInterface):
|
|
43
|
+
return parameter
|
|
44
|
+
|
|
45
|
+
if (num := len(tp_metadata[:-1])) == 0:
|
|
46
|
+
interface = tp_origin
|
|
47
|
+
elif num == 1:
|
|
48
|
+
interface = Annotated[tp_origin, tp_metadata[0]]
|
|
49
|
+
elif num == 2:
|
|
50
|
+
interface = Annotated[tp_origin, tp_metadata[0], tp_metadata[1]]
|
|
51
|
+
elif num == 3:
|
|
52
|
+
interface = Annotated[
|
|
53
|
+
tp_origin,
|
|
54
|
+
tp_metadata[0],
|
|
55
|
+
tp_metadata[1],
|
|
56
|
+
tp_metadata[2],
|
|
57
|
+
]
|
|
58
|
+
else:
|
|
59
|
+
raise TypeError("Too many annotated arguments.") # pragma: no cover
|
|
60
|
+
return parameter.replace(annotation=interface, default=default)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def patch_call_parameter(
|
|
64
|
+
call: Callable[..., Any], parameter: inspect.Parameter, container: Container
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Patch a parameter to inject dependencies using AnyDI.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
call: The call function.
|
|
70
|
+
parameter: The parameter to patch.
|
|
71
|
+
container: The AnyDI container.
|
|
72
|
+
"""
|
|
73
|
+
parameter = patch_annotated_parameter(parameter)
|
|
74
|
+
|
|
75
|
+
if not isinstance(parameter.default, HasInterface):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
if not container.strict and not container.is_registered(parameter.annotation):
|
|
79
|
+
logger.debug(
|
|
80
|
+
f"Callable `{get_full_qualname(call)}` injected parameter "
|
|
81
|
+
f"`{parameter.name}` with an annotation of "
|
|
82
|
+
f"`{get_full_qualname(parameter.annotation)}` "
|
|
83
|
+
"is not registered. It will be registered at runtime with the "
|
|
84
|
+
"first call because it is running in non-strict mode."
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
container._validate_injected_parameter(call, parameter) # noqa
|
|
88
|
+
|
|
89
|
+
parameter.default.interface = parameter.annotation
|
anydi/ext/fastapi.py
CHANGED
|
@@ -2,19 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import inspect
|
|
6
5
|
import logging
|
|
7
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, Iterator, cast
|
|
8
7
|
|
|
9
8
|
from fastapi import Depends, FastAPI, params
|
|
10
9
|
from fastapi.dependencies.models import Dependant
|
|
11
10
|
from fastapi.routing import APIRoute
|
|
12
11
|
from starlette.requests import Request
|
|
13
|
-
from typing_extensions import Annotated, get_args, get_origin
|
|
14
12
|
|
|
15
13
|
from anydi import Container
|
|
16
|
-
from anydi._utils import
|
|
14
|
+
from anydi._utils import get_typed_parameters
|
|
17
15
|
|
|
16
|
+
from ._utils import HasInterface, patch_call_parameter
|
|
18
17
|
from .starlette.middleware import RequestScopedMiddleware
|
|
19
18
|
|
|
20
19
|
__all__ = ["RequestScopedMiddleware", "install", "get_container", "Inject"]
|
|
@@ -48,7 +47,7 @@ def install(app: FastAPI, container: Container) -> None:
|
|
|
48
47
|
if not call:
|
|
49
48
|
continue # pragma: no cover
|
|
50
49
|
for parameter in get_typed_parameters(call):
|
|
51
|
-
|
|
50
|
+
patch_call_parameter(call, parameter, container)
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
def get_container(request: Request) -> Container:
|
|
@@ -63,22 +62,11 @@ def get_container(request: Request) -> Container:
|
|
|
63
62
|
return cast(Container, request.app.state.container)
|
|
64
63
|
|
|
65
64
|
|
|
66
|
-
class Resolver(params.Depends):
|
|
65
|
+
class Resolver(HasInterface, params.Depends):
|
|
67
66
|
"""Parameter dependency class for injecting dependencies using AnyDI."""
|
|
68
67
|
|
|
69
68
|
def __init__(self) -> None:
|
|
70
69
|
super().__init__(dependency=self._dependency, use_cache=True)
|
|
71
|
-
self._interface: Any = None
|
|
72
|
-
|
|
73
|
-
@property
|
|
74
|
-
def interface(self) -> Any:
|
|
75
|
-
if self._interface is None:
|
|
76
|
-
raise TypeError("Interface is not set.")
|
|
77
|
-
return self._interface
|
|
78
|
-
|
|
79
|
-
@interface.setter
|
|
80
|
-
def interface(self, interface: Any) -> None:
|
|
81
|
-
self._interface = interface
|
|
82
70
|
|
|
83
71
|
async def _dependency(self, container: Container = Depends(get_container)) -> Any:
|
|
84
72
|
return await container.aresolve(self.interface)
|
|
@@ -102,64 +90,3 @@ def _iter_dependencies(dependant: Dependant) -> Iterator[Dependant]:
|
|
|
102
90
|
if dependant.dependencies:
|
|
103
91
|
for sub_dependant in dependant.dependencies:
|
|
104
92
|
yield from _iter_dependencies(sub_dependant)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _patch_route_parameter(
|
|
108
|
-
call: Callable[..., Any], parameter: inspect.Parameter, container: Container
|
|
109
|
-
) -> None:
|
|
110
|
-
"""Patch a parameter to inject dependencies using AnyDI.
|
|
111
|
-
|
|
112
|
-
Args:
|
|
113
|
-
call: The call function.
|
|
114
|
-
parameter: The parameter to patch.
|
|
115
|
-
container: The AnyDI container.
|
|
116
|
-
"""
|
|
117
|
-
parameter = _patch_annotated_parameter(parameter)
|
|
118
|
-
|
|
119
|
-
if not isinstance(parameter.default, Resolver):
|
|
120
|
-
return None
|
|
121
|
-
|
|
122
|
-
if not container.strict and not container.is_registered(parameter.annotation):
|
|
123
|
-
logger.debug(
|
|
124
|
-
f"Callable `{get_full_qualname(call)}` injected parameter "
|
|
125
|
-
f"`{parameter.name}` with an annotation of "
|
|
126
|
-
f"`{get_full_qualname(parameter.annotation)}` "
|
|
127
|
-
"is not registered. It will be registered at runtime with the "
|
|
128
|
-
"first call because it is running in non-strict mode."
|
|
129
|
-
)
|
|
130
|
-
else:
|
|
131
|
-
container._validate_injected_parameter(call, parameter) # noqa
|
|
132
|
-
|
|
133
|
-
parameter.default.interface = parameter.annotation
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _patch_annotated_parameter(parameter: inspect.Parameter) -> inspect.Parameter:
|
|
137
|
-
"""Patch an annotated parameter to resolve the default value."""
|
|
138
|
-
if not (
|
|
139
|
-
get_origin(parameter.annotation) is Annotated
|
|
140
|
-
and parameter.default is parameter.empty
|
|
141
|
-
):
|
|
142
|
-
return parameter
|
|
143
|
-
|
|
144
|
-
tp_origin, *tp_metadata = get_args(parameter.annotation)
|
|
145
|
-
default = tp_metadata[-1]
|
|
146
|
-
|
|
147
|
-
if not isinstance(default, Resolver):
|
|
148
|
-
return parameter
|
|
149
|
-
|
|
150
|
-
if (num := len(tp_metadata[:-1])) == 0:
|
|
151
|
-
interface = tp_origin
|
|
152
|
-
elif num == 1:
|
|
153
|
-
interface = Annotated[tp_origin, tp_metadata[0]]
|
|
154
|
-
elif num == 2:
|
|
155
|
-
interface = Annotated[tp_origin, tp_metadata[0], tp_metadata[1]]
|
|
156
|
-
elif num == 3:
|
|
157
|
-
interface = Annotated[
|
|
158
|
-
tp_origin,
|
|
159
|
-
tp_metadata[0],
|
|
160
|
-
tp_metadata[1],
|
|
161
|
-
tp_metadata[2],
|
|
162
|
-
]
|
|
163
|
-
else:
|
|
164
|
-
raise TypeError("Too many annotated arguments.") # pragma: no cover
|
|
165
|
-
return parameter.replace(annotation=interface, default=default)
|
anydi/ext/faststream.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
from fast_depends.dependencies import Depends
|
|
5
|
+
from faststream import ContextRepo
|
|
6
|
+
from faststream.broker.core.usecase import BrokerUsecase
|
|
7
|
+
|
|
8
|
+
from anydi import Container
|
|
9
|
+
from anydi._utils import get_typed_parameters
|
|
10
|
+
|
|
11
|
+
from ._utils import HasInterface, patch_call_parameter
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def install(broker: BrokerUsecase[Any, Any], container: Container) -> None:
|
|
17
|
+
"""Install AnyDI into a FastStream broker.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
broker: The broker.
|
|
21
|
+
container: The container.
|
|
22
|
+
|
|
23
|
+
This function installs the AnyDI container into a FastStream broker by attaching
|
|
24
|
+
it to the broker. It also patches the broker handlers to inject the required
|
|
25
|
+
dependencies using AnyDI.
|
|
26
|
+
"""
|
|
27
|
+
broker._container = container # type: ignore[attr-defined]
|
|
28
|
+
|
|
29
|
+
for subscriber in broker._subscribers.values(): # noqa
|
|
30
|
+
call = subscriber.calls[0].handler._original_call # noqa
|
|
31
|
+
for parameter in get_typed_parameters(call):
|
|
32
|
+
patch_call_parameter(call, parameter, container)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_container(broker: BrokerUsecase[Any, Any]) -> Container:
|
|
36
|
+
return cast(Container, getattr(broker, "_container")) # noqa
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Resolver(HasInterface, Depends):
|
|
40
|
+
"""Parameter dependency class for injecting dependencies using AnyDI."""
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
super().__init__(dependency=self._dependency, use_cache=True, cast=True)
|
|
44
|
+
|
|
45
|
+
async def _dependency(self, context: ContextRepo) -> Any:
|
|
46
|
+
return get_container(context.get("broker")).resolve(self.interface)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def Inject() -> Any: # noqa
|
|
50
|
+
return Resolver()
|
anydi/ext/pytest_plugin.py
CHANGED
|
@@ -63,15 +63,12 @@ def _anydi_injected_parameter_iterator(
|
|
|
63
63
|
request: pytest.FixtureRequest,
|
|
64
64
|
_anydi_unresolved: list[str],
|
|
65
65
|
) -> Callable[[], Iterator[tuple[str, Any]]]:
|
|
66
|
-
registered_fixtures = request.session._fixturemanager._arg2fixturedefs # noqa
|
|
67
|
-
|
|
68
66
|
def _iterator() -> Iterator[tuple[str, inspect.Parameter]]:
|
|
69
67
|
for parameter in get_typed_parameters(request.function):
|
|
70
|
-
interface = parameter.annotation
|
|
71
68
|
if (
|
|
72
|
-
interface is parameter.empty
|
|
69
|
+
((interface := parameter.annotation) is parameter.empty)
|
|
73
70
|
or interface in _anydi_unresolved
|
|
74
|
-
or parameter.name in
|
|
71
|
+
or parameter.name in request.node.funcargs
|
|
75
72
|
):
|
|
76
73
|
continue
|
|
77
74
|
yield parameter.name, interface
|
|
@@ -95,14 +92,18 @@ def _anydi_inject(
|
|
|
95
92
|
container = cast(Container, request.getfixturevalue("anydi_setup_container"))
|
|
96
93
|
|
|
97
94
|
for argname, interface in _anydi_injected_parameter_iterator():
|
|
98
|
-
# Skip if the interface is not registered
|
|
99
|
-
if container.strict and not container.is_registered(interface):
|
|
100
|
-
continue
|
|
101
|
-
|
|
102
95
|
try:
|
|
103
|
-
|
|
104
|
-
|
|
96
|
+
# Release the instance if it was already resolved
|
|
97
|
+
container.release(interface)
|
|
98
|
+
except LookupError:
|
|
99
|
+
pass
|
|
100
|
+
try:
|
|
101
|
+
# Resolve the instance
|
|
102
|
+
instance = container.resolve(interface)
|
|
103
|
+
except LookupError:
|
|
105
104
|
_anydi_unresolved.append(interface)
|
|
105
|
+
continue
|
|
106
|
+
request.node.funcargs[argname] = instance
|
|
106
107
|
|
|
107
108
|
|
|
108
109
|
@pytest.fixture(autouse=True)
|
|
@@ -120,11 +121,15 @@ async def _anydi_ainject(
|
|
|
120
121
|
container = cast(Container, request.getfixturevalue("anydi_setup_container"))
|
|
121
122
|
|
|
122
123
|
for argname, interface in _anydi_injected_parameter_iterator():
|
|
123
|
-
# Skip if the interface is not registered
|
|
124
|
-
if container.strict and not container.is_registered(interface):
|
|
125
|
-
continue
|
|
126
|
-
|
|
127
124
|
try:
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
# Release the instance if it was already resolved
|
|
126
|
+
container.release(interface)
|
|
127
|
+
except LookupError:
|
|
128
|
+
pass
|
|
129
|
+
try:
|
|
130
|
+
# Resolve the instance
|
|
131
|
+
instance = await container.aresolve(interface)
|
|
132
|
+
except LookupError:
|
|
130
133
|
_anydi_unresolved.append(interface)
|
|
134
|
+
continue
|
|
135
|
+
request.node.funcargs[argname] = instance
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: anydi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.27.0a0
|
|
4
4
|
Summary: Dependency Injection library
|
|
5
5
|
Home-page: https://github.com/antonrh/anydi
|
|
6
6
|
License: MIT
|
|
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
24
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
25
26
|
Classifier: Topic :: Internet
|
|
26
27
|
Classifier: Topic :: Software Development
|
|
27
28
|
Classifier: Topic :: Software Development :: Libraries
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
anydi/__init__.py,sha256=aeaBp5vq09sG-e9sqqs9qpUtUIDNfOdFPrlAfE5Ku9E,584
|
|
2
|
-
anydi/_container.py,sha256
|
|
3
|
-
anydi/_context.py,sha256=
|
|
2
|
+
anydi/_container.py,sha256=IhfucOoAPYpMY9A0uHcqa1EOzE3_0TsExR-ZehZrZ6U,29246
|
|
3
|
+
anydi/_context.py,sha256=e0VX0fiflzW_2O9w3HvUH3YRCwXHsruQjf3Lu-zXgDw,10815
|
|
4
4
|
anydi/_logger.py,sha256=UpubJUnW83kffFxkhUlObm2DmZX1Pjqoz9YFKS-JOPg,52
|
|
5
5
|
anydi/_module.py,sha256=E1TfLud_Af-MPB83PxIzHVA1jlDW2FGaRP_il1a6y3Y,3675
|
|
6
6
|
anydi/_scanner.py,sha256=cyEk-K2Q8ssZStq8GrxMeEcCuAZMw-RXrjlgWEevKCs,6667
|
|
7
|
-
anydi/_types.py,sha256=
|
|
8
|
-
anydi/_utils.py,sha256=
|
|
7
|
+
anydi/_types.py,sha256=vQTrFjsYhlMxfo1nOFem05x2QUJMQkVh4ZaC7W0XZJY,3434
|
|
8
|
+
anydi/_utils.py,sha256=zP4UvO1aVQJTB8pFNUWAcncvSiuhcg4xNdRU7CoLrqw,3871
|
|
9
9
|
anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
anydi/ext/_utils.py,sha256=2kxLPTMM9Ro3s6-knbqYzONlqRB3hMcwZFFRQGHcFUg,2691
|
|
10
11
|
anydi/ext/django/__init__.py,sha256=QI1IABCVgSDTUoh7M9WMECKXwB3xvh04HfQ9TOWw1Mk,223
|
|
11
12
|
anydi/ext/django/_container.py,sha256=cxVoYQG16WP0S_Yv4TnLwuaaT7NVEOhLWO-YdALJUb4,418
|
|
12
13
|
anydi/ext/django/_settings.py,sha256=cKzFBGtPCsexZ2ZcInubBukIebhxzNfa3F0KuwoZYaA,844
|
|
@@ -16,13 +17,14 @@ anydi/ext/django/middleware.py,sha256=iVHWtE829khMY-BXbNNt0g2FrIApKprna7dCG9ObEi
|
|
|
16
17
|
anydi/ext/django/ninja/__init__.py,sha256=kW3grUgWp_nkWSG_-39ADHMrZLGNcj9TsJ9OW8iWWrk,546
|
|
17
18
|
anydi/ext/django/ninja/_operation.py,sha256=wSWa7D73XTVlOibmOciv2l6JHPe1ERZcXrqI8W-oO2w,2696
|
|
18
19
|
anydi/ext/django/ninja/_signature.py,sha256=2cSzKxBIxXLqtwNuH6GSlmjVJFftoGmleWfyk_NVEWw,2207
|
|
19
|
-
anydi/ext/fastapi.py,sha256=
|
|
20
|
-
anydi/ext/
|
|
20
|
+
anydi/ext/fastapi.py,sha256=vhfSyovXuCjvSkx6AiLOTNU975i8wDg72C5fqXQiFLw,2896
|
|
21
|
+
anydi/ext/faststream.py,sha256=svMtqFVSRTpuf4H5yeozHWmrBXi_F8cevb5d2mo3m-E,1617
|
|
22
|
+
anydi/ext/pytest_plugin.py,sha256=nDNqjblVQufLha6P8J8hZw4Q0EuwC71bOI_bdaGm-OQ,4043
|
|
21
23
|
anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
24
|
anydi/ext/starlette/middleware.py,sha256=Ni0BQaPjs_Ha6zcLZYYJ3-XkslTCnL9aCSa06rnRDMI,1139
|
|
23
25
|
anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
-
anydi-0.
|
|
25
|
-
anydi-0.
|
|
26
|
-
anydi-0.
|
|
27
|
-
anydi-0.
|
|
28
|
-
anydi-0.
|
|
26
|
+
anydi-0.27.0a0.dist-info/LICENSE,sha256=V6rU8a8fv6o2jQ-7ODHs0XfDFimot8Q6Km6CylRIDTo,1069
|
|
27
|
+
anydi-0.27.0a0.dist-info/METADATA,sha256=sVLcw-YRcQ7PD1C39knmNRgN0gbvoQlqzfU2pF7NpkM,5162
|
|
28
|
+
anydi-0.27.0a0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
29
|
+
anydi-0.27.0a0.dist-info/entry_points.txt,sha256=GmQblwzxFg42zva1HyBYJJ7TvrTIcSAGBHmyi3bvsi4,42
|
|
30
|
+
anydi-0.27.0a0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|