anydi 0.24.3__py3-none-any.whl → 0.25.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/__init__.py +2 -2
- anydi/_container.py +52 -39
- anydi/_context.py +8 -8
- anydi/_module.py +4 -4
- anydi/_scanner.py +29 -21
- anydi/_types.py +4 -14
- anydi/_utils.py +48 -48
- anydi/ext/django/__init__.py +3 -0
- anydi/ext/django/_container.py +18 -0
- anydi/ext/django/_utils.py +14 -0
- anydi/ext/django/apps.py +138 -0
- anydi/ext/django/ninja/__init__.py +16 -0
- anydi/ext/django/ninja/_operation.py +73 -0
- anydi/ext/django/ninja/_signature.py +62 -0
- anydi/ext/fastapi.py +2 -2
- anydi/ext/pytest_plugin.py +10 -12
- {anydi-0.24.3.dist-info → anydi-0.25.0a0.dist-info}/METADATA +3 -3
- anydi-0.25.0a0.dist-info/RECORD +26 -0
- anydi-0.24.3.dist-info/RECORD +0 -19
- {anydi-0.24.3.dist-info → anydi-0.25.0a0.dist-info}/LICENSE +0 -0
- {anydi-0.24.3.dist-info → anydi-0.25.0a0.dist-info}/WHEEL +0 -0
- {anydi-0.24.3.dist-info → anydi-0.25.0a0.dist-info}/entry_points.txt +0 -0
anydi/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AnyDI public objects and functions."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
from ._container import Container, request, singleton, transient
|
|
6
6
|
from ._module import Module, provider
|
|
@@ -14,7 +14,7 @@ def dep() -> Any:
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
# Alias for dependency auto marker
|
|
17
|
-
auto =
|
|
17
|
+
auto = dep
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
__all__ = [
|
anydi/_container.py
CHANGED
|
@@ -15,16 +15,21 @@ from typing import (
|
|
|
15
15
|
Awaitable,
|
|
16
16
|
Callable,
|
|
17
17
|
ContextManager,
|
|
18
|
+
Dict,
|
|
18
19
|
Iterable,
|
|
19
20
|
Iterator,
|
|
21
|
+
List,
|
|
20
22
|
Mapping,
|
|
23
|
+
Optional,
|
|
21
24
|
Sequence,
|
|
25
|
+
Type,
|
|
22
26
|
TypeVar,
|
|
27
|
+
Union,
|
|
23
28
|
cast,
|
|
24
29
|
overload,
|
|
25
30
|
)
|
|
26
31
|
|
|
27
|
-
from typing_extensions import ParamSpec, final, get_args, get_origin
|
|
32
|
+
from typing_extensions import Annotated, ParamSpec, final, get_args, get_origin
|
|
28
33
|
|
|
29
34
|
try:
|
|
30
35
|
from types import NoneType
|
|
@@ -42,18 +47,13 @@ from ._context import (
|
|
|
42
47
|
from ._logger import logger
|
|
43
48
|
from ._module import Module, ModuleRegistry
|
|
44
49
|
from ._scanner import Scanner
|
|
45
|
-
from ._types import AnyInterface, Interface, Provider, Scope
|
|
46
|
-
from ._utils import
|
|
47
|
-
get_full_qualname,
|
|
48
|
-
get_typed_parameters,
|
|
49
|
-
get_typed_return_annotation,
|
|
50
|
-
is_builtin_type,
|
|
51
|
-
)
|
|
50
|
+
from ._types import AnyInterface, Interface, Marker, Provider, Scope
|
|
51
|
+
from ._utils import get_full_qualname, get_signature, is_builtin_type
|
|
52
52
|
|
|
53
53
|
T = TypeVar("T", bound=Any)
|
|
54
54
|
P = ParamSpec("P")
|
|
55
55
|
|
|
56
|
-
ALLOWED_SCOPES:
|
|
56
|
+
ALLOWED_SCOPES: Dict[Scope, List[Scope]] = {
|
|
57
57
|
"singleton": ["singleton"],
|
|
58
58
|
"request": ["request", "singleton"],
|
|
59
59
|
"transient": ["transient", "singleton", "request"],
|
|
@@ -71,9 +71,10 @@ class Container:
|
|
|
71
71
|
def __init__(
|
|
72
72
|
self,
|
|
73
73
|
*,
|
|
74
|
-
providers: Mapping[
|
|
75
|
-
modules:
|
|
76
|
-
|
|
74
|
+
providers: Optional[Mapping[Type[Any], Provider]] = None,
|
|
75
|
+
modules: Optional[
|
|
76
|
+
Sequence[Union[Module, Type[Module], Callable[[Container], None]]]
|
|
77
|
+
] = None,
|
|
77
78
|
strict: bool = False,
|
|
78
79
|
) -> None:
|
|
79
80
|
"""Initialize the AnyDI instance.
|
|
@@ -83,13 +84,13 @@ class Container:
|
|
|
83
84
|
modules: Optional sequence of modules to register during initialization.
|
|
84
85
|
strict: Whether to enable strict mode. Defaults to False.
|
|
85
86
|
"""
|
|
86
|
-
self._providers:
|
|
87
|
+
self._providers: Dict[Type[Any], Provider] = {}
|
|
87
88
|
self._singleton_context = SingletonContext(self)
|
|
88
89
|
self._transient_context = TransientContext(self)
|
|
89
|
-
self._request_context_var: ContextVar[RequestContext
|
|
90
|
+
self._request_context_var: ContextVar[Optional[RequestContext]] = ContextVar(
|
|
90
91
|
"request_context", default=None
|
|
91
92
|
)
|
|
92
|
-
self._override_instances:
|
|
93
|
+
self._override_instances: Dict[Type[Any], Any] = {}
|
|
93
94
|
self._strict = strict
|
|
94
95
|
|
|
95
96
|
# Components
|
|
@@ -116,7 +117,7 @@ class Container:
|
|
|
116
117
|
return self._strict
|
|
117
118
|
|
|
118
119
|
@property
|
|
119
|
-
def providers(self) ->
|
|
120
|
+
def providers(self) -> Dict[Type[Any], Provider]:
|
|
120
121
|
"""Get the registered providers.
|
|
121
122
|
|
|
122
123
|
Returns:
|
|
@@ -332,7 +333,7 @@ class Container:
|
|
|
332
333
|
"""
|
|
333
334
|
related_providers = []
|
|
334
335
|
|
|
335
|
-
for parameter in provider.parameters:
|
|
336
|
+
for parameter in provider.parameters.values():
|
|
336
337
|
if parameter.annotation is inspect._empty: # noqa
|
|
337
338
|
raise TypeError(
|
|
338
339
|
f"Missing provider `{provider}` "
|
|
@@ -362,7 +363,7 @@ class Container:
|
|
|
362
363
|
"registered with matching scopes."
|
|
363
364
|
)
|
|
364
365
|
|
|
365
|
-
def _detect_scope(self, obj: Callable[..., Any]) -> Scope
|
|
366
|
+
def _detect_scope(self, obj: Callable[..., Any]) -> Optional[Scope]:
|
|
366
367
|
"""Detect the scope for a provider.
|
|
367
368
|
|
|
368
369
|
Args:
|
|
@@ -371,7 +372,7 @@ class Container:
|
|
|
371
372
|
The auto scope, or None if the auto scope cannot be detected.
|
|
372
373
|
"""
|
|
373
374
|
has_transient, has_request, has_singleton = False, False, False
|
|
374
|
-
for parameter in
|
|
375
|
+
for parameter in get_signature(obj).parameters.values():
|
|
375
376
|
sub_provider = self._get_or_register_provider(parameter.annotation)
|
|
376
377
|
if not has_transient and sub_provider.scope == "transient":
|
|
377
378
|
has_transient = True
|
|
@@ -388,7 +389,7 @@ class Container:
|
|
|
388
389
|
return None
|
|
389
390
|
|
|
390
391
|
def register_module(
|
|
391
|
-
self, module: Module
|
|
392
|
+
self, module: Union[Module, Type[Module], Callable[[Container], None]]
|
|
392
393
|
) -> None:
|
|
393
394
|
"""Register a module as a callable, module type, or module instance.
|
|
394
395
|
|
|
@@ -632,11 +633,14 @@ class Container:
|
|
|
632
633
|
def inject(self) -> Callable[[Callable[P, T]], Callable[P, T]]: ...
|
|
633
634
|
|
|
634
635
|
def inject(
|
|
635
|
-
self, obj: Callable[P, T
|
|
636
|
-
) ->
|
|
637
|
-
Callable[
|
|
638
|
-
|
|
639
|
-
|
|
636
|
+
self, obj: Union[Callable[P, Union[T, Awaitable[T]]], None] = None
|
|
637
|
+
) -> Union[
|
|
638
|
+
Callable[
|
|
639
|
+
[Callable[P, Union[T, Awaitable[T]]]],
|
|
640
|
+
Callable[P, Union[T, Awaitable[T]]],
|
|
641
|
+
],
|
|
642
|
+
Callable[P, Union[T, Awaitable[T]]],
|
|
643
|
+
]:
|
|
640
644
|
"""Decorator to inject dependencies into a callable.
|
|
641
645
|
|
|
642
646
|
Args:
|
|
@@ -648,8 +652,8 @@ class Container:
|
|
|
648
652
|
"""
|
|
649
653
|
|
|
650
654
|
def decorator(
|
|
651
|
-
obj: Callable[P, T
|
|
652
|
-
) -> Callable[P, T
|
|
655
|
+
obj: Callable[P, Union[T, Awaitable[T]]],
|
|
656
|
+
) -> Callable[P, Union[T, Awaitable[T]]]:
|
|
653
657
|
injected_params = self._get_injected_params(obj)
|
|
654
658
|
|
|
655
659
|
if inspect.iscoroutinefunction(obj):
|
|
@@ -690,9 +694,12 @@ class Container:
|
|
|
690
694
|
def scan(
|
|
691
695
|
self,
|
|
692
696
|
/,
|
|
693
|
-
packages:
|
|
697
|
+
packages: Union[
|
|
698
|
+
Union[types.ModuleType, str],
|
|
699
|
+
Iterable[Union[types.ModuleType, str]],
|
|
700
|
+
],
|
|
694
701
|
*,
|
|
695
|
-
tags: Iterable[str]
|
|
702
|
+
tags: Optional[Iterable[str]] = None,
|
|
696
703
|
) -> None:
|
|
697
704
|
"""Scan packages or modules for decorated members and inject dependencies.
|
|
698
705
|
|
|
@@ -716,26 +723,32 @@ class Container:
|
|
|
716
723
|
Raises:
|
|
717
724
|
TypeError: If the provider return annotation is missing or invalid.
|
|
718
725
|
"""
|
|
719
|
-
annotation =
|
|
726
|
+
annotation = get_signature(obj).return_annotation
|
|
720
727
|
|
|
721
|
-
if annotation is
|
|
728
|
+
if annotation is inspect._empty: # noqa
|
|
722
729
|
raise TypeError(
|
|
723
730
|
f"Missing `{get_full_qualname(obj)}` provider return annotation."
|
|
724
731
|
)
|
|
725
732
|
|
|
726
|
-
|
|
727
|
-
|
|
733
|
+
origin = get_origin(annotation) or annotation
|
|
734
|
+
args = get_args(annotation)
|
|
735
|
+
|
|
736
|
+
# Supported generic types
|
|
737
|
+
if origin in (list, dict, tuple, Annotated):
|
|
728
738
|
if args:
|
|
729
|
-
return
|
|
739
|
+
return annotation
|
|
730
740
|
else:
|
|
731
741
|
raise TypeError(
|
|
732
|
-
f"Cannot use `{get_full_qualname(obj)}`
|
|
742
|
+
f"Cannot use `{get_full_qualname(obj)}` generic type annotation "
|
|
733
743
|
"without actual type."
|
|
734
744
|
)
|
|
735
745
|
|
|
736
|
-
|
|
746
|
+
try:
|
|
747
|
+
return args[0]
|
|
748
|
+
except IndexError:
|
|
749
|
+
return annotation
|
|
737
750
|
|
|
738
|
-
def _get_injected_params(self, obj: Callable[..., Any]) ->
|
|
751
|
+
def _get_injected_params(self, obj: Callable[..., Any]) -> Dict[str, Any]:
|
|
739
752
|
"""Get the injected parameters of a callable object.
|
|
740
753
|
|
|
741
754
|
Args:
|
|
@@ -746,8 +759,8 @@ class Container:
|
|
|
746
759
|
of the injected parameters.
|
|
747
760
|
"""
|
|
748
761
|
injected_params = {}
|
|
749
|
-
for parameter in
|
|
750
|
-
if not
|
|
762
|
+
for parameter in get_signature(obj).parameters.values():
|
|
763
|
+
if not isinstance(parameter.default, Marker):
|
|
751
764
|
continue
|
|
752
765
|
try:
|
|
753
766
|
self._validate_injected_parameter(obj, parameter)
|
anydi/_context.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import abc
|
|
4
4
|
import contextlib
|
|
5
5
|
from types import TracebackType
|
|
6
|
-
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type, TypeVar, cast
|
|
7
7
|
|
|
8
8
|
from typing_extensions import Self, final
|
|
9
9
|
|
|
@@ -87,7 +87,7 @@ class ScopedContext(abc.ABC):
|
|
|
87
87
|
|
|
88
88
|
def _get_provider_arguments(
|
|
89
89
|
self, provider: Provider
|
|
90
|
-
) ->
|
|
90
|
+
) -> Tuple[List[Any], Dict[str, Any]]:
|
|
91
91
|
"""Retrieve the arguments for a provider.
|
|
92
92
|
|
|
93
93
|
Args:
|
|
@@ -97,7 +97,7 @@ class ScopedContext(abc.ABC):
|
|
|
97
97
|
The arguments for the provider.
|
|
98
98
|
"""
|
|
99
99
|
args, kwargs = [], {}
|
|
100
|
-
for parameter in provider.parameters:
|
|
100
|
+
for parameter in provider.parameters.values():
|
|
101
101
|
instance = self.container.resolve(parameter.annotation)
|
|
102
102
|
if parameter.kind == parameter.POSITIONAL_ONLY:
|
|
103
103
|
args.append(instance)
|
|
@@ -107,7 +107,7 @@ class ScopedContext(abc.ABC):
|
|
|
107
107
|
|
|
108
108
|
async def _aget_provider_arguments(
|
|
109
109
|
self, provider: Provider
|
|
110
|
-
) ->
|
|
110
|
+
) -> Tuple[List[Any], Dict[str, Any]]:
|
|
111
111
|
"""Asynchronously retrieve the arguments for a provider.
|
|
112
112
|
|
|
113
113
|
Args:
|
|
@@ -117,7 +117,7 @@ class ScopedContext(abc.ABC):
|
|
|
117
117
|
The arguments for the provider.
|
|
118
118
|
"""
|
|
119
119
|
args, kwargs = [], {}
|
|
120
|
-
for parameter in provider.parameters:
|
|
120
|
+
for parameter in provider.parameters.values():
|
|
121
121
|
instance = await self.container.aresolve(parameter.annotation)
|
|
122
122
|
if parameter.kind == parameter.POSITIONAL_ONLY:
|
|
123
123
|
args.append(instance)
|
|
@@ -132,7 +132,7 @@ class ResourceScopedContext(ScopedContext):
|
|
|
132
132
|
def __init__(self, container: Container) -> None:
|
|
133
133
|
"""Initialize the ScopedContext."""
|
|
134
134
|
super().__init__(container)
|
|
135
|
-
self._instances:
|
|
135
|
+
self._instances: Dict[Type[Any], Any] = {}
|
|
136
136
|
self._stack = contextlib.ExitStack()
|
|
137
137
|
self._async_stack = contextlib.AsyncExitStack()
|
|
138
138
|
|
|
@@ -237,7 +237,7 @@ class ResourceScopedContext(ScopedContext):
|
|
|
237
237
|
|
|
238
238
|
def __exit__(
|
|
239
239
|
self,
|
|
240
|
-
exc_type:
|
|
240
|
+
exc_type: Type[BaseException] | None,
|
|
241
241
|
exc_val: BaseException | None,
|
|
242
242
|
exc_tb: TracebackType | None,
|
|
243
243
|
) -> None:
|
|
@@ -265,7 +265,7 @@ class ResourceScopedContext(ScopedContext):
|
|
|
265
265
|
|
|
266
266
|
async def __aexit__(
|
|
267
267
|
self,
|
|
268
|
-
exc_type:
|
|
268
|
+
exc_type: Type[BaseException] | None,
|
|
269
269
|
exc_val: BaseException | None,
|
|
270
270
|
exc_tb: TracebackType | None,
|
|
271
271
|
) -> None:
|
anydi/_module.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Callable, TypeVar
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple, Type, TypeVar, Union
|
|
7
7
|
|
|
8
8
|
from typing_extensions import Concatenate, NamedTuple, ParamSpec
|
|
9
9
|
|
|
@@ -24,7 +24,7 @@ class ModuleMeta(type):
|
|
|
24
24
|
and stores it in the `providers` attribute.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
-
def __new__(cls, name: str, bases:
|
|
27
|
+
def __new__(cls, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]) -> Any:
|
|
28
28
|
"""Create a new instance of the ModuleMeta class.
|
|
29
29
|
|
|
30
30
|
This method extracts provider information from the class attributes and
|
|
@@ -49,7 +49,7 @@ class ModuleMeta(type):
|
|
|
49
49
|
class Module(metaclass=ModuleMeta):
|
|
50
50
|
"""A base class for defining AnyDI modules."""
|
|
51
51
|
|
|
52
|
-
providers:
|
|
52
|
+
providers: List[Tuple[str, ProviderDecoratorArgs]]
|
|
53
53
|
|
|
54
54
|
def configure(self, container: Container) -> None:
|
|
55
55
|
"""Configure the AnyDI container with providers and their dependencies.
|
|
@@ -67,7 +67,7 @@ class ModuleRegistry:
|
|
|
67
67
|
self.container = container
|
|
68
68
|
|
|
69
69
|
def register(
|
|
70
|
-
self, module: Module
|
|
70
|
+
self, module: Union[Module, Type[Module], Callable[[Container], None]]
|
|
71
71
|
) -> None:
|
|
72
72
|
"""Register a module as a callable, module type, or module instance.
|
|
73
73
|
|
anydi/_scanner.py
CHANGED
|
@@ -10,6 +10,8 @@ from typing import (
|
|
|
10
10
|
Any,
|
|
11
11
|
Callable,
|
|
12
12
|
Iterable,
|
|
13
|
+
List,
|
|
14
|
+
Optional,
|
|
13
15
|
TypeVar,
|
|
14
16
|
Union,
|
|
15
17
|
cast,
|
|
@@ -19,8 +21,8 @@ from typing import (
|
|
|
19
21
|
|
|
20
22
|
from typing_extensions import NamedTuple, ParamSpec
|
|
21
23
|
|
|
22
|
-
from ._types import
|
|
23
|
-
from ._utils import
|
|
24
|
+
from ._types import Marker
|
|
25
|
+
from ._utils import get_signature
|
|
24
26
|
|
|
25
27
|
if TYPE_CHECKING:
|
|
26
28
|
from ._container import Container
|
|
@@ -54,9 +56,9 @@ class Scanner:
|
|
|
54
56
|
def scan(
|
|
55
57
|
self,
|
|
56
58
|
/,
|
|
57
|
-
packages: ModuleType
|
|
59
|
+
packages: Union[Union[ModuleType, str], Iterable[Union[ModuleType, str]]],
|
|
58
60
|
*,
|
|
59
|
-
tags: Iterable[str]
|
|
61
|
+
tags: Optional[Iterable[str]] = None,
|
|
60
62
|
) -> None:
|
|
61
63
|
"""Scan packages or modules for decorated members and inject dependencies.
|
|
62
64
|
|
|
@@ -66,10 +68,10 @@ class Scanner:
|
|
|
66
68
|
tags: Optional list of tags to filter the scanned members. Only members
|
|
67
69
|
with at least one matching tag will be scanned. Defaults to None.
|
|
68
70
|
"""
|
|
69
|
-
dependencies:
|
|
71
|
+
dependencies: List[Dependency] = []
|
|
70
72
|
|
|
71
73
|
if isinstance(packages, Iterable) and not isinstance(packages, str):
|
|
72
|
-
scan_packages: Iterable[ModuleType
|
|
74
|
+
scan_packages: Iterable[Union[ModuleType, str]] = packages
|
|
73
75
|
else:
|
|
74
76
|
scan_packages = cast(Iterable[Union[ModuleType, str]], [packages])
|
|
75
77
|
|
|
@@ -82,10 +84,10 @@ class Scanner:
|
|
|
82
84
|
|
|
83
85
|
def _scan_package(
|
|
84
86
|
self,
|
|
85
|
-
package: ModuleType
|
|
87
|
+
package: Union[ModuleType, str],
|
|
86
88
|
*,
|
|
87
|
-
tags: Iterable[str]
|
|
88
|
-
) ->
|
|
89
|
+
tags: Optional[Iterable[str]] = None,
|
|
90
|
+
) -> List[Dependency]:
|
|
89
91
|
"""Scan a package or module for decorated members.
|
|
90
92
|
|
|
91
93
|
Args:
|
|
@@ -105,7 +107,7 @@ class Scanner:
|
|
|
105
107
|
if not package_path:
|
|
106
108
|
return self._scan_module(package, tags=tags)
|
|
107
109
|
|
|
108
|
-
dependencies:
|
|
110
|
+
dependencies: List[Dependency] = []
|
|
109
111
|
|
|
110
112
|
for module_info in pkgutil.walk_packages(
|
|
111
113
|
path=package_path, prefix=package.__name__ + "."
|
|
@@ -117,7 +119,7 @@ class Scanner:
|
|
|
117
119
|
|
|
118
120
|
def _scan_module(
|
|
119
121
|
self, module: ModuleType, *, tags: Iterable[str]
|
|
120
|
-
) ->
|
|
122
|
+
) -> List[Dependency]:
|
|
121
123
|
"""Scan a module for decorated members.
|
|
122
124
|
|
|
123
125
|
Args:
|
|
@@ -128,7 +130,7 @@ class Scanner:
|
|
|
128
130
|
Returns:
|
|
129
131
|
A list of scanned dependencies.
|
|
130
132
|
"""
|
|
131
|
-
dependencies:
|
|
133
|
+
dependencies: List[Dependency] = []
|
|
132
134
|
|
|
133
135
|
for _, member in inspect.getmembers(module):
|
|
134
136
|
if getattr(member, "__module__", None) != module.__name__ or not callable(
|
|
@@ -157,11 +159,11 @@ class Scanner:
|
|
|
157
159
|
|
|
158
160
|
# Get by Marker
|
|
159
161
|
if inspect.isclass(member):
|
|
160
|
-
|
|
162
|
+
signature = get_signature(member.__init__)
|
|
161
163
|
else:
|
|
162
|
-
|
|
163
|
-
for parameter in parameters:
|
|
164
|
-
if
|
|
164
|
+
signature = get_signature(member)
|
|
165
|
+
for parameter in signature.parameters.values():
|
|
166
|
+
if isinstance(parameter.default, Marker):
|
|
165
167
|
dependencies.append(
|
|
166
168
|
self._create_dependency(member=member, module=module)
|
|
167
169
|
)
|
|
@@ -186,7 +188,7 @@ class Scanner:
|
|
|
186
188
|
|
|
187
189
|
class InjectDecoratorArgs(NamedTuple):
|
|
188
190
|
wrapped: bool
|
|
189
|
-
tags: Iterable[str]
|
|
191
|
+
tags: Optional[Iterable[str]]
|
|
190
192
|
|
|
191
193
|
|
|
192
194
|
@overload
|
|
@@ -195,14 +197,20 @@ def injectable(obj: Callable[P, T]) -> Callable[P, T]: ...
|
|
|
195
197
|
|
|
196
198
|
@overload
|
|
197
199
|
def injectable(
|
|
198
|
-
*, tags: Iterable[str]
|
|
200
|
+
*, tags: Optional[Iterable[str]] = None
|
|
199
201
|
) -> Callable[[Callable[P, T]], Callable[P, T]]: ...
|
|
200
202
|
|
|
201
203
|
|
|
202
204
|
def injectable(
|
|
203
|
-
obj: Callable[P, T]
|
|
204
|
-
tags: Iterable[str]
|
|
205
|
-
) ->
|
|
205
|
+
obj: Optional[Callable[P, T]] = None,
|
|
206
|
+
tags: Optional[Iterable[str]] = None,
|
|
207
|
+
) -> Union[
|
|
208
|
+
Callable[
|
|
209
|
+
[Callable[P, T]],
|
|
210
|
+
Callable[P, T],
|
|
211
|
+
],
|
|
212
|
+
Callable[P, T],
|
|
213
|
+
]:
|
|
206
214
|
"""Decorator for marking a function or method as requiring dependency injection.
|
|
207
215
|
|
|
208
216
|
Args:
|
anydi/_types.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import inspect
|
|
4
2
|
from dataclasses import dataclass
|
|
5
3
|
from functools import cached_property
|
|
6
4
|
from typing import Any, Callable, Type, TypeVar, Union
|
|
7
5
|
|
|
8
|
-
from typing_extensions import Annotated, Literal,
|
|
6
|
+
from typing_extensions import Annotated, Literal, Mapping, TypeAlias
|
|
9
7
|
|
|
10
|
-
from ._utils import get_full_qualname,
|
|
8
|
+
from ._utils import get_full_qualname, get_signature
|
|
11
9
|
|
|
12
10
|
Scope = Literal["transient", "singleton", "request"]
|
|
13
11
|
|
|
@@ -21,14 +19,6 @@ class Marker:
|
|
|
21
19
|
|
|
22
20
|
__slots__ = ()
|
|
23
21
|
|
|
24
|
-
def __call__(self) -> Self:
|
|
25
|
-
return self
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def is_marker(obj: Any) -> bool:
|
|
29
|
-
"""Checks if an object is a marker."""
|
|
30
|
-
return isinstance(obj, Marker)
|
|
31
|
-
|
|
32
22
|
|
|
33
23
|
@dataclass(frozen=True)
|
|
34
24
|
class Provider:
|
|
@@ -60,13 +50,13 @@ class Provider:
|
|
|
60
50
|
return get_full_qualname(self.obj)
|
|
61
51
|
|
|
62
52
|
@cached_property
|
|
63
|
-
def parameters(self) ->
|
|
53
|
+
def parameters(self) -> Mapping[str, inspect.Parameter]:
|
|
64
54
|
"""Returns the parameters of the provider as a mapping.
|
|
65
55
|
|
|
66
56
|
Returns:
|
|
67
57
|
The parameters of the provider.
|
|
68
58
|
"""
|
|
69
|
-
return
|
|
59
|
+
return get_signature(self.obj).parameters
|
|
70
60
|
|
|
71
61
|
@cached_property
|
|
72
62
|
def is_class(self) -> bool:
|
anydi/_utils.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
"""Shared AnyDI utils module."""
|
|
2
2
|
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
3
|
import builtins
|
|
6
4
|
import functools
|
|
7
5
|
import inspect
|
|
8
6
|
import sys
|
|
9
|
-
from typing import Any, Callable,
|
|
7
|
+
from typing import Any, Callable, Dict, Type, TypeVar
|
|
10
8
|
|
|
11
9
|
from typing_extensions import Annotated, ParamSpec, get_origin
|
|
12
10
|
|
|
@@ -16,23 +14,22 @@ except ImportError:
|
|
|
16
14
|
anyio = None # type: ignore[assignment]
|
|
17
15
|
|
|
18
16
|
|
|
19
|
-
if sys.version_info < (3, 9): # pragma: nocover
|
|
20
|
-
|
|
21
|
-
def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
|
|
22
|
-
return type_._evaluate(globalns, localns) # noqa
|
|
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
17
|
T = TypeVar("T")
|
|
31
18
|
P = ParamSpec("P")
|
|
32
19
|
|
|
33
20
|
|
|
34
21
|
def get_full_qualname(obj: Any) -> str:
|
|
35
|
-
"""Get the fully qualified name of an object.
|
|
22
|
+
"""Get the fully qualified name of an object.
|
|
23
|
+
|
|
24
|
+
This function returns the fully qualified name of the given object,
|
|
25
|
+
which includes both the module name and the object's qualname.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
obj: The object for which to retrieve the fully qualified name.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The fully qualified name of the object.
|
|
32
|
+
"""
|
|
36
33
|
origin = get_origin(obj)
|
|
37
34
|
if origin is Annotated:
|
|
38
35
|
metadata = ", ".join(
|
|
@@ -56,44 +53,35 @@ def get_full_qualname(obj: Any) -> str:
|
|
|
56
53
|
return f"{module_name}.{qualname}"
|
|
57
54
|
|
|
58
55
|
|
|
59
|
-
def is_builtin_type(tp:
|
|
60
|
-
"""
|
|
56
|
+
def is_builtin_type(tp: Type[Any]) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Check if the given type is a built-in type.
|
|
59
|
+
Args:
|
|
60
|
+
tp (type): The type to check.
|
|
61
|
+
Returns:
|
|
62
|
+
bool: True if the type is a built-in type, False otherwise.
|
|
63
|
+
"""
|
|
61
64
|
return tp.__module__ == builtins.__name__
|
|
62
65
|
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return evaluate_forwardref(forward_ref, globalns, globalns)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
|
|
71
|
-
"""Get the typed annotation of a parameter."""
|
|
72
|
-
if isinstance(annotation, str):
|
|
73
|
-
annotation = ForwardRef(annotation)
|
|
74
|
-
annotation = evaluate_forwardref(annotation, globalns, globalns)
|
|
75
|
-
return annotation
|
|
76
|
-
|
|
67
|
+
@functools.lru_cache(maxsize=None)
|
|
68
|
+
def get_signature(obj: Callable[..., Any]) -> inspect.Signature:
|
|
69
|
+
"""Get the signature of a callable object.
|
|
77
70
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
annotation = signature.return_annotation
|
|
82
|
-
if annotation is inspect.Signature.empty:
|
|
83
|
-
return None
|
|
84
|
-
globalns = getattr(obj, "__globals__", {})
|
|
85
|
-
return get_typed_annotation(annotation, globalns)
|
|
71
|
+
This function uses the `inspect.signature` function to retrieve the signature
|
|
72
|
+
of the given callable object. It applies an LRU cache decorator to improve
|
|
73
|
+
performance by caching the signatures of previously inspected objects.
|
|
86
74
|
|
|
75
|
+
Args:
|
|
76
|
+
obj: The callable object to inspect.
|
|
87
77
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
for name, parameter in inspect.signature(obj).parameters.items()
|
|
96
|
-
]
|
|
78
|
+
Returns:
|
|
79
|
+
The signature of the callable object.
|
|
80
|
+
"""
|
|
81
|
+
signature_kwargs: Dict[str, Any] = {}
|
|
82
|
+
if sys.version_info >= (3, 10):
|
|
83
|
+
signature_kwargs["eval_str"] = True
|
|
84
|
+
return inspect.signature(obj, **signature_kwargs)
|
|
97
85
|
|
|
98
86
|
|
|
99
87
|
async def run_async(
|
|
@@ -102,7 +90,19 @@ async def run_async(
|
|
|
102
90
|
*args: P.args,
|
|
103
91
|
**kwargs: P.kwargs,
|
|
104
92
|
) -> T:
|
|
105
|
-
"""Runs the given function asynchronously using the `anyio` library.
|
|
93
|
+
"""Runs the given function asynchronously using the `anyio` library.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
func: The function to run asynchronously.
|
|
97
|
+
args: The positional arguments to pass to the function.
|
|
98
|
+
kwargs: The keyword arguments to pass to the function.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The result of the function.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ImportError: If the `anyio` library is not installed.
|
|
105
|
+
"""
|
|
106
106
|
if not anyio:
|
|
107
107
|
raise ImportError(
|
|
108
108
|
"`anyio` library is not currently installed. Please make sure to install "
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
from django.apps.registry import apps
|
|
4
|
+
from django.utils.functional import SimpleLazyObject
|
|
5
|
+
|
|
6
|
+
import anydi
|
|
7
|
+
|
|
8
|
+
from .apps import ContainerConfig
|
|
9
|
+
|
|
10
|
+
__all__ = ["container"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_container() -> anydi.Container:
|
|
14
|
+
app_config = cast(ContainerConfig, apps.get_app_config(ContainerConfig.label))
|
|
15
|
+
return app_config.container
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
container = cast(anydi.Container, SimpleLazyObject(_get_container))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
|
|
3
|
+
from django.urls import URLPattern, URLResolver
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def iter_urlpatterns(
|
|
7
|
+
urlpatterns: list[URLPattern | URLResolver],
|
|
8
|
+
) -> Iterator[URLPattern]:
|
|
9
|
+
"""Iterate over all views in urlpatterns."""
|
|
10
|
+
for url_pattern in urlpatterns:
|
|
11
|
+
if isinstance(url_pattern, URLResolver):
|
|
12
|
+
yield from iter_urlpatterns(url_pattern.url_patterns)
|
|
13
|
+
else:
|
|
14
|
+
yield url_pattern
|
anydi/ext/django/apps.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import types
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Annotated, Any, get_origin
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.core.cache import BaseCache, caches
|
|
8
|
+
from django.db import connections
|
|
9
|
+
from django.db.backends.base.base import BaseDatabaseWrapper
|
|
10
|
+
from django.urls import get_resolver
|
|
11
|
+
from django.utils.module_loading import import_string
|
|
12
|
+
|
|
13
|
+
import anydi
|
|
14
|
+
|
|
15
|
+
from ._utils import iter_urlpatterns
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ContainerConfig(AppConfig): # type: ignore[misc]
|
|
19
|
+
name = "anydi.ext.django"
|
|
20
|
+
label = "anydi_django"
|
|
21
|
+
|
|
22
|
+
# Prefix for Django settings
|
|
23
|
+
settings_prefix = "django.conf.settings"
|
|
24
|
+
|
|
25
|
+
def __init__(self, app_name: str, app_module: types.ModuleType | None) -> None:
|
|
26
|
+
super().__init__(app_name, app_module)
|
|
27
|
+
# Create a container
|
|
28
|
+
self.container = anydi.Container(
|
|
29
|
+
strict=getattr(settings, "ANYDI_STRICT_MODE", False),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def ready(self) -> None:
|
|
33
|
+
# Register Django settings
|
|
34
|
+
if getattr(settings, "ANYDI_REGISTER_SETTINGS", False):
|
|
35
|
+
self.register_settings()
|
|
36
|
+
|
|
37
|
+
# Register Django components
|
|
38
|
+
if getattr(settings, "ANYDI_REGISTER_COMPONENTS", False):
|
|
39
|
+
self.register_components()
|
|
40
|
+
|
|
41
|
+
# Register modules
|
|
42
|
+
for module_path in getattr(settings, "ANYDI_MODULES", []):
|
|
43
|
+
module_cls = import_string(module_path)
|
|
44
|
+
self.container.register_module(module_cls)
|
|
45
|
+
|
|
46
|
+
# Patching the django-ninja framework if it installed
|
|
47
|
+
if getattr(settings, "ANYDI_PATCH_NINJA", False):
|
|
48
|
+
self.patch_ninja()
|
|
49
|
+
|
|
50
|
+
# Auto-injecting the container into views
|
|
51
|
+
if urlconf := getattr(settings, "ANYDI_AUTO_INJECT_URLCONF", None):
|
|
52
|
+
self.auto_inject_urlconf(urlconf)
|
|
53
|
+
|
|
54
|
+
def register_settings(self) -> None: # noqa: C901
|
|
55
|
+
"""Register Django settings into the container."""
|
|
56
|
+
|
|
57
|
+
def _get_setting_value(value: Any) -> Any:
|
|
58
|
+
return lambda: value
|
|
59
|
+
|
|
60
|
+
for setting_name, value in settings.__dict__.items():
|
|
61
|
+
if not setting_name.isupper():
|
|
62
|
+
continue
|
|
63
|
+
self.container.register(
|
|
64
|
+
Annotated[Any, f"{self.settings_prefix}.{setting_name}"],
|
|
65
|
+
_get_setting_value(value),
|
|
66
|
+
scope="singleton",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _aware_settings(interface: Any) -> Any:
|
|
70
|
+
origin = get_origin(interface)
|
|
71
|
+
if origin is not Annotated:
|
|
72
|
+
return interface
|
|
73
|
+
named = interface.__metadata__[-1]
|
|
74
|
+
|
|
75
|
+
if isinstance(named, str):
|
|
76
|
+
_, setting_name = named.rsplit(self.settings_prefix, maxsplit=1)
|
|
77
|
+
return Annotated[Any, f"{self.settings_prefix}.{setting_name}"]
|
|
78
|
+
return interface
|
|
79
|
+
|
|
80
|
+
def _resolve(resolve: Any) -> Any:
|
|
81
|
+
@wraps(resolve)
|
|
82
|
+
def wrapper(interface: Any) -> Any:
|
|
83
|
+
return resolve(_aware_settings(interface))
|
|
84
|
+
|
|
85
|
+
return wrapper
|
|
86
|
+
|
|
87
|
+
def _aresolve(resolve: Any) -> Any:
|
|
88
|
+
@wraps(resolve)
|
|
89
|
+
async def wrapper(interface: Any) -> Any:
|
|
90
|
+
return await resolve(_aware_settings(interface))
|
|
91
|
+
|
|
92
|
+
return wrapper
|
|
93
|
+
|
|
94
|
+
# Patch resolvers
|
|
95
|
+
self.container.resolve = _resolve(self.container.resolve) # type: ignore[method-assign] # noqa
|
|
96
|
+
self.container.aresolve = _aresolve(self.container.aresolve) # type: ignore[method-assign] # noqa
|
|
97
|
+
|
|
98
|
+
def register_components(self) -> None:
|
|
99
|
+
"""Register Django components into the container."""
|
|
100
|
+
|
|
101
|
+
# Register caches
|
|
102
|
+
def _get_cache(cache_name: str) -> Any:
|
|
103
|
+
return lambda: caches[cache_name]
|
|
104
|
+
|
|
105
|
+
for cache_name in caches:
|
|
106
|
+
self.container.register(
|
|
107
|
+
Annotated[BaseCache, cache_name],
|
|
108
|
+
_get_cache(cache_name),
|
|
109
|
+
scope="singleton",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Register database connections
|
|
113
|
+
|
|
114
|
+
def _get_connection(alias: str) -> Any:
|
|
115
|
+
return lambda: connections[alias]
|
|
116
|
+
|
|
117
|
+
for alias in connections:
|
|
118
|
+
self.container.register(
|
|
119
|
+
Annotated[BaseDatabaseWrapper, alias],
|
|
120
|
+
_get_connection(alias),
|
|
121
|
+
scope="singleton",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def auto_inject_urlconf(self, urlconf: str) -> None:
|
|
125
|
+
"""Auto-inject the container into views."""
|
|
126
|
+
resolver = get_resolver(urlconf)
|
|
127
|
+
for pattern in iter_urlpatterns(resolver.url_patterns):
|
|
128
|
+
# Skip django-ninja views
|
|
129
|
+
if pattern.lookup_str.startswith("ninja."):
|
|
130
|
+
continue
|
|
131
|
+
pattern.callback = self.container.inject(pattern.callback)
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def patch_ninja() -> None:
|
|
135
|
+
"""Patch the django-ninja framework."""
|
|
136
|
+
from .ninja import patch
|
|
137
|
+
|
|
138
|
+
patch()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from ninja import operation
|
|
3
|
+
except ImportError as exc:
|
|
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() -> 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,73 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from django.http import HttpRequest, HttpResponseBase
|
|
4
|
+
from ninja.operation import (
|
|
5
|
+
AsyncOperation as BaseAsyncOperation, # noqa
|
|
6
|
+
Operation as BaseOperation,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from anydi.ext.django import container
|
|
10
|
+
|
|
11
|
+
from ._signature import ViewSignature
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _update_exc_args(exc: Exception) -> None:
|
|
15
|
+
if isinstance(exc, TypeError) and "required positional argument" in str(exc):
|
|
16
|
+
msg = "Did you fail to use functools.wraps() in a decorator?"
|
|
17
|
+
msg = f"{exc.args[0]}: {msg}" if exc.args else msg
|
|
18
|
+
exc.args = (msg,) + exc.args[1:]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Operation(BaseOperation):
|
|
22
|
+
signature: ViewSignature
|
|
23
|
+
|
|
24
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
25
|
+
super().__init__(*args, **kwargs)
|
|
26
|
+
self.dependencies = self.signature.dependencies
|
|
27
|
+
|
|
28
|
+
def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
|
|
29
|
+
error = self._run_checks(request)
|
|
30
|
+
if error:
|
|
31
|
+
return error
|
|
32
|
+
try:
|
|
33
|
+
temporal_response = self.api.create_temporal_response(request)
|
|
34
|
+
values = self._get_values(request, kw, temporal_response)
|
|
35
|
+
values.update(self._get_dependencies())
|
|
36
|
+
result = self.view_func(request, **values)
|
|
37
|
+
return self._result_to_response(request, result, temporal_response)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
_update_exc_args(e)
|
|
40
|
+
return self.api.on_exception(request, e)
|
|
41
|
+
|
|
42
|
+
def _get_dependencies(self) -> dict[str, Any]:
|
|
43
|
+
return {
|
|
44
|
+
name: container.resolve(interface) for name, interface in self.dependencies
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AsyncOperation(BaseAsyncOperation):
|
|
49
|
+
signature: ViewSignature
|
|
50
|
+
|
|
51
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
52
|
+
super().__init__(*args, **kwargs)
|
|
53
|
+
self.dependencies = self.signature.dependencies
|
|
54
|
+
|
|
55
|
+
async def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase: # type: ignore
|
|
56
|
+
error = await self._run_checks(request)
|
|
57
|
+
if error:
|
|
58
|
+
return error
|
|
59
|
+
try:
|
|
60
|
+
temporal_response = self.api.create_temporal_response(request)
|
|
61
|
+
values = self._get_values(request, kw, temporal_response)
|
|
62
|
+
values.update(await self._get_dependencies())
|
|
63
|
+
result = await self.view_func(request, **values)
|
|
64
|
+
return self._result_to_response(request, result, temporal_response)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
_update_exc_args(e)
|
|
67
|
+
return self.api.on_exception(request, e)
|
|
68
|
+
|
|
69
|
+
async def _get_dependencies(self) -> dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
name: await container.aresolve(interface)
|
|
72
|
+
for name, interface in self.dependencies
|
|
73
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from django.http import HttpResponse
|
|
6
|
+
from ninja.signature.details import ( # noqa
|
|
7
|
+
FuncParam,
|
|
8
|
+
ViewSignature as BaseViewSignature,
|
|
9
|
+
)
|
|
10
|
+
from ninja.signature.utils import get_path_param_names, get_typed_signature
|
|
11
|
+
|
|
12
|
+
from anydi._types import Marker # noqa
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ViewSignature(BaseViewSignature):
|
|
16
|
+
def __init__(self, path: str, view_func: Callable[..., Any]) -> None:
|
|
17
|
+
self.view_func = view_func
|
|
18
|
+
self.signature = get_typed_signature(self.view_func)
|
|
19
|
+
self.path = path
|
|
20
|
+
self.path_params_names = get_path_param_names(path)
|
|
21
|
+
self.docstring = inspect.cleandoc(view_func.__doc__ or "")
|
|
22
|
+
self.has_kwargs = False
|
|
23
|
+
self.dependencies = []
|
|
24
|
+
|
|
25
|
+
self.params = []
|
|
26
|
+
for name, arg in self.signature.parameters.items():
|
|
27
|
+
if name == "request":
|
|
28
|
+
# TODO: maybe better assert that 1st param is request or check by type?
|
|
29
|
+
# maybe even have attribute like `has_request`
|
|
30
|
+
# so that users can ignore passing request if not needed
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
if arg.kind == arg.VAR_KEYWORD:
|
|
34
|
+
# Skipping **kwargs
|
|
35
|
+
self.has_kwargs = True
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
if arg.kind == arg.VAR_POSITIONAL:
|
|
39
|
+
# Skipping *args
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
if arg.annotation is HttpResponse:
|
|
43
|
+
self.response_arg = name
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
# Skip default values that are anydi dependency markers
|
|
47
|
+
if isinstance(arg.default, Marker):
|
|
48
|
+
self.dependencies.append((name, arg.annotation))
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
func_param = self._get_param_type(name, arg)
|
|
52
|
+
self.params.append(func_param)
|
|
53
|
+
|
|
54
|
+
if hasattr(view_func, "_ninja_contribute_args"):
|
|
55
|
+
for p_name, p_type, p_source in view_func._ninja_contribute_args: # noqa
|
|
56
|
+
self.params.append(
|
|
57
|
+
FuncParam(p_name, p_source.alias or p_name, p_source, p_type, False)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
self.models = self._create_models()
|
|
61
|
+
|
|
62
|
+
self._validate_view_path_params()
|
anydi/ext/fastapi.py
CHANGED
|
@@ -13,7 +13,7 @@ from starlette.requests import Request
|
|
|
13
13
|
from typing_extensions import Annotated, get_args, get_origin
|
|
14
14
|
|
|
15
15
|
from anydi import Container
|
|
16
|
-
from anydi._utils import get_full_qualname,
|
|
16
|
+
from anydi._utils import get_full_qualname, get_signature
|
|
17
17
|
|
|
18
18
|
from .starlette.middleware import RequestScopedMiddleware
|
|
19
19
|
|
|
@@ -47,7 +47,7 @@ def install(app: FastAPI, container: Container) -> None:
|
|
|
47
47
|
call, *params = dependant.cache_key
|
|
48
48
|
if not call:
|
|
49
49
|
continue # pragma: no cover
|
|
50
|
-
for parameter in
|
|
50
|
+
for parameter in get_signature(call).parameters.values():
|
|
51
51
|
_patch_route_parameter(call, parameter, container)
|
|
52
52
|
|
|
53
53
|
|
anydi/ext/pytest_plugin.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import inspect
|
|
4
|
-
from typing import Any, Callable, Iterator, cast
|
|
2
|
+
from typing import Any, Callable, Iterator, List, Tuple, cast
|
|
5
3
|
|
|
6
4
|
import pytest
|
|
7
5
|
|
|
@@ -51,8 +49,8 @@ def _anydi_should_inject(request: pytest.FixtureRequest) -> bool:
|
|
|
51
49
|
|
|
52
50
|
|
|
53
51
|
@pytest.fixture(scope="session")
|
|
54
|
-
def _anydi_unresolved() -> Iterator[
|
|
55
|
-
unresolved:
|
|
52
|
+
def _anydi_unresolved() -> Iterator[List[Any]]:
|
|
53
|
+
unresolved: List[Any] = []
|
|
56
54
|
yield unresolved
|
|
57
55
|
unresolved.clear()
|
|
58
56
|
|
|
@@ -60,9 +58,9 @@ def _anydi_unresolved() -> Iterator[list[Any]]:
|
|
|
60
58
|
@pytest.fixture
|
|
61
59
|
def _anydi_injected_parameter_iterator(
|
|
62
60
|
request: pytest.FixtureRequest,
|
|
63
|
-
_anydi_unresolved:
|
|
64
|
-
) -> Callable[[], Iterator[
|
|
65
|
-
def _iterator() -> Iterator[
|
|
61
|
+
_anydi_unresolved: List[str],
|
|
62
|
+
) -> Callable[[], Iterator[Tuple[str, Any]]]:
|
|
63
|
+
def _iterator() -> Iterator[Tuple[str, inspect.Parameter]]:
|
|
66
64
|
for name, parameter in inspect.signature(request.function).parameters.items():
|
|
67
65
|
if (
|
|
68
66
|
((interface := parameter.annotation) is parameter.empty)
|
|
@@ -79,8 +77,8 @@ def _anydi_injected_parameter_iterator(
|
|
|
79
77
|
def _anydi_inject(
|
|
80
78
|
request: pytest.FixtureRequest,
|
|
81
79
|
_anydi_should_inject: bool,
|
|
82
|
-
_anydi_injected_parameter_iterator: Callable[[], Iterator[
|
|
83
|
-
_anydi_unresolved:
|
|
80
|
+
_anydi_injected_parameter_iterator: Callable[[], Iterator[Tuple[str, Any]]],
|
|
81
|
+
_anydi_unresolved: List[str],
|
|
84
82
|
) -> None:
|
|
85
83
|
"""Inject dependencies into the test function."""
|
|
86
84
|
|
|
@@ -109,8 +107,8 @@ def _anydi_inject(
|
|
|
109
107
|
async def _anydi_ainject(
|
|
110
108
|
request: pytest.FixtureRequest,
|
|
111
109
|
_anydi_should_inject: bool,
|
|
112
|
-
_anydi_injected_parameter_iterator: Callable[[], Iterator[
|
|
113
|
-
_anydi_unresolved:
|
|
110
|
+
_anydi_injected_parameter_iterator: Callable[[], Iterator[Tuple[str, Any]]],
|
|
111
|
+
_anydi_unresolved: List[str],
|
|
114
112
|
) -> None:
|
|
115
113
|
"""Inject dependencies into the test function."""
|
|
116
114
|
if not inspect.iscoroutinefunction(request.function) or not _anydi_should_inject:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: anydi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0a0
|
|
4
4
|
Summary: Dependency Injection library
|
|
5
5
|
Home-page: https://github.com/antonrh/anydi
|
|
6
6
|
License: MIT
|
|
@@ -89,7 +89,7 @@ pip install anydi
|
|
|
89
89
|
*app.py*
|
|
90
90
|
|
|
91
91
|
```python
|
|
92
|
-
from anydi import
|
|
92
|
+
from anydi import Container, dep
|
|
93
93
|
|
|
94
94
|
container = Container()
|
|
95
95
|
|
|
@@ -100,7 +100,7 @@ def message() -> str:
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
@container.inject
|
|
103
|
-
def say_hello(message: str =
|
|
103
|
+
def say_hello(message: str = dep()) -> None:
|
|
104
104
|
print(message)
|
|
105
105
|
|
|
106
106
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
anydi/__init__.py,sha256=p2GQq45mulZRlC8oJ7bvOpjVFCeycsgI2IcNJjs_LhU,562
|
|
2
|
+
anydi/_container.py,sha256=bd1qe6DY6dyyIjBXbRhSglUD0MJX6OeVKVSGqUqFTuU,28347
|
|
3
|
+
anydi/_context.py,sha256=btGJzvTMkj5v95rAw6kjOclISKcSugC4wzrHlWlCk_I,10258
|
|
4
|
+
anydi/_logger.py,sha256=UpubJUnW83kffFxkhUlObm2DmZX1Pjqoz9YFKS-JOPg,52
|
|
5
|
+
anydi/_module.py,sha256=1fBo9-RWxo7TeyP0Y2uJokT-NXP2pjik6CXNoeo3l-8,3712
|
|
6
|
+
anydi/_scanner.py,sha256=YYPqzAQRgxxMpOSOpE7EqA_hccC6VjKi9Y8y_5HbtlA,6781
|
|
7
|
+
anydi/_types.py,sha256=AuHR2hvqaMazL55xj8I9YI6nyBAD16uQznNcmZoQsPE,3240
|
|
8
|
+
anydi/_utils.py,sha256=haRwC6YTTFf6CNTfwsRQ4NZsNByoUkEmE4-rh1-VYLs,3152
|
|
9
|
+
anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
anydi/ext/django/__init__.py,sha256=wsFBQP2j76Ui_0SJlN_8LKhZvEEdxA-w_yoyRAcaM58,59
|
|
11
|
+
anydi/ext/django/_container.py,sha256=cxVoYQG16WP0S_Yv4TnLwuaaT7NVEOhLWO-YdALJUb4,418
|
|
12
|
+
anydi/ext/django/_utils.py,sha256=C-1UabpwgCHUryAT-Pv8vU5QFfBahQlqhjecvihjtgY,430
|
|
13
|
+
anydi/ext/django/apps.py,sha256=d1679lLRtlzaEQCwSj92JBTJcEk0s0F5cD_Hi3jbM2Y,4739
|
|
14
|
+
anydi/ext/django/ninja/__init__.py,sha256=TBufIoxOWDbKHqd4-t6MBLwILqMUhwoEiJkpp1RrZZc,520
|
|
15
|
+
anydi/ext/django/ninja/_operation.py,sha256=A_RbMbJyVaafoeBhZpmPQ96l1RQFLA1FchkJC_uRKnM,2660
|
|
16
|
+
anydi/ext/django/ninja/_signature.py,sha256=Nbt2Zh-XlOtqrkhYydOUvCnNbuDytTunJPeQDtyTIyo,2177
|
|
17
|
+
anydi/ext/fastapi.py,sha256=zKuo7nNpcMXppUsime5LLKTkAiNrlEtcLRFK4FRur0c,5311
|
|
18
|
+
anydi/ext/pytest_plugin.py,sha256=tmncz2IbIR7FGlgmAtcf00yDKg9SQP9lCFnJBmtHx3A,3976
|
|
19
|
+
anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
anydi/ext/starlette/middleware.py,sha256=Ni0BQaPjs_Ha6zcLZYYJ3-XkslTCnL9aCSa06rnRDMI,1139
|
|
21
|
+
anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
anydi-0.25.0a0.dist-info/LICENSE,sha256=V6rU8a8fv6o2jQ-7ODHs0XfDFimot8Q6Km6CylRIDTo,1069
|
|
23
|
+
anydi-0.25.0a0.dist-info/METADATA,sha256=t4xUiUIM8ogKK0vHY9rr2xVaZ8iZmW85kZNkImahCXU,4373
|
|
24
|
+
anydi-0.25.0a0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
25
|
+
anydi-0.25.0a0.dist-info/entry_points.txt,sha256=GmQblwzxFg42zva1HyBYJJ7TvrTIcSAGBHmyi3bvsi4,42
|
|
26
|
+
anydi-0.25.0a0.dist-info/RECORD,,
|
anydi-0.24.3.dist-info/RECORD
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
anydi/__init__.py,sha256=aeaBp5vq09sG-e9sqqs9qpUtUIDNfOdFPrlAfE5Ku9E,584
|
|
2
|
-
anydi/_container.py,sha256=geBYRsvWECDuKJSAal84RjF88ZYf9_w4wxBUgOI3XWs,27978
|
|
3
|
-
anydi/_context.py,sha256=k956mFE_pfPdU0fxOJ8YRHBZx7sU_ln8fheYNofbmSs,10215
|
|
4
|
-
anydi/_logger.py,sha256=UpubJUnW83kffFxkhUlObm2DmZX1Pjqoz9YFKS-JOPg,52
|
|
5
|
-
anydi/_module.py,sha256=E1TfLud_Af-MPB83PxIzHVA1jlDW2FGaRP_il1a6y3Y,3675
|
|
6
|
-
anydi/_scanner.py,sha256=cyEk-K2Q8ssZStq8GrxMeEcCuAZMw-RXrjlgWEevKCs,6667
|
|
7
|
-
anydi/_types.py,sha256=vQTrFjsYhlMxfo1nOFem05x2QUJMQkVh4ZaC7W0XZJY,3434
|
|
8
|
-
anydi/_utils.py,sha256=xM5Lw4SNcUKL-9nA8arlhUeUveXFpvvY8cB9ZGV2h6g,3439
|
|
9
|
-
anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
anydi/ext/fastapi.py,sha256=kVUKVKtqCx1Nfnm1oh2BMyB0G7qQKPw6OGfxFlqUqtc,5305
|
|
11
|
-
anydi/ext/pytest_plugin.py,sha256=vtjQCwQ0_saG8qhYAYn2wQzXVrXfwXOEhJlTjGqtXA8,3999
|
|
12
|
-
anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
anydi/ext/starlette/middleware.py,sha256=Ni0BQaPjs_Ha6zcLZYYJ3-XkslTCnL9aCSa06rnRDMI,1139
|
|
14
|
-
anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
anydi-0.24.3.dist-info/LICENSE,sha256=V6rU8a8fv6o2jQ-7ODHs0XfDFimot8Q6Km6CylRIDTo,1069
|
|
16
|
-
anydi-0.24.3.dist-info/METADATA,sha256=NcsRJMFeKJAEWl6FERdpxutYYFfYYQhh5tGi39CvVKo,4371
|
|
17
|
-
anydi-0.24.3.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
18
|
-
anydi-0.24.3.dist-info/entry_points.txt,sha256=GmQblwzxFg42zva1HyBYJJ7TvrTIcSAGBHmyi3bvsi4,42
|
|
19
|
-
anydi-0.24.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|