anydi 0.22.0__py3-none-any.whl → 0.37.4__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/__init__.py +14 -14
- anydi/_container.py +811 -571
- anydi/_context.py +39 -281
- anydi/_provider.py +232 -0
- anydi/_types.py +49 -96
- anydi/_utils.py +108 -77
- anydi/ext/_utils.py +49 -28
- anydi/ext/django/__init__.py +9 -0
- anydi/ext/django/_container.py +18 -0
- anydi/ext/django/_settings.py +39 -0
- anydi/ext/django/_utils.py +128 -0
- anydi/ext/django/apps.py +85 -0
- anydi/ext/django/middleware.py +28 -0
- anydi/ext/django/ninja/__init__.py +16 -0
- anydi/ext/django/ninja/_operation.py +75 -0
- anydi/ext/django/ninja/_signature.py +64 -0
- anydi/ext/fastapi.py +11 -27
- anydi/ext/faststream.py +58 -0
- anydi/ext/pydantic_settings.py +48 -0
- anydi/ext/pytest_plugin.py +67 -41
- anydi/ext/starlette/middleware.py +2 -16
- {anydi-0.22.0.dist-info → anydi-0.37.4.dist-info}/METADATA +71 -24
- anydi-0.37.4.dist-info/RECORD +29 -0
- {anydi-0.22.0.dist-info → anydi-0.37.4.dist-info}/WHEEL +1 -1
- anydi-0.37.4.dist-info/entry_points.txt +2 -0
- anydi/_logger.py +0 -3
- anydi/_module.py +0 -124
- anydi/_scanner.py +0 -233
- anydi-0.22.0.dist-info/RECORD +0 -20
- anydi-0.22.0.dist-info/entry_points.txt +0 -3
- {anydi-0.22.0.dist-info → anydi-0.37.4.dist-info/licenses}/LICENSE +0 -0
anydi/ext/django/apps.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import types
|
|
5
|
+
from typing import Callable, cast
|
|
6
|
+
|
|
7
|
+
from django.apps import AppConfig
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
10
|
+
from django.utils.module_loading import import_string
|
|
11
|
+
|
|
12
|
+
import anydi
|
|
13
|
+
|
|
14
|
+
from ._settings import get_settings
|
|
15
|
+
from ._utils import inject_urlpatterns, register_components, register_settings
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ContainerConfig(AppConfig):
|
|
21
|
+
name = "anydi.ext.django"
|
|
22
|
+
label = "anydi_django"
|
|
23
|
+
|
|
24
|
+
def __init__(self, app_name: str, app_module: types.ModuleType | None) -> None:
|
|
25
|
+
super().__init__(app_name, app_module)
|
|
26
|
+
self.settings = get_settings()
|
|
27
|
+
# Create a container
|
|
28
|
+
container_factory_path = self.settings["CONTAINER_FACTORY"]
|
|
29
|
+
if container_factory_path:
|
|
30
|
+
try:
|
|
31
|
+
container_factory = cast(
|
|
32
|
+
Callable[[], anydi.Container], import_string(container_factory_path)
|
|
33
|
+
)
|
|
34
|
+
except ImportError as exc:
|
|
35
|
+
raise ImproperlyConfigured(
|
|
36
|
+
f"Cannot import container factory '{container_factory_path}'."
|
|
37
|
+
) from exc
|
|
38
|
+
self.container = container_factory()
|
|
39
|
+
else:
|
|
40
|
+
self.container = anydi.Container(
|
|
41
|
+
strict=self.settings["STRICT_MODE"],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def ready(self) -> None: # noqa: C901
|
|
45
|
+
# Register Django settings
|
|
46
|
+
if self.settings["REGISTER_SETTINGS"]:
|
|
47
|
+
register_settings(
|
|
48
|
+
self.container,
|
|
49
|
+
prefix=getattr(
|
|
50
|
+
settings,
|
|
51
|
+
"ANYDI_SETTINGS_PREFIX",
|
|
52
|
+
"django.conf.settings.",
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Register Django components
|
|
57
|
+
if self.settings["REGISTER_COMPONENTS"]:
|
|
58
|
+
register_components(self.container)
|
|
59
|
+
|
|
60
|
+
# Register modules
|
|
61
|
+
for module_path in self.settings["MODULES"]:
|
|
62
|
+
try:
|
|
63
|
+
module_cls = import_string(module_path)
|
|
64
|
+
except ImportError as exc:
|
|
65
|
+
raise ImproperlyConfigured(
|
|
66
|
+
f"Cannot import module '{module_path}'."
|
|
67
|
+
) from exc
|
|
68
|
+
self.container.register_module(module_cls)
|
|
69
|
+
|
|
70
|
+
# Patching the django-ninja framework if it installed
|
|
71
|
+
if self.settings["PATCH_NINJA"]:
|
|
72
|
+
from .ninja import patch_ninja
|
|
73
|
+
|
|
74
|
+
patch_ninja()
|
|
75
|
+
|
|
76
|
+
# Auto-injecting the container into views
|
|
77
|
+
if urlconf := self.settings["INJECT_URLCONF"]:
|
|
78
|
+
if isinstance(urlconf, str):
|
|
79
|
+
urlconf = [urlconf]
|
|
80
|
+
for u in urlconf:
|
|
81
|
+
inject_urlpatterns(self.container, urlconf=u)
|
|
82
|
+
|
|
83
|
+
# Scan packages
|
|
84
|
+
for scan_package in self.settings["SCAN_PACKAGES"]:
|
|
85
|
+
self.container.scan(scan_package)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
|
|
3
|
+
from asgiref.sync import iscoroutinefunction
|
|
4
|
+
from django.http import HttpRequest
|
|
5
|
+
from django.utils.decorators import sync_and_async_middleware
|
|
6
|
+
|
|
7
|
+
from ._container import container
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@sync_and_async_middleware
|
|
11
|
+
def request_scoped_middleware(
|
|
12
|
+
get_response: Callable[..., Any],
|
|
13
|
+
) -> Callable[..., Any]:
|
|
14
|
+
if iscoroutinefunction(get_response):
|
|
15
|
+
|
|
16
|
+
async def async_middleware(request: HttpRequest) -> Any:
|
|
17
|
+
async with container.arequest_context() as context:
|
|
18
|
+
context.set(HttpRequest, request)
|
|
19
|
+
return await get_response(request)
|
|
20
|
+
|
|
21
|
+
return async_middleware
|
|
22
|
+
|
|
23
|
+
def middleware(request: HttpRequest) -> Any:
|
|
24
|
+
with container.request_context() as context:
|
|
25
|
+
context.set(HttpRequest, request)
|
|
26
|
+
return get_response(request)
|
|
27
|
+
|
|
28
|
+
return middleware
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from ninja import operation
|
|
3
|
+
except ImportError as exc: # pragma: no cover
|
|
4
|
+
raise ImportError(
|
|
5
|
+
"'django-ninja' is not installed. "
|
|
6
|
+
"Please install it using 'pip install django-ninja'."
|
|
7
|
+
) from exc
|
|
8
|
+
|
|
9
|
+
from ._operation import AsyncOperation, Operation
|
|
10
|
+
from ._signature import ViewSignature
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def patch_ninja() -> None:
|
|
14
|
+
operation.ViewSignature = ViewSignature # type: ignore[attr-defined]
|
|
15
|
+
operation.Operation = Operation # type: ignore[misc]
|
|
16
|
+
operation.AsyncOperation = AsyncOperation # type: ignore[misc]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from django.http import HttpRequest, HttpResponseBase
|
|
6
|
+
from ninja.operation import (
|
|
7
|
+
AsyncOperation as BaseAsyncOperation, # noqa
|
|
8
|
+
Operation as BaseOperation,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from anydi.ext.django import container
|
|
12
|
+
|
|
13
|
+
from ._signature import ViewSignature
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _update_exc_args(exc: Exception) -> None:
|
|
17
|
+
if isinstance(exc, TypeError) and "required positional argument" in str(exc):
|
|
18
|
+
msg = "Did you fail to use functools.wraps() in a decorator?"
|
|
19
|
+
msg = f"{exc.args[0]}: {msg}" if exc.args else msg
|
|
20
|
+
exc.args = (msg,) + exc.args[1:]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Operation(BaseOperation):
|
|
24
|
+
signature: ViewSignature
|
|
25
|
+
|
|
26
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
27
|
+
super().__init__(*args, **kwargs)
|
|
28
|
+
self.dependencies = self.signature.dependencies
|
|
29
|
+
|
|
30
|
+
def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
|
|
31
|
+
error = self._run_checks(request)
|
|
32
|
+
if error:
|
|
33
|
+
return error
|
|
34
|
+
try:
|
|
35
|
+
temporal_response = self.api.create_temporal_response(request)
|
|
36
|
+
values = self._get_values(request, kw, temporal_response)
|
|
37
|
+
values.update(self._get_dependencies())
|
|
38
|
+
result = self.view_func(request, **values)
|
|
39
|
+
return self._result_to_response(request, result, temporal_response)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
_update_exc_args(e)
|
|
42
|
+
return self.api.on_exception(request, e)
|
|
43
|
+
|
|
44
|
+
def _get_dependencies(self) -> dict[str, Any]:
|
|
45
|
+
return {
|
|
46
|
+
name: container.resolve(interface) for name, interface in self.dependencies
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AsyncOperation(BaseAsyncOperation):
|
|
51
|
+
signature: ViewSignature
|
|
52
|
+
|
|
53
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
54
|
+
super().__init__(*args, **kwargs)
|
|
55
|
+
self.dependencies = self.signature.dependencies
|
|
56
|
+
|
|
57
|
+
async def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase: # type: ignore
|
|
58
|
+
error = await self._run_checks(request)
|
|
59
|
+
if error:
|
|
60
|
+
return error
|
|
61
|
+
try:
|
|
62
|
+
temporal_response = self.api.create_temporal_response(request)
|
|
63
|
+
values = self._get_values(request, kw, temporal_response)
|
|
64
|
+
values.update(await self._get_dependencies())
|
|
65
|
+
result = await self.view_func(request, **values)
|
|
66
|
+
return self._result_to_response(request, result, temporal_response)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
_update_exc_args(e)
|
|
69
|
+
return self.api.on_exception(request, e)
|
|
70
|
+
|
|
71
|
+
async def _get_dependencies(self) -> dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
name: await container.aresolve(interface)
|
|
74
|
+
for name, interface in self.dependencies
|
|
75
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from django.http import HttpResponse
|
|
8
|
+
from ninja.signature.details import (
|
|
9
|
+
FuncParam, # noqa
|
|
10
|
+
ViewSignature as BaseViewSignature,
|
|
11
|
+
)
|
|
12
|
+
from ninja.signature.utils import get_path_param_names, get_typed_signature
|
|
13
|
+
|
|
14
|
+
from anydi._types import is_marker # noqa
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ViewSignature(BaseViewSignature):
|
|
18
|
+
def __init__(self, path: str, view_func: Callable[..., Any]) -> None:
|
|
19
|
+
self.view_func = view_func
|
|
20
|
+
self.signature = get_typed_signature(self.view_func)
|
|
21
|
+
self.path = path
|
|
22
|
+
self.path_params_names = get_path_param_names(path)
|
|
23
|
+
self.docstring = inspect.cleandoc(view_func.__doc__ or "")
|
|
24
|
+
self.has_kwargs = False
|
|
25
|
+
self.dependencies = []
|
|
26
|
+
|
|
27
|
+
self.params = []
|
|
28
|
+
for name, arg in self.signature.parameters.items():
|
|
29
|
+
if name == "request":
|
|
30
|
+
# TODO: maybe better assert that 1st param is request or check by type?
|
|
31
|
+
# maybe even have attribute like `has_request`
|
|
32
|
+
# so that users can ignore passing request if not needed
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
if arg.kind == arg.VAR_KEYWORD:
|
|
36
|
+
# Skipping **kwargs
|
|
37
|
+
self.has_kwargs = True
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
if arg.kind == arg.VAR_POSITIONAL:
|
|
41
|
+
# Skipping *args
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
if arg.annotation is HttpResponse:
|
|
45
|
+
self.response_arg = name
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# Skip default values that are anydi dependency markers
|
|
49
|
+
if is_marker(arg.default):
|
|
50
|
+
self.dependencies.append((name, arg.annotation))
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
func_param = self._get_param_type(name, arg)
|
|
54
|
+
self.params.append(func_param)
|
|
55
|
+
|
|
56
|
+
if hasattr(view_func, "_ninja_contribute_args"):
|
|
57
|
+
for p_name, p_type, p_source in view_func._ninja_contribute_args: # noqa
|
|
58
|
+
self.params.append(
|
|
59
|
+
FuncParam(p_name, p_source.alias or p_name, p_source, p_type, False)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.models = self._create_models()
|
|
63
|
+
|
|
64
|
+
self._validate_view_path_params()
|
anydi/ext/fastapi.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""AnyDI FastAPI extension."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import Any, cast
|
|
4
7
|
|
|
5
8
|
from fastapi import Depends, FastAPI, params
|
|
6
9
|
from fastapi.dependencies.models import Dependant
|
|
@@ -8,9 +11,9 @@ from fastapi.routing import APIRoute
|
|
|
8
11
|
from starlette.requests import Request
|
|
9
12
|
|
|
10
13
|
from anydi import Container
|
|
11
|
-
from anydi._utils import
|
|
14
|
+
from anydi._utils import get_typed_parameters
|
|
12
15
|
|
|
13
|
-
from ._utils import HasInterface,
|
|
16
|
+
from ._utils import HasInterface, patch_call_parameter
|
|
14
17
|
from .starlette.middleware import RequestScopedMiddleware
|
|
15
18
|
|
|
16
19
|
__all__ = ["RequestScopedMiddleware", "install", "get_container", "Inject"]
|
|
@@ -19,10 +22,6 @@ __all__ = ["RequestScopedMiddleware", "install", "get_container", "Inject"]
|
|
|
19
22
|
def install(app: FastAPI, container: Container) -> None:
|
|
20
23
|
"""Install AnyDI into a FastAPI application.
|
|
21
24
|
|
|
22
|
-
Args:
|
|
23
|
-
app: The FastAPI application instance.
|
|
24
|
-
container: The container.
|
|
25
|
-
|
|
26
25
|
This function installs the AnyDI container into a FastAPI application by attaching
|
|
27
26
|
it to the application state. It also patches the route dependencies to inject the
|
|
28
27
|
required dependencies using AnyDI.
|
|
@@ -41,42 +40,27 @@ def install(app: FastAPI, container: Container) -> None:
|
|
|
41
40
|
call, *params = dependant.cache_key
|
|
42
41
|
if not call:
|
|
43
42
|
continue # pragma: no cover
|
|
44
|
-
for parameter in
|
|
45
|
-
|
|
43
|
+
for parameter in get_typed_parameters(call):
|
|
44
|
+
patch_call_parameter(container, call, parameter)
|
|
46
45
|
|
|
47
46
|
|
|
48
47
|
def get_container(request: Request) -> Container:
|
|
49
|
-
"""Get the AnyDI container from a FastAPI request.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
request: The FastAPI request.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
The AnyDI container associated with the request.
|
|
56
|
-
"""
|
|
48
|
+
"""Get the AnyDI container from a FastAPI request."""
|
|
57
49
|
return cast(Container, request.app.state.container)
|
|
58
50
|
|
|
59
51
|
|
|
60
|
-
class Resolver(params.Depends
|
|
52
|
+
class Resolver(HasInterface, params.Depends):
|
|
61
53
|
"""Parameter dependency class for injecting dependencies using AnyDI."""
|
|
62
54
|
|
|
63
55
|
def __init__(self) -> None:
|
|
64
56
|
super().__init__(dependency=self._dependency, use_cache=True)
|
|
65
|
-
HasInterface.__init__(self)
|
|
66
57
|
|
|
67
58
|
async def _dependency(self, container: Container = Depends(get_container)) -> Any:
|
|
68
59
|
return await container.aresolve(self.interface)
|
|
69
60
|
|
|
70
61
|
|
|
71
62
|
def Inject() -> Any: # noqa
|
|
72
|
-
"""Decorator for marking a function parameter as requiring injection.
|
|
73
|
-
|
|
74
|
-
The `Inject` decorator is used to mark a function parameter as requiring injection
|
|
75
|
-
of a dependency resolved by AnyDI.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
The `Resolver` instance representing the parameter dependency.
|
|
79
|
-
"""
|
|
63
|
+
"""Decorator for marking a function parameter as requiring injection."""
|
|
80
64
|
return Resolver()
|
|
81
65
|
|
|
82
66
|
|
anydi/ext/faststream.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""AnyDI FastStream extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from fast_depends.dependencies import Depends
|
|
8
|
+
from faststream import ContextRepo
|
|
9
|
+
from faststream.broker.core.usecase import BrokerUsecase
|
|
10
|
+
|
|
11
|
+
from anydi import Container
|
|
12
|
+
from anydi._utils import get_typed_parameters
|
|
13
|
+
|
|
14
|
+
from ._utils import HasInterface, patch_call_parameter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def install(broker: BrokerUsecase[Any, Any], container: Container) -> None:
|
|
18
|
+
"""Install AnyDI into a FastStream broker.
|
|
19
|
+
|
|
20
|
+
This function installs the AnyDI container into a FastStream broker by attaching
|
|
21
|
+
it to the broker. It also patches the broker handlers to inject the required
|
|
22
|
+
dependencies using AnyDI.
|
|
23
|
+
"""
|
|
24
|
+
broker._container = container # type: ignore[attr-defined]
|
|
25
|
+
|
|
26
|
+
for handler in _get_broken_handlers(broker):
|
|
27
|
+
call = handler._original_call # noqa
|
|
28
|
+
for parameter in get_typed_parameters(call):
|
|
29
|
+
patch_call_parameter(container, call, parameter)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_broken_handlers(broker: BrokerUsecase[Any, Any]) -> list[Any]:
|
|
33
|
+
if hasattr(broker, "handlers"):
|
|
34
|
+
return [handler.calls[0][0] for handler in broker.handlers.values()]
|
|
35
|
+
# faststream > 0.5.0
|
|
36
|
+
return [
|
|
37
|
+
subscriber.calls[0].handler
|
|
38
|
+
for subscriber in broker._subscribers.values() # noqa
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_container(broker: BrokerUsecase[Any, Any]) -> Container:
|
|
43
|
+
return cast(Container, getattr(broker, "_container")) # noqa
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Resolver(HasInterface, Depends):
|
|
47
|
+
"""Parameter dependency class for injecting dependencies using AnyDI."""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
super().__init__(dependency=self._dependency, use_cache=True, cast=True)
|
|
51
|
+
|
|
52
|
+
async def _dependency(self, context: ContextRepo) -> Any:
|
|
53
|
+
container = get_container(context.get("broker"))
|
|
54
|
+
return await container.aresolve(self.interface)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def Inject() -> Any: # noqa
|
|
58
|
+
return Resolver()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import Annotated, Any, Callable
|
|
5
|
+
|
|
6
|
+
from pydantic.fields import ComputedFieldInfo, FieldInfo # noqa
|
|
7
|
+
from pydantic_settings import BaseSettings
|
|
8
|
+
|
|
9
|
+
from anydi import Container
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def install(
|
|
13
|
+
settings: BaseSettings | Iterable[BaseSettings],
|
|
14
|
+
container: Container,
|
|
15
|
+
*,
|
|
16
|
+
prefix: str = "settings.",
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Install Pydantic settings into an AnyDI container."""
|
|
19
|
+
|
|
20
|
+
# Ensure prefix ends with a dot
|
|
21
|
+
if prefix[-1] != ".":
|
|
22
|
+
prefix += "."
|
|
23
|
+
|
|
24
|
+
def _register_settings(_settings: BaseSettings) -> None:
|
|
25
|
+
all_fields = {**_settings.model_fields, **_settings.model_computed_fields}
|
|
26
|
+
for setting_name, field_info in all_fields.items():
|
|
27
|
+
if isinstance(field_info, ComputedFieldInfo):
|
|
28
|
+
interface = field_info.return_type
|
|
29
|
+
elif isinstance(field_info, FieldInfo):
|
|
30
|
+
interface = field_info.annotation
|
|
31
|
+
else:
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
container.register(
|
|
35
|
+
Annotated[interface, f"{prefix}{setting_name}"],
|
|
36
|
+
_get_setting_value(getattr(_settings, setting_name)),
|
|
37
|
+
scope="singleton",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if isinstance(settings, BaseSettings):
|
|
41
|
+
_register_settings(settings)
|
|
42
|
+
else:
|
|
43
|
+
for _settings in settings:
|
|
44
|
+
_register_settings(_settings)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_setting_value(setting_value: Any) -> Callable[[], Any]:
|
|
48
|
+
return lambda: setting_value
|
anydi/ext/pytest_plugin.py
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import inspect
|
|
2
|
-
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import Any, Callable, cast
|
|
3
7
|
|
|
4
8
|
import pytest
|
|
9
|
+
from _pytest.python import async_warn_and_skip
|
|
10
|
+
from anyio.pytest_plugin import extract_backend_and_options, get_runner
|
|
5
11
|
|
|
6
12
|
from anydi import Container
|
|
13
|
+
from anydi._utils import get_typed_parameters
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
7
16
|
|
|
8
17
|
|
|
9
18
|
def pytest_configure(config: pytest.Config) -> None:
|
|
@@ -49,8 +58,8 @@ def _anydi_should_inject(request: pytest.FixtureRequest) -> bool:
|
|
|
49
58
|
|
|
50
59
|
|
|
51
60
|
@pytest.fixture(scope="session")
|
|
52
|
-
def _anydi_unresolved() -> Iterator[
|
|
53
|
-
unresolved:
|
|
61
|
+
def _anydi_unresolved() -> Iterator[list[Any]]:
|
|
62
|
+
unresolved: list[Any] = []
|
|
54
63
|
yield unresolved
|
|
55
64
|
unresolved.clear()
|
|
56
65
|
|
|
@@ -58,17 +67,20 @@ def _anydi_unresolved() -> Iterator[List[Any]]:
|
|
|
58
67
|
@pytest.fixture
|
|
59
68
|
def _anydi_injected_parameter_iterator(
|
|
60
69
|
request: pytest.FixtureRequest,
|
|
61
|
-
_anydi_unresolved:
|
|
62
|
-
) -> Callable[[], Iterator[
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
_anydi_unresolved: list[str],
|
|
71
|
+
) -> Callable[[], Iterator[tuple[str, Any]]]:
|
|
72
|
+
registered_fixtures = request.session._fixturemanager._arg2fixturedefs # noqa
|
|
73
|
+
|
|
74
|
+
def _iterator() -> Iterator[tuple[str, inspect.Parameter]]:
|
|
75
|
+
for parameter in get_typed_parameters(request.function):
|
|
76
|
+
interface = parameter.annotation
|
|
65
77
|
if (
|
|
66
|
-
|
|
78
|
+
interface is inspect.Parameter.empty
|
|
67
79
|
or interface in _anydi_unresolved
|
|
68
|
-
or name in
|
|
80
|
+
or parameter.name in registered_fixtures
|
|
69
81
|
):
|
|
70
82
|
continue
|
|
71
|
-
yield name, interface
|
|
83
|
+
yield parameter.name, interface
|
|
72
84
|
|
|
73
85
|
return _iterator
|
|
74
86
|
|
|
@@ -77,8 +89,8 @@ def _anydi_injected_parameter_iterator(
|
|
|
77
89
|
def _anydi_inject(
|
|
78
90
|
request: pytest.FixtureRequest,
|
|
79
91
|
_anydi_should_inject: bool,
|
|
80
|
-
_anydi_injected_parameter_iterator: Callable[[], Iterator[
|
|
81
|
-
_anydi_unresolved:
|
|
92
|
+
_anydi_injected_parameter_iterator: Callable[[], Iterator[tuple[str, Any]]],
|
|
93
|
+
_anydi_unresolved: list[str],
|
|
82
94
|
) -> None:
|
|
83
95
|
"""Inject dependencies into the test function."""
|
|
84
96
|
|
|
@@ -89,44 +101,58 @@ def _anydi_inject(
|
|
|
89
101
|
container = cast(Container, request.getfixturevalue("anydi_setup_container"))
|
|
90
102
|
|
|
91
103
|
for argname, interface in _anydi_injected_parameter_iterator():
|
|
104
|
+
# Skip if the interface is not registered
|
|
105
|
+
if container.strict and not container.is_registered(interface):
|
|
106
|
+
continue
|
|
107
|
+
|
|
92
108
|
try:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# Resolve the instance
|
|
99
|
-
instance = container.resolve(interface)
|
|
100
|
-
except (LookupError, TypeError):
|
|
109
|
+
request.node.funcargs[argname] = container.resolve(interface)
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
logger.warning(
|
|
112
|
+
f"Failed to resolve dependency for argument '{argname}'.", exc_info=exc
|
|
113
|
+
)
|
|
101
114
|
_anydi_unresolved.append(interface)
|
|
102
|
-
continue
|
|
103
|
-
request.node.funcargs[argname] = instance
|
|
104
115
|
|
|
105
116
|
|
|
106
117
|
@pytest.fixture(autouse=True)
|
|
107
|
-
|
|
118
|
+
def _anydi_ainject(
|
|
108
119
|
request: pytest.FixtureRequest,
|
|
109
120
|
_anydi_should_inject: bool,
|
|
110
|
-
_anydi_injected_parameter_iterator: Callable[[], Iterator[
|
|
111
|
-
_anydi_unresolved:
|
|
121
|
+
_anydi_injected_parameter_iterator: Callable[[], Iterator[tuple[str, Any]]],
|
|
122
|
+
_anydi_unresolved: list[str],
|
|
112
123
|
) -> None:
|
|
113
124
|
"""Inject dependencies into the test function."""
|
|
114
|
-
if
|
|
125
|
+
if (
|
|
126
|
+
not inspect.iscoroutinefunction(request.function)
|
|
127
|
+
and not inspect.isasyncgenfunction(request.function)
|
|
128
|
+
or not _anydi_should_inject
|
|
129
|
+
):
|
|
115
130
|
return
|
|
116
131
|
|
|
117
|
-
#
|
|
118
|
-
|
|
132
|
+
# Skip if the anyio backend is not available
|
|
133
|
+
if "anyio_backend" not in request.fixturenames:
|
|
134
|
+
async_warn_and_skip(request.node.nodeid)
|
|
119
135
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
async def _awrapper() -> None:
|
|
137
|
+
# Setup the container
|
|
138
|
+
container = cast(Container, request.getfixturevalue("anydi_setup_container"))
|
|
139
|
+
|
|
140
|
+
for argname, interface in _anydi_injected_parameter_iterator():
|
|
141
|
+
# Skip if the interface is not registered
|
|
142
|
+
if container.strict and not container.is_registered(interface):
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
request.node.funcargs[argname] = await container.aresolve(interface)
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
logger.warning(
|
|
149
|
+
f"Failed to resolve dependency for argument '{argname}'.",
|
|
150
|
+
exc_info=exc,
|
|
151
|
+
)
|
|
152
|
+
_anydi_unresolved.append(interface)
|
|
153
|
+
|
|
154
|
+
anyio_backend = request.getfixturevalue("anyio_backend")
|
|
155
|
+
backend_name, backend_options = extract_backend_and_options(anyio_backend)
|
|
156
|
+
|
|
157
|
+
with get_runner(backend_name, backend_options) as runner:
|
|
158
|
+
runner.run_fixture(_awrapper, {})
|
|
@@ -12,26 +12,12 @@ class RequestScopedMiddleware(BaseHTTPMiddleware):
|
|
|
12
12
|
"""Starlette middleware for managing request-scoped AnyDI context."""
|
|
13
13
|
|
|
14
14
|
def __init__(self, app: ASGIApp, container: Container) -> None:
|
|
15
|
-
"""Initialize the RequestScopedMiddleware.
|
|
16
|
-
|
|
17
|
-
Args:
|
|
18
|
-
app: The ASGI application.
|
|
19
|
-
container: The container.
|
|
20
|
-
"""
|
|
21
15
|
super().__init__(app)
|
|
22
16
|
self.container = container
|
|
23
17
|
|
|
24
18
|
async def dispatch(
|
|
25
19
|
self, request: Request, call_next: RequestResponseEndpoint
|
|
26
20
|
) -> Response:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
request: The incoming request.
|
|
31
|
-
call_next: The next request-response endpoint.
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
The response to the request.
|
|
35
|
-
"""
|
|
36
|
-
async with self.container.arequest_context():
|
|
21
|
+
async with self.container.arequest_context() as context:
|
|
22
|
+
context.set(Request, request)
|
|
37
23
|
return await call_next(request)
|