anydi 0.67.2__py3-none-any.whl → 0.69.0__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/_cli.py +80 -0
- anydi/_container.py +697 -263
- anydi/_context.py +14 -14
- anydi/_decorators.py +115 -8
- anydi/_graph.py +217 -0
- anydi/_injector.py +12 -10
- anydi/_marker.py +11 -13
- anydi/_provider.py +46 -8
- anydi/_resolver.py +205 -159
- anydi/_scanner.py +46 -7
- anydi/ext/fastapi.py +1 -1
- anydi/ext/faststream.py +1 -1
- anydi/ext/pydantic_settings.py +3 -3
- anydi/ext/pytest_plugin.py +125 -380
- anydi/ext/typer.py +4 -4
- {anydi-0.67.2.dist-info → anydi-0.69.0.dist-info}/METADATA +1 -1
- anydi-0.69.0.dist-info/RECORD +29 -0
- {anydi-0.67.2.dist-info → anydi-0.69.0.dist-info}/entry_points.txt +3 -0
- anydi-0.67.2.dist-info/RECORD +0 -27
- {anydi-0.67.2.dist-info → anydi-0.69.0.dist-info}/WHEEL +0 -0
anydi/_context.py
CHANGED
|
@@ -15,22 +15,22 @@ from ._types import NOT_SET
|
|
|
15
15
|
class InstanceContext:
|
|
16
16
|
"""A context to store instances."""
|
|
17
17
|
|
|
18
|
-
__slots__ = ("
|
|
18
|
+
__slots__ = ("_items", "_stack", "_async_stack", "_lock", "_async_lock")
|
|
19
19
|
|
|
20
20
|
def __init__(self) -> None:
|
|
21
|
-
self.
|
|
21
|
+
self._items: dict[Any, Any] = {}
|
|
22
22
|
self._stack: contextlib.ExitStack | None = None
|
|
23
23
|
self._async_stack: contextlib.AsyncExitStack | None = None
|
|
24
24
|
self._lock: threading.RLock | None = None
|
|
25
25
|
self._async_lock: AsyncRLock | None = None
|
|
26
26
|
|
|
27
|
-
def get(self,
|
|
27
|
+
def get(self, key: Any, default: Any = NOT_SET) -> Any:
|
|
28
28
|
"""Get an instance from the context."""
|
|
29
|
-
return self.
|
|
29
|
+
return self._items.get(key, default)
|
|
30
30
|
|
|
31
|
-
def set(self,
|
|
31
|
+
def set(self, key: Any, value: Any) -> None:
|
|
32
32
|
"""Set an instance in the context."""
|
|
33
|
-
self.
|
|
33
|
+
self._items[key] = value
|
|
34
34
|
|
|
35
35
|
def enter(self, cm: contextlib.AbstractContextManager[Any]) -> Any:
|
|
36
36
|
"""Enter the context."""
|
|
@@ -44,17 +44,17 @@ class InstanceContext:
|
|
|
44
44
|
self._async_stack = contextlib.AsyncExitStack()
|
|
45
45
|
return await self._async_stack.enter_async_context(cm)
|
|
46
46
|
|
|
47
|
-
def __setitem__(self,
|
|
48
|
-
self.
|
|
47
|
+
def __setitem__(self, key: Any, value: Any) -> None:
|
|
48
|
+
self._items[key] = value
|
|
49
49
|
|
|
50
|
-
def __getitem__(self,
|
|
51
|
-
return self.
|
|
50
|
+
def __getitem__(self, key: Any) -> Any:
|
|
51
|
+
return self._items[key]
|
|
52
52
|
|
|
53
|
-
def __contains__(self,
|
|
54
|
-
return
|
|
53
|
+
def __contains__(self, key: Any) -> bool:
|
|
54
|
+
return key in self._items
|
|
55
55
|
|
|
56
|
-
def __delitem__(self,
|
|
57
|
-
self.
|
|
56
|
+
def __delitem__(self, key: Any) -> None:
|
|
57
|
+
self._items.pop(key, None)
|
|
58
58
|
|
|
59
59
|
def __enter__(self) -> Self:
|
|
60
60
|
"""Enter the context."""
|
anydi/_decorators.py
CHANGED
|
@@ -11,11 +11,13 @@ from typing import (
|
|
|
11
11
|
overload,
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
+
from typing_extensions import NotRequired
|
|
15
|
+
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
17
|
from ._module import Module
|
|
16
18
|
|
|
17
19
|
|
|
18
|
-
from ._types import Scope
|
|
20
|
+
from ._types import NOT_SET, Scope
|
|
19
21
|
|
|
20
22
|
T = TypeVar("T")
|
|
21
23
|
P = ParamSpec("P")
|
|
@@ -27,23 +29,128 @@ ModuleT = TypeVar("ModuleT", bound="Module")
|
|
|
27
29
|
class ProvidedMetadata(TypedDict):
|
|
28
30
|
"""Metadata for classes marked as provided by AnyDI."""
|
|
29
31
|
|
|
32
|
+
dependency_type: NotRequired[Any]
|
|
30
33
|
scope: Scope
|
|
34
|
+
from_context: NotRequired[bool]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
def provided(
|
|
39
|
+
dependency_type: Any, /, *, scope: Scope, from_context: bool = False
|
|
40
|
+
) -> Callable[[ClassT], ClassT]: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@overload
|
|
44
|
+
def provided(
|
|
45
|
+
*, scope: Scope, from_context: bool = False
|
|
46
|
+
) -> Callable[[ClassT], ClassT]: ...
|
|
31
47
|
|
|
32
48
|
|
|
33
|
-
def provided(
|
|
49
|
+
def provided(
|
|
50
|
+
dependency_type: Any = NOT_SET, /, *, scope: Scope, from_context: bool = False
|
|
51
|
+
) -> Callable[[ClassT], ClassT]:
|
|
34
52
|
"""Decorator for marking a class as provided by AnyDI with a specific scope."""
|
|
35
53
|
|
|
36
54
|
def decorator(cls: ClassT) -> ClassT:
|
|
37
|
-
|
|
55
|
+
metadata: ProvidedMetadata = {"scope": scope}
|
|
56
|
+
if dependency_type is not NOT_SET:
|
|
57
|
+
metadata["dependency_type"] = dependency_type
|
|
58
|
+
if from_context:
|
|
59
|
+
metadata["from_context"] = from_context
|
|
60
|
+
cls.__provided__ = metadata # type: ignore[attr-defined]
|
|
38
61
|
return cls
|
|
39
62
|
|
|
40
63
|
return decorator
|
|
41
64
|
|
|
42
65
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
@overload
|
|
67
|
+
def singleton(cls: ClassT, /) -> ClassT: ...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@overload
|
|
71
|
+
def singleton(
|
|
72
|
+
cls: None = None, /, *, dependency_type: Any = NOT_SET
|
|
73
|
+
) -> Callable[[ClassT], ClassT]: ...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def singleton(
|
|
77
|
+
cls: ClassT | None = None, /, *, dependency_type: Any = NOT_SET
|
|
78
|
+
) -> Callable[[ClassT], ClassT] | ClassT:
|
|
79
|
+
"""Decorator for marking a class as a singleton dependency."""
|
|
80
|
+
|
|
81
|
+
def decorator(c: ClassT) -> ClassT:
|
|
82
|
+
metadata: ProvidedMetadata = {"scope": "singleton"}
|
|
83
|
+
if dependency_type is not NOT_SET:
|
|
84
|
+
metadata["dependency_type"] = dependency_type
|
|
85
|
+
c.__provided__ = metadata # type: ignore[attr-defined]
|
|
86
|
+
return c
|
|
87
|
+
|
|
88
|
+
if cls is None:
|
|
89
|
+
return decorator
|
|
90
|
+
|
|
91
|
+
return decorator(cls)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@overload
|
|
95
|
+
def transient(cls: ClassT, /) -> ClassT: ...
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@overload
|
|
99
|
+
def transient(
|
|
100
|
+
cls: None = None, /, *, dependency_type: Any = NOT_SET
|
|
101
|
+
) -> Callable[[ClassT], ClassT]: ...
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def transient(
|
|
105
|
+
cls: ClassT | None = None, /, *, dependency_type: Any = NOT_SET
|
|
106
|
+
) -> Callable[[ClassT], ClassT] | ClassT:
|
|
107
|
+
"""Decorator for marking a class as a transient dependency."""
|
|
108
|
+
|
|
109
|
+
def decorator(c: ClassT) -> ClassT:
|
|
110
|
+
metadata: ProvidedMetadata = {"scope": "transient"}
|
|
111
|
+
if dependency_type is not NOT_SET:
|
|
112
|
+
metadata["dependency_type"] = dependency_type
|
|
113
|
+
c.__provided__ = metadata # type: ignore[attr-defined]
|
|
114
|
+
return c
|
|
115
|
+
|
|
116
|
+
if cls is None:
|
|
117
|
+
return decorator
|
|
118
|
+
|
|
119
|
+
return decorator(cls)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@overload
|
|
123
|
+
def request(cls: ClassT, /, *, from_context: bool = False) -> ClassT: ...
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@overload
|
|
127
|
+
def request(
|
|
128
|
+
cls: None = None, /, *, dependency_type: Any = NOT_SET, from_context: bool = False
|
|
129
|
+
) -> Callable[[ClassT], ClassT]: ...
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def request(
|
|
133
|
+
cls: ClassT | None = None,
|
|
134
|
+
/,
|
|
135
|
+
*,
|
|
136
|
+
dependency_type: Any = NOT_SET,
|
|
137
|
+
from_context: bool = False,
|
|
138
|
+
) -> Callable[[ClassT], ClassT] | ClassT:
|
|
139
|
+
"""Decorator for marking a class as a request-scoped dependency."""
|
|
140
|
+
|
|
141
|
+
def decorator(c: ClassT) -> ClassT:
|
|
142
|
+
metadata: ProvidedMetadata = {"scope": "request"}
|
|
143
|
+
if dependency_type is not NOT_SET:
|
|
144
|
+
metadata["dependency_type"] = dependency_type
|
|
145
|
+
if from_context:
|
|
146
|
+
metadata["from_context"] = from_context
|
|
147
|
+
c.__provided__ = metadata # type: ignore[attr-defined]
|
|
148
|
+
return c
|
|
149
|
+
|
|
150
|
+
if cls is None:
|
|
151
|
+
return decorator
|
|
152
|
+
|
|
153
|
+
return decorator(cls)
|
|
47
154
|
|
|
48
155
|
|
|
49
156
|
class Provided(Protocol):
|
|
@@ -51,7 +158,7 @@ class Provided(Protocol):
|
|
|
51
158
|
|
|
52
159
|
|
|
53
160
|
def is_provided(cls: Any) -> TypeGuard[type[Provided]]:
|
|
54
|
-
return hasattr(cls, "__provided__")
|
|
161
|
+
return hasattr(cls, "__provided__") and "scope" in cls.__provided__
|
|
55
162
|
|
|
56
163
|
|
|
57
164
|
class ProviderMetadata(TypedDict):
|
anydi/_graph.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Graph generation for AnyDI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
7
|
+
|
|
8
|
+
from typing_extensions import type_repr
|
|
9
|
+
|
|
10
|
+
from ._provider import Provider
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ._container import Container
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Graph:
|
|
17
|
+
"""Graph generator for the dependency container."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, container: Container) -> None:
|
|
20
|
+
self._container = container
|
|
21
|
+
|
|
22
|
+
def draw(
|
|
23
|
+
self,
|
|
24
|
+
output_format: Literal["tree", "mermaid", "dot", "json"] = "tree",
|
|
25
|
+
*,
|
|
26
|
+
full_path: bool = False,
|
|
27
|
+
**kwargs: Any,
|
|
28
|
+
) -> str:
|
|
29
|
+
"""Draw the dependency graph."""
|
|
30
|
+
if output_format == "mermaid":
|
|
31
|
+
return self._mermaid(full_path=full_path)
|
|
32
|
+
if output_format == "dot":
|
|
33
|
+
return self._dot(full_path=full_path)
|
|
34
|
+
if output_format == "json":
|
|
35
|
+
return self._json(full_path=full_path, ident=kwargs.get("ident", 2))
|
|
36
|
+
return self._tree(full_path=full_path)
|
|
37
|
+
|
|
38
|
+
def _mermaid(self, full_path: bool) -> str:
|
|
39
|
+
"""Generate mermaid format dependency graph."""
|
|
40
|
+
lines: list[str] = ["graph TD"]
|
|
41
|
+
seen_nodes: set[str] = set()
|
|
42
|
+
|
|
43
|
+
for provider in self._container.providers.values():
|
|
44
|
+
dependency_repr = self._get_name(provider, full_path)
|
|
45
|
+
scope_label = self._get_scope_label(provider.scope, provider.from_context)
|
|
46
|
+
node_id = dependency_repr.replace(".", "_")
|
|
47
|
+
|
|
48
|
+
if node_id not in seen_nodes:
|
|
49
|
+
seen_nodes.add(node_id)
|
|
50
|
+
|
|
51
|
+
for param in provider.parameters:
|
|
52
|
+
if param.provider is None:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
dep_name = self._get_name(param.provider, full_path)
|
|
56
|
+
dep_scope = self._get_scope_label(
|
|
57
|
+
param.provider.scope, param.provider.from_context
|
|
58
|
+
)
|
|
59
|
+
dep_node_id = dep_name.replace(".", "_")
|
|
60
|
+
|
|
61
|
+
if dep_node_id not in seen_nodes:
|
|
62
|
+
seen_nodes.add(dep_node_id)
|
|
63
|
+
|
|
64
|
+
# Use dashed line for from_context dependencies
|
|
65
|
+
if param.provider.from_context:
|
|
66
|
+
arrow = "-.->"
|
|
67
|
+
else:
|
|
68
|
+
arrow = "-->"
|
|
69
|
+
|
|
70
|
+
lines.append(
|
|
71
|
+
f' {node_id}["{dependency_repr} ({scope_label})"] '
|
|
72
|
+
f"{arrow}|{param.name}| "
|
|
73
|
+
f'{dep_node_id}["{dep_name} ({dep_scope})"]'
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return "\n".join(lines)
|
|
77
|
+
|
|
78
|
+
def _dot(self, full_path: bool) -> str:
|
|
79
|
+
"""Generate DOT format dependency graph."""
|
|
80
|
+
lines: list[str] = ["digraph G {"]
|
|
81
|
+
lines.append(" node [shape=box];")
|
|
82
|
+
seen_edges: set[tuple[str, str]] = set()
|
|
83
|
+
|
|
84
|
+
for provider in self._container.providers.values():
|
|
85
|
+
provider_name = self._get_name(provider, full_path)
|
|
86
|
+
scope_label = self._get_scope_label(provider.scope, provider.from_context)
|
|
87
|
+
node_id = f'"{provider_name} ({scope_label})"'
|
|
88
|
+
|
|
89
|
+
for param in provider.parameters:
|
|
90
|
+
if param.provider is None:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
dep_name = self._get_name(param.provider, full_path)
|
|
94
|
+
dep_scope = self._get_scope_label(
|
|
95
|
+
param.provider.scope, param.provider.from_context
|
|
96
|
+
)
|
|
97
|
+
dep_node_id = f'"{dep_name} ({dep_scope})"'
|
|
98
|
+
|
|
99
|
+
if (node_id, dep_node_id) not in seen_edges:
|
|
100
|
+
style = " [style=dashed]" if param.provider.from_context else ""
|
|
101
|
+
lines.append(
|
|
102
|
+
f' {node_id} -> {dep_node_id} [label="{param.name}"]{style};'
|
|
103
|
+
)
|
|
104
|
+
seen_edges.add((node_id, dep_node_id))
|
|
105
|
+
|
|
106
|
+
lines.append("}")
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
|
|
109
|
+
def _json(self, full_path: bool, ident: int) -> str:
|
|
110
|
+
"""Generate JSON format dependency graph."""
|
|
111
|
+
container_type = type(self._container)
|
|
112
|
+
providers: list[dict[str, Any]] = []
|
|
113
|
+
|
|
114
|
+
for provider in self._container.providers.values():
|
|
115
|
+
# Exclude Container itself
|
|
116
|
+
if provider.dependency_type is container_type:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
dependencies: list[dict[str, str]] = []
|
|
120
|
+
for param in provider.parameters:
|
|
121
|
+
if param.provider is None:
|
|
122
|
+
continue
|
|
123
|
+
dependencies.append(
|
|
124
|
+
{
|
|
125
|
+
"name": param.name,
|
|
126
|
+
"type": self._get_name(param.provider, full_path),
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
providers.append(
|
|
131
|
+
{
|
|
132
|
+
"type": self._get_name(provider, full_path),
|
|
133
|
+
"scope": provider.scope,
|
|
134
|
+
"from_context": provider.from_context,
|
|
135
|
+
"dependencies": dependencies,
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return json.dumps({"providers": providers}, indent=ident)
|
|
140
|
+
|
|
141
|
+
def _tree(self, full_path: bool) -> str:
|
|
142
|
+
"""Generate tree format dependency graph."""
|
|
143
|
+
lines: list[str] = []
|
|
144
|
+
|
|
145
|
+
# Find all dependency types (providers that are dependencies of others)
|
|
146
|
+
all_deps: set[Any] = set()
|
|
147
|
+
for provider in self._container.providers.values():
|
|
148
|
+
for param in provider.parameters:
|
|
149
|
+
if param.provider is not None:
|
|
150
|
+
all_deps.add(param.provider.dependency_type)
|
|
151
|
+
|
|
152
|
+
# Root providers: not a dependency of any other provider
|
|
153
|
+
# Exclude Container itself (internal implementation detail)
|
|
154
|
+
container_type = type(self._container)
|
|
155
|
+
root_providers = [
|
|
156
|
+
p
|
|
157
|
+
for p in self._container.providers.values()
|
|
158
|
+
if p.dependency_type not in all_deps
|
|
159
|
+
and p.dependency_type is not container_type
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
for i, provider in enumerate(root_providers):
|
|
163
|
+
if i > 0:
|
|
164
|
+
lines.append("")
|
|
165
|
+
lines.append(self._format_tree_node(provider, full_path))
|
|
166
|
+
self._render_tree_children(provider, "", set(), lines, full_path)
|
|
167
|
+
|
|
168
|
+
return "\n".join(lines)
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def _format_tree_node(
|
|
172
|
+
cls, provider: Provider, full_path: bool, param_name: str | None = None
|
|
173
|
+
) -> str:
|
|
174
|
+
name = cls._get_name(provider, full_path)
|
|
175
|
+
scope_label = Graph._get_scope_label(provider.scope, provider.from_context)
|
|
176
|
+
context_marker = " [context]" if provider.from_context else ""
|
|
177
|
+
if param_name:
|
|
178
|
+
return f"{param_name}: {name} ({scope_label}){context_marker}"
|
|
179
|
+
return f"{name} ({scope_label}){context_marker}"
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _render_tree_children(
|
|
183
|
+
provider: Provider,
|
|
184
|
+
prefix: str,
|
|
185
|
+
visited: set[Any],
|
|
186
|
+
lines: list[str],
|
|
187
|
+
full_path: bool,
|
|
188
|
+
) -> None:
|
|
189
|
+
if provider.dependency_type in visited:
|
|
190
|
+
return
|
|
191
|
+
visited = visited | {provider.dependency_type}
|
|
192
|
+
|
|
193
|
+
deps = [p for p in provider.parameters if p.provider is not None]
|
|
194
|
+
for i, param in enumerate(deps):
|
|
195
|
+
dep_provider = param.provider
|
|
196
|
+
if dep_provider is None:
|
|
197
|
+
continue
|
|
198
|
+
is_last = i == len(deps) - 1
|
|
199
|
+
connector = "└── " if is_last else "├── "
|
|
200
|
+
node_text = Graph._format_tree_node(dep_provider, full_path, param.name)
|
|
201
|
+
lines.append(f"{prefix}{connector}{node_text}")
|
|
202
|
+
extension = " " if is_last else "│ "
|
|
203
|
+
Graph._render_tree_children(
|
|
204
|
+
dep_provider, prefix + extension, visited, lines, full_path
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def _get_name(provider: Provider, full_path: bool) -> str:
|
|
209
|
+
if full_path:
|
|
210
|
+
return type_repr(provider.dependency_type)
|
|
211
|
+
return type_repr(provider.dependency_type).rsplit(".", 1)[-1]
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _get_scope_label(scope: str, from_context: bool) -> str:
|
|
215
|
+
if from_context:
|
|
216
|
+
return f"{scope}/context"
|
|
217
|
+
return scope
|
anydi/_injector.py
CHANGED
|
@@ -69,9 +69,11 @@ class Injector:
|
|
|
69
69
|
"""Get the injected parameters of a callable object."""
|
|
70
70
|
injected_params: dict[str, Any] = {}
|
|
71
71
|
for parameter in inspect.signature(call, eval_str=True).parameters.values():
|
|
72
|
-
|
|
72
|
+
dependency_type, should_inject, _ = self.validate_parameter(
|
|
73
|
+
parameter, call=call
|
|
74
|
+
)
|
|
73
75
|
if should_inject:
|
|
74
|
-
injected_params[parameter.name] =
|
|
76
|
+
injected_params[parameter.name] = dependency_type
|
|
75
77
|
return injected_params
|
|
76
78
|
|
|
77
79
|
def validate_parameter(
|
|
@@ -79,28 +81,28 @@ class Injector:
|
|
|
79
81
|
) -> tuple[Any, bool, Marker | None]:
|
|
80
82
|
"""Validate an injected parameter."""
|
|
81
83
|
parameter = self.unwrap_parameter(parameter)
|
|
82
|
-
|
|
84
|
+
dependency_type = parameter.annotation
|
|
83
85
|
|
|
84
86
|
marker = parameter.default
|
|
85
87
|
if not is_marker(marker):
|
|
86
|
-
return
|
|
88
|
+
return dependency_type, False, None
|
|
87
89
|
|
|
88
|
-
if
|
|
90
|
+
if dependency_type is inspect.Parameter.empty:
|
|
89
91
|
raise TypeError(
|
|
90
92
|
f"Missing `{type_repr(call)}` parameter `{parameter.name}` annotation."
|
|
91
93
|
)
|
|
92
94
|
|
|
93
|
-
# Set inject marker
|
|
94
|
-
parameter.default.
|
|
95
|
+
# Set inject marker dependency type
|
|
96
|
+
parameter.default.dependency_type = dependency_type
|
|
95
97
|
|
|
96
|
-
if not self.container.has_provider_for(
|
|
98
|
+
if not self.container.has_provider_for(dependency_type):
|
|
97
99
|
raise LookupError(
|
|
98
100
|
f"`{type_repr(call)}` has an unknown dependency parameter "
|
|
99
101
|
f"`{parameter.name}` with an annotation of "
|
|
100
|
-
f"`{type_repr(
|
|
102
|
+
f"`{type_repr(dependency_type)}`."
|
|
101
103
|
)
|
|
102
104
|
|
|
103
|
-
return
|
|
105
|
+
return dependency_type, True, marker
|
|
104
106
|
|
|
105
107
|
@staticmethod
|
|
106
108
|
def unwrap_parameter(parameter: inspect.Parameter) -> inspect.Parameter:
|
anydi/_marker.py
CHANGED
|
@@ -12,18 +12,18 @@ T = TypeVar("T")
|
|
|
12
12
|
class Marker:
|
|
13
13
|
"""Marker stored in annotations or defaults to request injection."""
|
|
14
14
|
|
|
15
|
-
__slots__ = ("
|
|
15
|
+
__slots__ = ("_dependency_type", "_attrs", "_preferred_owner", "_current_owner")
|
|
16
16
|
|
|
17
17
|
_FRAMEWORK_ATTRS = frozenset({"dependency", "use_cache", "cast", "cast_result"})
|
|
18
18
|
|
|
19
|
-
def __init__(self,
|
|
19
|
+
def __init__(self, dependency_type: Any = NOT_SET) -> None:
|
|
20
20
|
# Avoid reinitializing attributes when mixins call __init__ multiple times
|
|
21
21
|
if not hasattr(self, "_attrs"):
|
|
22
22
|
super().__init__()
|
|
23
23
|
self._attrs: dict[str, dict[str, Any]] = {}
|
|
24
24
|
self._preferred_owner = "fastapi"
|
|
25
25
|
self._current_owner: str | None = None
|
|
26
|
-
self.
|
|
26
|
+
self._dependency_type = dependency_type
|
|
27
27
|
|
|
28
28
|
def set_owner(self, owner: str) -> None:
|
|
29
29
|
self._preferred_owner = owner
|
|
@@ -53,14 +53,14 @@ class Marker:
|
|
|
53
53
|
raise AttributeError(name)
|
|
54
54
|
|
|
55
55
|
@property
|
|
56
|
-
def
|
|
57
|
-
if self.
|
|
58
|
-
raise TypeError("
|
|
59
|
-
return self.
|
|
56
|
+
def dependency_type(self) -> Any:
|
|
57
|
+
if self._dependency_type is NOT_SET:
|
|
58
|
+
raise TypeError("Dependency type is not set.")
|
|
59
|
+
return self._dependency_type
|
|
60
60
|
|
|
61
|
-
@
|
|
62
|
-
def
|
|
63
|
-
self.
|
|
61
|
+
@dependency_type.setter
|
|
62
|
+
def dependency_type(self, dependency_type: Any) -> None:
|
|
63
|
+
self._dependency_type = dependency_type
|
|
64
64
|
|
|
65
65
|
def __class_getitem__(cls, item: Any) -> Any:
|
|
66
66
|
return Annotated[item, cls()]
|
|
@@ -98,9 +98,7 @@ class _ProvideMeta(type):
|
|
|
98
98
|
"""Metaclass for Provide that delegates __class_getitem__ to the active marker."""
|
|
99
99
|
|
|
100
100
|
def __getitem__(cls, item: Any) -> Any:
|
|
101
|
-
|
|
102
|
-
return _marker_cls.__class_getitem__(item) # type: ignore
|
|
103
|
-
return Annotated[item, _marker_cls()]
|
|
101
|
+
return _marker_cls.__class_getitem__(item)
|
|
104
102
|
|
|
105
103
|
|
|
106
104
|
if TYPE_CHECKING:
|
anydi/_provider.py
CHANGED
|
@@ -2,10 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import enum
|
|
4
4
|
import inspect
|
|
5
|
+
import warnings
|
|
5
6
|
from collections.abc import Callable
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
+
from dataclasses import KW_ONLY, dataclass
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
10
|
+
from typing_extensions import type_repr
|
|
11
|
+
|
|
9
12
|
from ._types import NOT_SET, Scope
|
|
10
13
|
|
|
11
14
|
|
|
@@ -35,8 +38,8 @@ class ProviderKind(enum.IntEnum):
|
|
|
35
38
|
|
|
36
39
|
@dataclass(frozen=True, slots=True)
|
|
37
40
|
class ProviderParameter:
|
|
41
|
+
dependency_type: Any
|
|
38
42
|
name: str
|
|
39
|
-
annotation: Any
|
|
40
43
|
default: Any
|
|
41
44
|
has_default: bool
|
|
42
45
|
provider: Provider | None = None
|
|
@@ -45,10 +48,10 @@ class ProviderParameter:
|
|
|
45
48
|
|
|
46
49
|
@dataclass(frozen=True, slots=True)
|
|
47
50
|
class Provider:
|
|
48
|
-
|
|
51
|
+
dependency_type: Any
|
|
52
|
+
factory: Callable[..., Any]
|
|
49
53
|
scope: Scope
|
|
50
|
-
|
|
51
|
-
name: str
|
|
54
|
+
from_context: bool
|
|
52
55
|
parameters: tuple[ProviderParameter, ...]
|
|
53
56
|
is_class: bool
|
|
54
57
|
is_coroutine: bool
|
|
@@ -57,9 +60,44 @@ class Provider:
|
|
|
57
60
|
is_async: bool
|
|
58
61
|
is_resource: bool
|
|
59
62
|
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
dep_repr = type_repr(self.dependency_type)
|
|
65
|
+
# For class providers, factory == dependency_type, so just show the type
|
|
66
|
+
if self.is_class:
|
|
67
|
+
return dep_repr
|
|
68
|
+
# For factory providers, include the factory path
|
|
69
|
+
factory_repr = type_repr(self.factory)
|
|
70
|
+
return f"{dep_repr} (via {factory_repr})"
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
|
|
73
|
+
@dataclass(slots=True)
|
|
62
74
|
class ProviderDef:
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
dependency_type: Any = NOT_SET
|
|
76
|
+
factory: Callable[..., Any] = NOT_SET
|
|
77
|
+
_: KW_ONLY
|
|
78
|
+
from_context: bool = False
|
|
79
|
+
scope: Scope = "singleton"
|
|
65
80
|
interface: Any = NOT_SET
|
|
81
|
+
call: Callable[..., Any] = NOT_SET
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
if self.interface is not NOT_SET:
|
|
85
|
+
warnings.warn(
|
|
86
|
+
"The `interface` is deprecated. Use `dependency_type` instead.",
|
|
87
|
+
DeprecationWarning,
|
|
88
|
+
stacklevel=2,
|
|
89
|
+
)
|
|
90
|
+
if self.call is not NOT_SET:
|
|
91
|
+
warnings.warn(
|
|
92
|
+
"The `call` is deprecated. Use `factory` instead.",
|
|
93
|
+
DeprecationWarning,
|
|
94
|
+
stacklevel=2,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if self.dependency_type is NOT_SET:
|
|
98
|
+
self.dependency_type = self.interface
|
|
99
|
+
if self.factory is NOT_SET:
|
|
100
|
+
self.factory = self.call
|
|
101
|
+
|
|
102
|
+
self.interface = self.dependency_type
|
|
103
|
+
self.call = self.factory
|