anydi 0.24.3__tar.gz → 0.25.0a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anydi
3
- Version: 0.24.3
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 auto, Container
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 = auto) -> None:
103
+ def say_hello(message: str = dep()) -> None:
104
104
  print(message)
105
105
 
106
106
 
@@ -49,7 +49,7 @@ pip install anydi
49
49
  *app.py*
50
50
 
51
51
  ```python
52
- from anydi import auto, Container
52
+ from anydi import Container, dep
53
53
 
54
54
  container = Container()
55
55
 
@@ -60,7 +60,7 @@ def message() -> str:
60
60
 
61
61
 
62
62
  @container.inject
63
- def say_hello(message: str = auto) -> None:
63
+ def say_hello(message: str = dep()) -> None:
64
64
  print(message)
65
65
 
66
66
 
@@ -1,6 +1,6 @@
1
1
  """AnyDI public objects and functions."""
2
2
 
3
- from typing import Any, cast
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 = cast(Any, Marker())
17
+ auto = dep
18
18
 
19
19
 
20
20
  __all__ = [
@@ -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, is_marker
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: dict[Scope, list[Scope]] = {
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[type[Any], Provider] | None = None,
75
- modules: Sequence[Module | type[Module] | Callable[[Container], None]]
76
- | None = None,
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: dict[type[Any], Provider] = {}
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 | None] = ContextVar(
90
+ self._request_context_var: ContextVar[Optional[RequestContext]] = ContextVar(
90
91
  "request_context", default=None
91
92
  )
92
- self._override_instances: dict[type[Any], Any] = {}
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) -> dict[type[Any], Provider]:
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 | None:
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 get_typed_parameters(obj):
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 | type[Module] | Callable[[Container], None]
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 | Awaitable[T]] | None = None
636
- ) -> (
637
- Callable[[Callable[P, T | Awaitable[T]]], Callable[P, T | Awaitable[T]]]
638
- | Callable[P, T | Awaitable[T]]
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 | Awaitable[T]],
652
- ) -> Callable[P, T | Awaitable[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: types.ModuleType | str | Iterable[types.ModuleType | str],
697
+ packages: Union[
698
+ Union[types.ModuleType, str],
699
+ Iterable[Union[types.ModuleType, str]],
700
+ ],
694
701
  *,
695
- tags: Iterable[str] | None = None,
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 = get_typed_return_annotation(obj)
726
+ annotation = get_signature(obj).return_annotation
720
727
 
721
- if annotation is None:
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
- if get_origin(annotation) in (get_origin(Iterator), get_origin(AsyncIterator)):
727
- args = get_args(annotation)
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 args[0]
739
+ return annotation
730
740
  else:
731
741
  raise TypeError(
732
- f"Cannot use `{get_full_qualname(obj)}` resource type annotation "
742
+ f"Cannot use `{get_full_qualname(obj)}` generic type annotation "
733
743
  "without actual type."
734
744
  )
735
745
 
736
- return annotation
746
+ try:
747
+ return args[0]
748
+ except IndexError:
749
+ return annotation
737
750
 
738
- def _get_injected_params(self, obj: Callable[..., Any]) -> dict[str, 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 get_typed_parameters(obj):
750
- if not is_marker(parameter.default):
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)
@@ -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
- ) -> tuple[list[Any], dict[str, Any]]:
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
- ) -> tuple[list[Any], dict[str, Any]]:
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: dict[type[Any], Any] = {}
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: type[BaseException] | None,
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: type[BaseException] | None,
268
+ exc_type: Type[BaseException] | None,
269
269
  exc_val: BaseException | None,
270
270
  exc_tb: TracebackType | None,
271
271
  ) -> None:
@@ -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: tuple[type, ...], attrs: dict[str, Any]) -> Any:
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: list[tuple[str, ProviderDecoratorArgs]]
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 | type[Module] | Callable[[Container], None]
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
 
@@ -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 is_marker
23
- from ._utils import get_typed_parameters
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 | str | Iterable[ModuleType | str],
59
+ packages: Union[Union[ModuleType, str], Iterable[Union[ModuleType, str]]],
58
60
  *,
59
- tags: Iterable[str] | None = None,
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: list[Dependency] = []
71
+ dependencies: List[Dependency] = []
70
72
 
71
73
  if isinstance(packages, Iterable) and not isinstance(packages, str):
72
- scan_packages: Iterable[ModuleType | str] = packages
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 | str,
87
+ package: Union[ModuleType, str],
86
88
  *,
87
- tags: Iterable[str] | None = None,
88
- ) -> list[Dependency]:
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: list[Dependency] = []
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
- ) -> list[Dependency]:
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: list[Dependency] = []
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
- parameters = get_typed_parameters(member.__init__)
162
+ signature = get_signature(member.__init__)
161
163
  else:
162
- parameters = get_typed_parameters(member)
163
- for parameter in parameters:
164
- if is_marker(parameter.default):
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] | None
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] | None = None
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] | None = None,
204
- tags: Iterable[str] | None = None,
205
- ) -> Callable[[Callable[P, T]], Callable[P, T]] | Callable[P, T]:
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:
@@ -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, Self, TypeAlias
6
+ from typing_extensions import Annotated, Literal, Mapping, TypeAlias
9
7
 
10
- from ._utils import get_full_qualname, get_typed_parameters
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) -> list[inspect.Parameter]:
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 get_typed_parameters(self.obj)
59
+ return get_signature(self.obj).parameters
70
60
 
71
61
  @cached_property
72
62
  def is_class(self) -> bool:
@@ -0,0 +1,111 @@
1
+ """Shared AnyDI utils module."""
2
+
3
+ import builtins
4
+ import functools
5
+ import inspect
6
+ import sys
7
+ from typing import Any, Callable, Dict, Type, TypeVar
8
+
9
+ from typing_extensions import Annotated, ParamSpec, get_origin
10
+
11
+ try:
12
+ import anyio # noqa
13
+ except ImportError:
14
+ anyio = None # type: ignore[assignment]
15
+
16
+
17
+ T = TypeVar("T")
18
+ P = ParamSpec("P")
19
+
20
+
21
+ def get_full_qualname(obj: Any) -> str:
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
+ """
33
+ origin = get_origin(obj)
34
+ if origin is Annotated:
35
+ metadata = ", ".join(
36
+ [
37
+ f'"{arg}"' if isinstance(arg, str) else str(arg)
38
+ for arg in obj.__metadata__
39
+ ]
40
+ )
41
+ return f"Annotated[{get_full_qualname(obj.__args__[0])}, {metadata}]]"
42
+
43
+ qualname = getattr(obj, "__qualname__", None)
44
+ module_name = getattr(obj, "__module__", None)
45
+ if qualname is None:
46
+ qualname = type(obj).__qualname__
47
+
48
+ if module_name is None:
49
+ module_name = type(obj).__module__
50
+
51
+ if module_name == builtins.__name__:
52
+ return qualname
53
+ return f"{module_name}.{qualname}"
54
+
55
+
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
+ """
64
+ return tp.__module__ == builtins.__name__
65
+
66
+
67
+ @functools.lru_cache(maxsize=None)
68
+ def get_signature(obj: Callable[..., Any]) -> inspect.Signature:
69
+ """Get the signature of a callable object.
70
+
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.
74
+
75
+ Args:
76
+ obj: The callable object to inspect.
77
+
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)
85
+
86
+
87
+ async def run_async(
88
+ func: Callable[P, T],
89
+ /,
90
+ *args: P.args,
91
+ **kwargs: P.kwargs,
92
+ ) -> T:
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
+ if not anyio:
107
+ raise ImportError(
108
+ "`anyio` library is not currently installed. Please make sure to install "
109
+ "it first, or consider using `anydi[full]` instead."
110
+ )
111
+ return await anyio.to_thread.run_sync(functools.partial(func, *args, **kwargs))
@@ -0,0 +1,3 @@
1
+ from ._container import container
2
+
3
+ __all__ = ["container"]
@@ -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
@@ -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()
@@ -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, get_typed_parameters
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 get_typed_parameters(call):
50
+ for parameter in get_signature(call).parameters.values():
51
51
  _patch_route_parameter(call, parameter, container)
52
52
 
53
53
 
@@ -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[list[Any]]:
55
- unresolved: list[Any] = []
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: list[str],
64
- ) -> Callable[[], Iterator[tuple[str, Any]]]:
65
- def _iterator() -> Iterator[tuple[str, inspect.Parameter]]:
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[tuple[str, Any]]],
83
- _anydi_unresolved: list[str],
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[tuple[str, Any]]],
113
- _anydi_unresolved: list[str],
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
  [tool.poetry]
2
2
  name = "anydi"
3
- version = "0.24.3"
3
+ version = "0.25.0a0"
4
4
  description = "Dependency Injection library"
5
5
  authors = ["Anton Ruhlov <antonruhlov@gmail.com>"]
6
6
  license = "MIT"
@@ -50,8 +50,10 @@ mypy = "^1.10.0"
50
50
  ruff = "^0.4.3"
51
51
  pytest = "^8.1.0"
52
52
  pytest-cov = "^5.0.0"
53
- fastapi = "^0.95.1"
53
+ fastapi = "^0.100.0"
54
54
  httpx = "^0.26.0"
55
+ django = "^4.2"
56
+ django-ninja = "^1.1.0"
55
57
 
56
58
  [tool.poetry.plugins.pytest11]
57
59
  anydi = "anydi.ext.pytest_plugin"
@@ -60,7 +62,7 @@ anydi = "anydi.ext.pytest_plugin"
60
62
  line-length = 88
61
63
 
62
64
  [tool.ruff.lint]
63
- select = ["A", "B", "C", "E", "F", "I", "W", "TID252", "T20", "UP"]
65
+ select = ["A", "B", "C", "E", "F", "I", "W", "TID252", "T20"]
64
66
  ignore = ["A003", "B008", "B009", "B010", "D104", "D107"]
65
67
 
66
68
  [tool.ruff.lint.isort]
@@ -74,6 +76,12 @@ convention = "google"
74
76
  python_version = "3.10"
75
77
  strict = true
76
78
 
79
+ [[tool.mypy.overrides]]
80
+ module = [
81
+ "django.*",
82
+ ]
83
+ ignore_missing_imports = true
84
+
77
85
  [tool.pytest.ini_options]
78
86
  addopts = [
79
87
  "--strict-config",
@@ -1,111 +0,0 @@
1
- """Shared AnyDI utils module."""
2
-
3
- from __future__ import annotations
4
-
5
- import builtins
6
- import functools
7
- import inspect
8
- import sys
9
- from typing import Any, Callable, ForwardRef, TypeVar, cast
10
-
11
- from typing_extensions import Annotated, ParamSpec, get_origin
12
-
13
- try:
14
- import anyio # noqa
15
- except ImportError:
16
- anyio = None # type: ignore[assignment]
17
-
18
-
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
- T = TypeVar("T")
31
- P = ParamSpec("P")
32
-
33
-
34
- def get_full_qualname(obj: Any) -> str:
35
- """Get the fully qualified name of an object."""
36
- origin = get_origin(obj)
37
- if origin is Annotated:
38
- metadata = ", ".join(
39
- [
40
- f'"{arg}"' if isinstance(arg, str) else str(arg)
41
- for arg in obj.__metadata__
42
- ]
43
- )
44
- return f"Annotated[{get_full_qualname(obj.__args__[0])}, {metadata}]]"
45
-
46
- qualname = getattr(obj, "__qualname__", None)
47
- module_name = getattr(obj, "__module__", None)
48
- if qualname is None:
49
- qualname = type(obj).__qualname__
50
-
51
- if module_name is None:
52
- module_name = type(obj).__module__
53
-
54
- if module_name == builtins.__name__:
55
- return qualname
56
- return f"{module_name}.{qualname}"
57
-
58
-
59
- def is_builtin_type(tp: type[Any]) -> bool:
60
- """Check if the given type is a built-in type."""
61
- return tp.__module__ == builtins.__name__
62
-
63
-
64
- def make_forwardref(annotation: str, globalns: dict[str, Any]) -> Any:
65
- """Create a forward reference from a string annotation."""
66
- forward_ref = ForwardRef(annotation)
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
-
77
-
78
- def get_typed_return_annotation(obj: Callable[..., Any]) -> Any:
79
- """Get the typed return annotation of a callable object."""
80
- signature = inspect.signature(obj)
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)
86
-
87
-
88
- def get_typed_parameters(obj: Callable[..., Any]) -> list[inspect.Parameter]:
89
- """Get the typed parameters of a callable object."""
90
- globalns = getattr(obj, "__globals__", {})
91
- return [
92
- parameter.replace(
93
- annotation=get_typed_annotation(parameter.annotation, globalns)
94
- )
95
- for name, parameter in inspect.signature(obj).parameters.items()
96
- ]
97
-
98
-
99
- async def run_async(
100
- func: Callable[P, T],
101
- /,
102
- *args: P.args,
103
- **kwargs: P.kwargs,
104
- ) -> T:
105
- """Runs the given function asynchronously using the `anyio` library."""
106
- if not anyio:
107
- raise ImportError(
108
- "`anyio` library is not currently installed. Please make sure to install "
109
- "it first, or consider using `anydi[full]` instead."
110
- )
111
- return await anyio.to_thread.run_sync(functools.partial(func, *args, **kwargs))
File without changes
File without changes
File without changes
File without changes