anydi 0.31.0__py3-none-any.whl → 0.32.1__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 -8
- anydi/_container.py +122 -515
- anydi/_context.py +99 -188
- anydi/_injector.py +94 -0
- anydi/_module.py +4 -41
- anydi/_provider.py +187 -0
- anydi/_scanner.py +14 -68
- anydi/_types.py +4 -103
- anydi/_utils.py +22 -71
- anydi/ext/_utils.py +5 -11
- anydi/ext/django/_settings.py +1 -1
- anydi/ext/django/_utils.py +2 -2
- anydi/ext/django/apps.py +1 -1
- anydi/ext/django/middleware.py +11 -9
- anydi/ext/fastapi.py +4 -24
- anydi/ext/faststream.py +0 -7
- anydi/ext/pydantic_settings.py +2 -2
- anydi/ext/pytest_plugin.py +3 -2
- anydi/ext/starlette/middleware.py +2 -16
- {anydi-0.31.0.dist-info → anydi-0.32.1.dist-info}/METADATA +1 -1
- anydi-0.32.1.dist-info/RECORD +33 -0
- anydi-0.31.0.dist-info/RECORD +0 -31
- {anydi-0.31.0.dist-info → anydi-0.32.1.dist-info}/LICENSE +0 -0
- {anydi-0.31.0.dist-info → anydi-0.32.1.dist-info}/WHEEL +0 -0
- {anydi-0.31.0.dist-info → anydi-0.32.1.dist-info}/entry_points.txt +0 -0
anydi/_provider.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import uuid
|
|
5
|
+
from collections.abc import AsyncIterator, Iterator
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from typing_extensions import get_args, get_origin
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from types import NoneType
|
|
13
|
+
except ImportError:
|
|
14
|
+
NoneType = type(None) # type: ignore[misc]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
from ._types import Event, Scope
|
|
18
|
+
from ._utils import get_full_qualname, get_typed_annotation
|
|
19
|
+
|
|
20
|
+
_sentinel = object()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CallableKind(IntEnum):
|
|
24
|
+
CLASS = 1
|
|
25
|
+
FUNCTION = 2
|
|
26
|
+
COROUTINE = 3
|
|
27
|
+
GENERATOR = 4
|
|
28
|
+
ASYNC_GENERATOR = 5
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Provider:
|
|
32
|
+
__slots__ = (
|
|
33
|
+
"_call",
|
|
34
|
+
"_call_module",
|
|
35
|
+
"_call_globals",
|
|
36
|
+
"_scope",
|
|
37
|
+
"_qualname",
|
|
38
|
+
"_kind",
|
|
39
|
+
"_interface",
|
|
40
|
+
"_parameters",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self, call: Callable[..., Any], *, scope: Scope, interface: Any = _sentinel
|
|
45
|
+
) -> None:
|
|
46
|
+
self._call = call
|
|
47
|
+
self._call_module = getattr(call, "__module__", None)
|
|
48
|
+
self._call_globals = getattr(call, "__globals__", {})
|
|
49
|
+
self._scope = scope
|
|
50
|
+
self._qualname = get_full_qualname(call)
|
|
51
|
+
|
|
52
|
+
# Detect the kind of callable provider
|
|
53
|
+
self._detect_kind()
|
|
54
|
+
|
|
55
|
+
# Validate the scope of the provider
|
|
56
|
+
self._validate_scope()
|
|
57
|
+
|
|
58
|
+
# Get the signature
|
|
59
|
+
signature = inspect.signature(call)
|
|
60
|
+
|
|
61
|
+
# Detect the interface
|
|
62
|
+
self._detect_interface(interface, signature)
|
|
63
|
+
|
|
64
|
+
# Detect the parameters
|
|
65
|
+
self._detect_parameters(signature)
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
return self._qualname
|
|
69
|
+
|
|
70
|
+
def __eq__(self, other: object) -> bool:
|
|
71
|
+
if not isinstance(other, Provider):
|
|
72
|
+
return NotImplemented
|
|
73
|
+
return (
|
|
74
|
+
self._call == other._call
|
|
75
|
+
and self._scope == other._scope
|
|
76
|
+
and self._interface == other._interface
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def call(self) -> Callable[..., Any]:
|
|
81
|
+
return self._call
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def kind(self) -> CallableKind:
|
|
85
|
+
return self._kind
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def scope(self) -> Scope:
|
|
89
|
+
return self._scope
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def interface(self) -> Any:
|
|
93
|
+
return self._interface
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def parameters(self) -> list[inspect.Parameter]:
|
|
97
|
+
return self._parameters
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def is_resource(self) -> bool:
|
|
101
|
+
"""Check if the provider is a resource."""
|
|
102
|
+
return self._kind in {
|
|
103
|
+
CallableKind.GENERATOR,
|
|
104
|
+
CallableKind.ASYNC_GENERATOR,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
def _validate_scope(self) -> None:
|
|
108
|
+
"""Validate the scope of the provider."""
|
|
109
|
+
if self.is_resource and self.scope == "transient":
|
|
110
|
+
raise TypeError(
|
|
111
|
+
f"The resource provider `{self}` is attempting to register "
|
|
112
|
+
"with a transient scope, which is not allowed."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _detect_kind(self) -> None:
|
|
116
|
+
"""Detect the kind of callable provider."""
|
|
117
|
+
if inspect.isclass(self.call):
|
|
118
|
+
self._kind = CallableKind.CLASS
|
|
119
|
+
elif inspect.iscoroutinefunction(self.call):
|
|
120
|
+
self._kind = CallableKind.COROUTINE
|
|
121
|
+
elif inspect.isasyncgenfunction(self.call):
|
|
122
|
+
self._kind = CallableKind.ASYNC_GENERATOR
|
|
123
|
+
elif inspect.isgeneratorfunction(self.call):
|
|
124
|
+
self._kind = CallableKind.GENERATOR
|
|
125
|
+
elif inspect.isfunction(self.call) or inspect.ismethod(self.call):
|
|
126
|
+
self._kind = CallableKind.FUNCTION
|
|
127
|
+
else:
|
|
128
|
+
raise TypeError(
|
|
129
|
+
f"The provider `{self.call}` is invalid because it is not a callable "
|
|
130
|
+
"object. Only callable providers are allowed."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _detect_interface(self, interface: Any, signature: inspect.Signature) -> None:
|
|
134
|
+
"""Detect the interface of callable provider."""
|
|
135
|
+
# If the callable is a class, return the class itself
|
|
136
|
+
if self._kind == CallableKind.CLASS:
|
|
137
|
+
self._interface = self._call
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
if interface is _sentinel:
|
|
141
|
+
interface = self._resolve_interface(interface, signature)
|
|
142
|
+
|
|
143
|
+
# If the callable is an iterator, return the actual type
|
|
144
|
+
iterator_types = {Iterator, AsyncIterator}
|
|
145
|
+
if interface in iterator_types or get_origin(interface) in iterator_types:
|
|
146
|
+
if args := get_args(interface):
|
|
147
|
+
interface = args[0]
|
|
148
|
+
# If the callable is a generator, return the resource type
|
|
149
|
+
if interface is NoneType or interface is None:
|
|
150
|
+
self._interface = type(f"Event_{uuid.uuid4().hex}", (Event,), {})
|
|
151
|
+
return
|
|
152
|
+
else:
|
|
153
|
+
raise TypeError(
|
|
154
|
+
f"Cannot use `{self}` resource type annotation "
|
|
155
|
+
"without actual type argument."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# None interface is not allowed
|
|
159
|
+
if interface in {None, NoneType}:
|
|
160
|
+
raise TypeError(f"Missing `{self}` provider return annotation.")
|
|
161
|
+
|
|
162
|
+
# Set the interface
|
|
163
|
+
self._interface = interface
|
|
164
|
+
|
|
165
|
+
def _resolve_interface(self, interface: Any, signature: inspect.Signature) -> Any:
|
|
166
|
+
"""Resolve the interface of the callable provider."""
|
|
167
|
+
interface = signature.return_annotation
|
|
168
|
+
if interface is inspect.Signature.empty:
|
|
169
|
+
return None
|
|
170
|
+
return get_typed_annotation(
|
|
171
|
+
interface,
|
|
172
|
+
self._call_globals,
|
|
173
|
+
module=self._call_module,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _detect_parameters(self, signature: inspect.Signature) -> None:
|
|
177
|
+
"""Detect the parameters of the callable provider."""
|
|
178
|
+
self._parameters = [
|
|
179
|
+
parameter.replace(
|
|
180
|
+
annotation=get_typed_annotation(
|
|
181
|
+
parameter.annotation,
|
|
182
|
+
self._call_globals,
|
|
183
|
+
module=self._call_module,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
for parameter in signature.parameters.values()
|
|
187
|
+
]
|
anydi/_scanner.py
CHANGED
|
@@ -3,13 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
import importlib
|
|
4
4
|
import inspect
|
|
5
5
|
import pkgutil
|
|
6
|
+
from collections.abc import Iterable
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from types import ModuleType
|
|
8
9
|
from typing import (
|
|
9
10
|
TYPE_CHECKING,
|
|
10
11
|
Any,
|
|
11
12
|
Callable,
|
|
12
|
-
Iterable,
|
|
13
13
|
TypeVar,
|
|
14
14
|
Union,
|
|
15
15
|
cast,
|
|
@@ -32,13 +32,6 @@ P = ParamSpec("P")
|
|
|
32
32
|
|
|
33
33
|
@dataclass(frozen=True)
|
|
34
34
|
class Dependency:
|
|
35
|
-
"""Represents a scanned dependency.
|
|
36
|
-
|
|
37
|
-
Attributes:
|
|
38
|
-
member: The member object that represents the dependency.
|
|
39
|
-
module: The module where the dependency is defined.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
35
|
member: Any
|
|
43
36
|
module: ModuleType
|
|
44
37
|
|
|
@@ -58,14 +51,7 @@ class Scanner:
|
|
|
58
51
|
*,
|
|
59
52
|
tags: Iterable[str] | None = None,
|
|
60
53
|
) -> None:
|
|
61
|
-
"""Scan packages or modules for decorated members and inject dependencies.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
packages: A single package or module to scan,
|
|
65
|
-
or an iterable of packages or modules to scan.
|
|
66
|
-
tags: Optional list of tags to filter the scanned members. Only members
|
|
67
|
-
with at least one matching tag will be scanned. Defaults to None.
|
|
68
|
-
"""
|
|
54
|
+
"""Scan packages or modules for decorated members and inject dependencies."""
|
|
69
55
|
dependencies: list[Dependency] = []
|
|
70
56
|
|
|
71
57
|
if isinstance(packages, Iterable) and not isinstance(packages, str):
|
|
@@ -86,16 +72,7 @@ class Scanner:
|
|
|
86
72
|
*,
|
|
87
73
|
tags: Iterable[str] | None = None,
|
|
88
74
|
) -> list[Dependency]:
|
|
89
|
-
"""Scan a package or module for decorated members.
|
|
90
|
-
|
|
91
|
-
Args:
|
|
92
|
-
package: The package or module to scan.
|
|
93
|
-
tags: Optional list of tags to filter the scanned members. Only members
|
|
94
|
-
with at least one matching tag will be scanned. Defaults to None.
|
|
95
|
-
|
|
96
|
-
Returns:
|
|
97
|
-
A list of scanned dependencies.
|
|
98
|
-
"""
|
|
75
|
+
"""Scan a package or module for decorated members."""
|
|
99
76
|
tags = tags or []
|
|
100
77
|
if isinstance(package, str):
|
|
101
78
|
package = importlib.import_module(package)
|
|
@@ -118,16 +95,7 @@ class Scanner:
|
|
|
118
95
|
def _scan_module(
|
|
119
96
|
self, module: ModuleType, *, tags: Iterable[str]
|
|
120
97
|
) -> list[Dependency]:
|
|
121
|
-
"""Scan a module for decorated members.
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
module: The module to scan.
|
|
125
|
-
tags: List of tags to filter the scanned members. Only members with at
|
|
126
|
-
least one matching tag will be scanned.
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
A list of scanned dependencies.
|
|
130
|
-
"""
|
|
98
|
+
"""Scan a module for decorated members."""
|
|
131
99
|
dependencies: list[Dependency] = []
|
|
132
100
|
|
|
133
101
|
for _, member in inspect.getmembers(module):
|
|
@@ -156,11 +124,7 @@ class Scanner:
|
|
|
156
124
|
continue
|
|
157
125
|
|
|
158
126
|
# Get by Marker
|
|
159
|
-
|
|
160
|
-
parameters = get_typed_parameters(member.__init__)
|
|
161
|
-
else:
|
|
162
|
-
parameters = get_typed_parameters(member)
|
|
163
|
-
for parameter in parameters:
|
|
127
|
+
for parameter in get_typed_parameters(member):
|
|
164
128
|
if is_marker(parameter.default):
|
|
165
129
|
dependencies.append(
|
|
166
130
|
self._create_dependency(member=member, module=module)
|
|
@@ -170,15 +134,7 @@ class Scanner:
|
|
|
170
134
|
return dependencies
|
|
171
135
|
|
|
172
136
|
def _create_dependency(self, member: Any, module: ModuleType) -> Dependency:
|
|
173
|
-
"""Create a `Dependency` object from the scanned member and module.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
member: The scanned member.
|
|
177
|
-
module: The module containing the scanned member.
|
|
178
|
-
|
|
179
|
-
Returns:
|
|
180
|
-
A `ScannedDependency` object.
|
|
181
|
-
"""
|
|
137
|
+
"""Create a `Dependency` object from the scanned member and module."""
|
|
182
138
|
if hasattr(member, "__wrapped__"):
|
|
183
139
|
member = member.__wrapped__
|
|
184
140
|
return Dependency(member=member, module=module)
|
|
@@ -190,7 +146,7 @@ class InjectDecoratorArgs(NamedTuple):
|
|
|
190
146
|
|
|
191
147
|
|
|
192
148
|
@overload
|
|
193
|
-
def injectable(
|
|
149
|
+
def injectable(func: Callable[P, T]) -> Callable[P, T]: ...
|
|
194
150
|
|
|
195
151
|
|
|
196
152
|
@overload
|
|
@@ -200,26 +156,16 @@ def injectable(
|
|
|
200
156
|
|
|
201
157
|
|
|
202
158
|
def injectable(
|
|
203
|
-
|
|
159
|
+
func: Callable[P, T] | None = None,
|
|
204
160
|
tags: Iterable[str] | None = None,
|
|
205
161
|
) -> Callable[[Callable[P, T]], Callable[P, T]] | Callable[P, T]:
|
|
206
|
-
"""Decorator for marking a function or method as requiring dependency injection.
|
|
207
|
-
|
|
208
|
-
Args:
|
|
209
|
-
obj: The target function or method to be decorated.
|
|
210
|
-
tags: Optional tags to associate with the injection point.
|
|
211
|
-
|
|
212
|
-
Returns:
|
|
213
|
-
If `obj` is provided, returns the decorated target function or method.
|
|
214
|
-
If `obj` is not provided, returns a decorator that can be used to mark
|
|
215
|
-
a function or method as requiring dependency injection.
|
|
216
|
-
"""
|
|
162
|
+
"""Decorator for marking a function or method as requiring dependency injection."""
|
|
217
163
|
|
|
218
|
-
def decorator(
|
|
219
|
-
setattr(
|
|
220
|
-
return
|
|
164
|
+
def decorator(inner: Callable[P, T]) -> Callable[P, T]:
|
|
165
|
+
setattr(inner, "__injectable__", InjectDecoratorArgs(wrapped=True, tags=tags))
|
|
166
|
+
return inner
|
|
221
167
|
|
|
222
|
-
if
|
|
168
|
+
if func is None:
|
|
223
169
|
return decorator
|
|
224
170
|
|
|
225
|
-
return decorator(
|
|
171
|
+
return decorator(func)
|
anydi/_types.py
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
from
|
|
5
|
-
from functools import cached_property
|
|
6
|
-
from typing import Any, Callable, Type, TypeVar, Union
|
|
4
|
+
from typing import Annotated, Any, TypeVar, Union
|
|
7
5
|
|
|
8
|
-
from typing_extensions import
|
|
9
|
-
|
|
10
|
-
from ._utils import get_full_qualname, get_typed_parameters
|
|
6
|
+
from typing_extensions import Literal, Self, TypeAlias
|
|
11
7
|
|
|
12
8
|
Scope = Literal["transient", "singleton", "request"]
|
|
13
9
|
|
|
14
10
|
T = TypeVar("T")
|
|
15
|
-
AnyInterface: TypeAlias = Union[
|
|
16
|
-
Interface: TypeAlias =
|
|
11
|
+
AnyInterface: TypeAlias = Union[type[Any], Annotated[Any, ...]]
|
|
12
|
+
Interface: TypeAlias = type[T]
|
|
17
13
|
|
|
18
14
|
|
|
19
15
|
class Marker:
|
|
@@ -39,98 +35,3 @@ class Event:
|
|
|
39
35
|
def is_event_type(obj: Any) -> bool:
|
|
40
36
|
"""Checks if an object is an event type."""
|
|
41
37
|
return inspect.isclass(obj) and issubclass(obj, Event)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@dataclass(frozen=True)
|
|
45
|
-
class Provider:
|
|
46
|
-
"""Represents a provider object.
|
|
47
|
-
|
|
48
|
-
Attributes:
|
|
49
|
-
obj: The callable object that serves as the provider.
|
|
50
|
-
scope: The scope of the provider.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
obj: Callable[..., Any]
|
|
54
|
-
scope: Scope
|
|
55
|
-
|
|
56
|
-
def __str__(self) -> str:
|
|
57
|
-
"""Returns a string representation of the provider.
|
|
58
|
-
|
|
59
|
-
Returns:
|
|
60
|
-
The string representation of the provider.
|
|
61
|
-
"""
|
|
62
|
-
return self.name
|
|
63
|
-
|
|
64
|
-
@cached_property
|
|
65
|
-
def name(self) -> str:
|
|
66
|
-
"""Returns the full qualified name of the provider object.
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
The full qualified name of the provider object.
|
|
70
|
-
"""
|
|
71
|
-
return get_full_qualname(self.obj)
|
|
72
|
-
|
|
73
|
-
@cached_property
|
|
74
|
-
def parameters(self) -> list[inspect.Parameter]:
|
|
75
|
-
"""Returns the parameters of the provider as a mapping.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
The parameters of the provider.
|
|
79
|
-
"""
|
|
80
|
-
return get_typed_parameters(self.obj)
|
|
81
|
-
|
|
82
|
-
@cached_property
|
|
83
|
-
def is_class(self) -> bool:
|
|
84
|
-
"""Checks if the provider object is a class.
|
|
85
|
-
|
|
86
|
-
Returns:
|
|
87
|
-
True if the provider object is a class, False otherwise.
|
|
88
|
-
"""
|
|
89
|
-
return inspect.isclass(self.obj)
|
|
90
|
-
|
|
91
|
-
@cached_property
|
|
92
|
-
def is_function(self) -> bool:
|
|
93
|
-
"""Checks if the provider object is a function.
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
True if the provider object is a function, False otherwise.
|
|
97
|
-
"""
|
|
98
|
-
return (inspect.isfunction(self.obj) or inspect.ismethod(self.obj)) and not (
|
|
99
|
-
self.is_resource
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
@cached_property
|
|
103
|
-
def is_coroutine(self) -> bool:
|
|
104
|
-
"""Checks if the provider object is a coroutine function.
|
|
105
|
-
|
|
106
|
-
Returns:
|
|
107
|
-
True if the provider object is a coroutine function, False otherwise.
|
|
108
|
-
"""
|
|
109
|
-
return inspect.iscoroutinefunction(self.obj)
|
|
110
|
-
|
|
111
|
-
@cached_property
|
|
112
|
-
def is_generator(self) -> bool:
|
|
113
|
-
"""Checks if the provider object is a generator function.
|
|
114
|
-
|
|
115
|
-
Returns:
|
|
116
|
-
True if the provider object is a resource, False otherwise.
|
|
117
|
-
"""
|
|
118
|
-
return inspect.isgeneratorfunction(self.obj)
|
|
119
|
-
|
|
120
|
-
@cached_property
|
|
121
|
-
def is_async_generator(self) -> bool:
|
|
122
|
-
"""Checks if the provider object is an async generator function.
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
True if the provider object is an async resource, False otherwise.
|
|
126
|
-
"""
|
|
127
|
-
return inspect.isasyncgenfunction(self.obj)
|
|
128
|
-
|
|
129
|
-
@property
|
|
130
|
-
def is_resource(self) -> bool:
|
|
131
|
-
"""Checks if the provider object is a sync or async generator function.
|
|
132
|
-
|
|
133
|
-
Returns:
|
|
134
|
-
True if the provider object is a resource, False otherwise.
|
|
135
|
-
"""
|
|
136
|
-
return self.is_generator or self.is_async_generator
|
anydi/_utils.py
CHANGED
|
@@ -6,8 +6,9 @@ import builtins
|
|
|
6
6
|
import functools
|
|
7
7
|
import importlib
|
|
8
8
|
import inspect
|
|
9
|
+
import re
|
|
9
10
|
import sys
|
|
10
|
-
from typing import Any,
|
|
11
|
+
from typing import Any, Callable, ForwardRef, TypeVar
|
|
11
12
|
|
|
12
13
|
from typing_extensions import ParamSpec, get_args, get_origin
|
|
13
14
|
|
|
@@ -23,28 +24,23 @@ P = ParamSpec("P")
|
|
|
23
24
|
|
|
24
25
|
def get_full_qualname(obj: Any) -> str:
|
|
25
26
|
"""Get the fully qualified name of an object."""
|
|
26
|
-
qualname
|
|
27
|
-
module = getattr(obj, "__module__",
|
|
28
|
-
|
|
29
|
-
if qualname is None:
|
|
30
|
-
qualname = type(obj).__qualname__
|
|
31
|
-
|
|
32
|
-
if module is None:
|
|
33
|
-
module = type(obj).__module__
|
|
34
|
-
|
|
35
|
-
if module == builtins.__name__:
|
|
36
|
-
return qualname
|
|
27
|
+
# Get module and qualname with defaults to handle non-types directly
|
|
28
|
+
module = getattr(obj, "__module__", type(obj).__module__)
|
|
29
|
+
qualname = getattr(obj, "__qualname__", type(obj).__qualname__)
|
|
37
30
|
|
|
38
31
|
origin = get_origin(obj)
|
|
39
|
-
|
|
32
|
+
# If origin exists, handle generics recursively
|
|
40
33
|
if origin:
|
|
41
|
-
args = ", ".join(
|
|
42
|
-
get_full_qualname(arg) if not isinstance(arg, str) else f'"{arg}"'
|
|
43
|
-
for arg in get_args(obj)
|
|
44
|
-
)
|
|
34
|
+
args = ", ".join(get_full_qualname(arg) for arg in get_args(obj))
|
|
45
35
|
return f"{get_full_qualname(origin)}[{args}]"
|
|
46
36
|
|
|
47
|
-
|
|
37
|
+
# Substitute standard library prefixes for clarity
|
|
38
|
+
full_qualname = f"{module}.{qualname}"
|
|
39
|
+
return re.sub(
|
|
40
|
+
r"\b(builtins|typing|typing_extensions|collections\.abc|types)\.",
|
|
41
|
+
"",
|
|
42
|
+
full_qualname,
|
|
43
|
+
)
|
|
48
44
|
|
|
49
45
|
|
|
50
46
|
def is_builtin_type(tp: type[Any]) -> bool:
|
|
@@ -52,69 +48,33 @@ def is_builtin_type(tp: type[Any]) -> bool:
|
|
|
52
48
|
return tp.__module__ == builtins.__name__
|
|
53
49
|
|
|
54
50
|
|
|
55
|
-
def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
|
|
56
|
-
return type_._evaluate(globalns, localns, recursive_guard=frozenset())
|
|
57
|
-
|
|
58
|
-
|
|
59
51
|
def get_typed_annotation(
|
|
60
|
-
annotation: Any,
|
|
61
|
-
globalns: dict[str, Any],
|
|
62
|
-
module: Any = None,
|
|
63
|
-
is_class: bool = False,
|
|
52
|
+
annotation: Any, globalns: dict[str, Any], module: Any = None
|
|
64
53
|
) -> Any:
|
|
65
|
-
"""Get the typed annotation of a
|
|
54
|
+
"""Get the typed annotation of a callable object."""
|
|
66
55
|
if isinstance(annotation, str):
|
|
67
|
-
if sys.version_info >= (3, 10
|
|
68
|
-
|
|
69
|
-
elif sys.version_info >= (3, 10, 0):
|
|
70
|
-
annotation = ForwardRef(annotation, module=module)
|
|
56
|
+
if sys.version_info >= (3, 10):
|
|
57
|
+
ref = ForwardRef(annotation, module=module)
|
|
71
58
|
else:
|
|
72
|
-
|
|
73
|
-
annotation =
|
|
59
|
+
ref = ForwardRef(annotation)
|
|
60
|
+
annotation = ref._evaluate(globalns, globalns, recursive_guard=frozenset()) # noqa
|
|
74
61
|
return annotation
|
|
75
62
|
|
|
76
63
|
|
|
77
|
-
def get_typed_return_annotation(obj: Callable[..., Any]) -> Any:
|
|
78
|
-
"""Get the typed return annotation of a callable object."""
|
|
79
|
-
signature = inspect.signature(obj)
|
|
80
|
-
annotation = signature.return_annotation
|
|
81
|
-
if annotation is inspect.Signature.empty:
|
|
82
|
-
return None
|
|
83
|
-
globalns = getattr(obj, "__globals__", {})
|
|
84
|
-
module = getattr(obj, "__module__", None)
|
|
85
|
-
is_class = inspect.isclass(obj)
|
|
86
|
-
return get_typed_annotation(annotation, globalns, module=module, is_class=is_class)
|
|
87
|
-
|
|
88
|
-
|
|
89
64
|
def get_typed_parameters(obj: Callable[..., Any]) -> list[inspect.Parameter]:
|
|
90
65
|
"""Get the typed parameters of a callable object."""
|
|
91
66
|
globalns = getattr(obj, "__globals__", {})
|
|
92
67
|
module = getattr(obj, "__module__", None)
|
|
93
|
-
is_class = inspect.isclass(obj)
|
|
94
68
|
return [
|
|
95
69
|
parameter.replace(
|
|
96
70
|
annotation=get_typed_annotation(
|
|
97
|
-
parameter.annotation,
|
|
98
|
-
globalns,
|
|
99
|
-
module=module,
|
|
100
|
-
is_class=is_class,
|
|
71
|
+
parameter.annotation, globalns, module=module
|
|
101
72
|
)
|
|
102
73
|
)
|
|
103
|
-
for
|
|
74
|
+
for parameter in inspect.signature(obj).parameters.values()
|
|
104
75
|
]
|
|
105
76
|
|
|
106
77
|
|
|
107
|
-
_resource_origins = (
|
|
108
|
-
get_origin(Iterator),
|
|
109
|
-
get_origin(AsyncIterator),
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def has_resource_origin(origin: Any) -> bool:
|
|
114
|
-
"""Check if the given origin is a resource origin."""
|
|
115
|
-
return origin in _resource_origins
|
|
116
|
-
|
|
117
|
-
|
|
118
78
|
async def run_async(
|
|
119
79
|
func: Callable[P, T],
|
|
120
80
|
/,
|
|
@@ -133,15 +93,6 @@ async def run_async(
|
|
|
133
93
|
def import_string(dotted_path: str) -> Any:
|
|
134
94
|
"""
|
|
135
95
|
Import a module or a specific attribute from a module using its dotted string path.
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
dotted_path: The dotted path to the object to import.
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
object: The imported module or attribute/class/function.
|
|
142
|
-
|
|
143
|
-
Raises:
|
|
144
|
-
ImportError: If the import fails.
|
|
145
96
|
"""
|
|
146
97
|
try:
|
|
147
98
|
module_path, _, attribute_name = dotted_path.rpartition(".")
|
anydi/ext/_utils.py
CHANGED
|
@@ -4,9 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
6
|
import logging
|
|
7
|
-
from typing import Any, Callable
|
|
7
|
+
from typing import Annotated, Any, Callable
|
|
8
8
|
|
|
9
|
-
from typing_extensions import
|
|
9
|
+
from typing_extensions import get_args, get_origin
|
|
10
10
|
|
|
11
11
|
from anydi import Container
|
|
12
12
|
from anydi._utils import get_full_qualname
|
|
@@ -32,7 +32,7 @@ def patch_annotated_parameter(parameter: inspect.Parameter) -> inspect.Parameter
|
|
|
32
32
|
"""Patch an annotated parameter to resolve the default value."""
|
|
33
33
|
if not (
|
|
34
34
|
get_origin(parameter.annotation) is Annotated
|
|
35
|
-
and parameter.default is
|
|
35
|
+
and parameter.default is inspect.Parameter.empty
|
|
36
36
|
):
|
|
37
37
|
return parameter
|
|
38
38
|
|
|
@@ -63,13 +63,7 @@ def patch_annotated_parameter(parameter: inspect.Parameter) -> inspect.Parameter
|
|
|
63
63
|
def patch_call_parameter(
|
|
64
64
|
call: Callable[..., Any], parameter: inspect.Parameter, container: Container
|
|
65
65
|
) -> None:
|
|
66
|
-
"""Patch a parameter to inject dependencies using AnyDI.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
call: The call function.
|
|
70
|
-
parameter: The parameter to patch.
|
|
71
|
-
container: The AnyDI container.
|
|
72
|
-
"""
|
|
66
|
+
"""Patch a parameter to inject dependencies using AnyDI."""
|
|
73
67
|
parameter = patch_annotated_parameter(parameter)
|
|
74
68
|
|
|
75
69
|
if not isinstance(parameter.default, HasInterface):
|
|
@@ -84,6 +78,6 @@ def patch_call_parameter(
|
|
|
84
78
|
"first call because it is running in non-strict mode."
|
|
85
79
|
)
|
|
86
80
|
else:
|
|
87
|
-
container._validate_injected_parameter(call, parameter) # noqa
|
|
81
|
+
container._injector._validate_injected_parameter(call, parameter) # noqa
|
|
88
82
|
|
|
89
83
|
parameter.default.interface = parameter.annotation
|
anydi/ext/django/_settings.py
CHANGED
anydi/ext/django/_utils.py
CHANGED
|
@@ -2,14 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Iterator
|
|
4
4
|
from functools import wraps
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Annotated, Any
|
|
6
6
|
|
|
7
7
|
from django.conf import settings
|
|
8
8
|
from django.core.cache import BaseCache, caches
|
|
9
9
|
from django.db import connections
|
|
10
10
|
from django.db.backends.base.base import BaseDatabaseWrapper
|
|
11
11
|
from django.urls import URLPattern, URLResolver, get_resolver
|
|
12
|
-
from typing_extensions import
|
|
12
|
+
from typing_extensions import get_origin
|
|
13
13
|
|
|
14
14
|
from anydi import Container
|
|
15
15
|
|
anydi/ext/django/apps.py
CHANGED
|
@@ -17,7 +17,7 @@ from ._utils import inject_urlpatterns, register_components, register_settings
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class ContainerConfig(AppConfig):
|
|
20
|
+
class ContainerConfig(AppConfig):
|
|
21
21
|
name = "anydi.ext.django"
|
|
22
22
|
label = "anydi_django"
|
|
23
23
|
|