anydi 0.69.0__tar.gz → 0.70.0__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.
- {anydi-0.69.0 → anydi-0.70.0}/PKG-INFO +1 -1
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_container.py +44 -4
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_decorators.py +24 -26
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_graph.py +27 -16
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_resolver.py +19 -2
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_scanner.py +6 -4
- {anydi-0.69.0 → anydi-0.70.0}/pyproject.toml +1 -1
- {anydi-0.69.0 → anydi-0.70.0}/README.md +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/__init__.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_async_lock.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_cli.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_context.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_injector.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_marker.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_module.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_provider.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/_types.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/__init__.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/django/__init__.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/fastapi.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/faststream.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/pydantic_settings.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/pytest_plugin.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/starlette/__init__.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/starlette/middleware.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/typer.py +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/py.typed +0 -0
- {anydi-0.69.0 → anydi-0.70.0}/anydi/testing.py +0 -0
|
@@ -17,7 +17,7 @@ from typing import Any, Literal, TypeVar, get_args, get_origin, overload
|
|
|
17
17
|
from typing_extensions import ParamSpec, Self, type_repr
|
|
18
18
|
|
|
19
19
|
from ._context import InstanceContext
|
|
20
|
-
from ._decorators import is_provided
|
|
20
|
+
from ._decorators import get_alias_list, is_provided
|
|
21
21
|
from ._graph import Graph
|
|
22
22
|
from ._injector import Injector
|
|
23
23
|
from ._marker import Marker
|
|
@@ -49,6 +49,7 @@ class Container:
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
self._resources: dict[str, list[Any]] = defaultdict(list)
|
|
52
|
+
self._aliases: dict[Any, Any] = {} # alias_type → canonical_type
|
|
52
53
|
self._singleton_context = InstanceContext()
|
|
53
54
|
self._scoped_context: dict[str, ContextVar[InstanceContext]] = {}
|
|
54
55
|
|
|
@@ -95,6 +96,11 @@ class Container:
|
|
|
95
96
|
"""Get the registered providers."""
|
|
96
97
|
return self._providers
|
|
97
98
|
|
|
99
|
+
@property
|
|
100
|
+
def aliases(self) -> dict[Any, Any]:
|
|
101
|
+
"""Get the registered aliases."""
|
|
102
|
+
return self._aliases
|
|
103
|
+
|
|
98
104
|
@property
|
|
99
105
|
def ready(self) -> bool:
|
|
100
106
|
"""Check if the container is ready."""
|
|
@@ -370,9 +376,30 @@ class Container:
|
|
|
370
376
|
dependency_type, factory, scope, from_context, override, None
|
|
371
377
|
)
|
|
372
378
|
|
|
379
|
+
def alias(self, alias_type: Any, canonical_type: Any, /) -> None:
|
|
380
|
+
"""Register an alias for a dependency type."""
|
|
381
|
+
if self.ready:
|
|
382
|
+
raise RuntimeError(
|
|
383
|
+
"Cannot register aliases after build() has been called. "
|
|
384
|
+
"All aliases must be registered before building the container."
|
|
385
|
+
)
|
|
386
|
+
if alias_type == canonical_type:
|
|
387
|
+
raise ValueError("Alias type cannot be the same as canonical type.")
|
|
388
|
+
if alias_type in self._aliases:
|
|
389
|
+
raise ValueError(
|
|
390
|
+
f"Alias `{type_repr(alias_type)}` is already registered "
|
|
391
|
+
f"for `{type_repr(self._aliases[alias_type])}`."
|
|
392
|
+
)
|
|
393
|
+
self._aliases[alias_type] = canonical_type
|
|
394
|
+
|
|
395
|
+
def _resolve_alias(self, dependency_type: Any) -> Any:
|
|
396
|
+
"""Resolve an alias to its canonical type."""
|
|
397
|
+
return self._aliases.get(dependency_type, dependency_type)
|
|
398
|
+
|
|
373
399
|
def is_registered(self, dependency_type: Any, /) -> bool:
|
|
374
400
|
"""Check if a provider is registered for the specified dependency type."""
|
|
375
|
-
|
|
401
|
+
canonical = self._resolve_alias(dependency_type)
|
|
402
|
+
return canonical in self._providers
|
|
376
403
|
|
|
377
404
|
def has_provider_for(self, dependency_type: Any, /) -> bool:
|
|
378
405
|
"""Check if a provider exists for the specified dependency type."""
|
|
@@ -570,9 +597,10 @@ class Container:
|
|
|
570
597
|
return provider
|
|
571
598
|
|
|
572
599
|
def _get_provider(self, dependency_type: Any) -> Provider:
|
|
573
|
-
"""Get provider by dependency type."""
|
|
600
|
+
"""Get provider by dependency type, resolving aliases."""
|
|
601
|
+
canonical = self._resolve_alias(dependency_type)
|
|
574
602
|
try:
|
|
575
|
-
return self._providers[
|
|
603
|
+
return self._providers[canonical]
|
|
576
604
|
except KeyError:
|
|
577
605
|
raise LookupError(
|
|
578
606
|
f"The provider for `{type_repr(dependency_type)}` has "
|
|
@@ -597,6 +625,10 @@ class Container:
|
|
|
597
625
|
False,
|
|
598
626
|
defaults,
|
|
599
627
|
)
|
|
628
|
+
# Register aliases if specified
|
|
629
|
+
if not self.ready:
|
|
630
|
+
for alias_type in get_alias_list(dependency_type.__provided__):
|
|
631
|
+
self.alias(alias_type, dependency_type)
|
|
600
632
|
registered = True
|
|
601
633
|
else:
|
|
602
634
|
raise LookupError(
|
|
@@ -677,6 +709,9 @@ class Container:
|
|
|
677
709
|
False,
|
|
678
710
|
None,
|
|
679
711
|
)
|
|
712
|
+
# Register aliases if specified
|
|
713
|
+
for alias_type in get_alias_list(dependency_type.__provided__):
|
|
714
|
+
self._aliases[alias_type] = dependency_type
|
|
680
715
|
# Recursively ensure the @provided class is resolved
|
|
681
716
|
dep_provider = self._ensure_provider_resolved(
|
|
682
717
|
dep_provider, resolving
|
|
@@ -977,6 +1012,11 @@ class Container:
|
|
|
977
1012
|
False,
|
|
978
1013
|
None,
|
|
979
1014
|
)
|
|
1015
|
+
# Register aliases if specified
|
|
1016
|
+
for alias_type in get_alias_list(
|
|
1017
|
+
param_dependency_type.__provided__
|
|
1018
|
+
):
|
|
1019
|
+
self._aliases[alias_type] = param_dependency_type
|
|
980
1020
|
elif param.has_default:
|
|
981
1021
|
# Has default, can be missing
|
|
982
1022
|
resolved_params.append(param)
|
|
@@ -29,32 +29,30 @@ ModuleT = TypeVar("ModuleT", bound="Module")
|
|
|
29
29
|
class ProvidedMetadata(TypedDict):
|
|
30
30
|
"""Metadata for classes marked as provided by AnyDI."""
|
|
31
31
|
|
|
32
|
-
dependency_type: NotRequired[Any]
|
|
33
32
|
scope: Scope
|
|
33
|
+
alias: NotRequired[Any]
|
|
34
34
|
from_context: NotRequired[bool]
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*, scope: Scope, from_context: bool = False
|
|
46
|
-
) -> Callable[[ClassT], ClassT]: ...
|
|
37
|
+
def get_alias_list(provided: ProvidedMetadata) -> list[Any]:
|
|
38
|
+
"""Get alias list from __provided__ metadata, normalizing single to list."""
|
|
39
|
+
alias = provided.get("alias")
|
|
40
|
+
if alias is None:
|
|
41
|
+
return []
|
|
42
|
+
if isinstance(alias, list | tuple):
|
|
43
|
+
return list(alias) # type: ignore
|
|
44
|
+
return [alias]
|
|
47
45
|
|
|
48
46
|
|
|
49
47
|
def provided(
|
|
50
|
-
|
|
48
|
+
*, scope: Scope, alias: Any = NOT_SET, from_context: bool = False
|
|
51
49
|
) -> Callable[[ClassT], ClassT]:
|
|
52
50
|
"""Decorator for marking a class as provided by AnyDI with a specific scope."""
|
|
53
51
|
|
|
54
52
|
def decorator(cls: ClassT) -> ClassT:
|
|
55
53
|
metadata: ProvidedMetadata = {"scope": scope}
|
|
56
|
-
if
|
|
57
|
-
metadata["
|
|
54
|
+
if alias is not NOT_SET:
|
|
55
|
+
metadata["alias"] = alias
|
|
58
56
|
if from_context:
|
|
59
57
|
metadata["from_context"] = from_context
|
|
60
58
|
cls.__provided__ = metadata # type: ignore[attr-defined]
|
|
@@ -69,19 +67,19 @@ def singleton(cls: ClassT, /) -> ClassT: ...
|
|
|
69
67
|
|
|
70
68
|
@overload
|
|
71
69
|
def singleton(
|
|
72
|
-
cls: None = None, /, *,
|
|
70
|
+
cls: None = None, /, *, alias: Any = NOT_SET
|
|
73
71
|
) -> Callable[[ClassT], ClassT]: ...
|
|
74
72
|
|
|
75
73
|
|
|
76
74
|
def singleton(
|
|
77
|
-
cls: ClassT | None = None, /, *,
|
|
75
|
+
cls: ClassT | None = None, /, *, alias: Any = NOT_SET
|
|
78
76
|
) -> Callable[[ClassT], ClassT] | ClassT:
|
|
79
77
|
"""Decorator for marking a class as a singleton dependency."""
|
|
80
78
|
|
|
81
79
|
def decorator(c: ClassT) -> ClassT:
|
|
82
80
|
metadata: ProvidedMetadata = {"scope": "singleton"}
|
|
83
|
-
if
|
|
84
|
-
metadata["
|
|
81
|
+
if alias is not NOT_SET:
|
|
82
|
+
metadata["alias"] = alias
|
|
85
83
|
c.__provided__ = metadata # type: ignore[attr-defined]
|
|
86
84
|
return c
|
|
87
85
|
|
|
@@ -97,19 +95,19 @@ def transient(cls: ClassT, /) -> ClassT: ...
|
|
|
97
95
|
|
|
98
96
|
@overload
|
|
99
97
|
def transient(
|
|
100
|
-
cls: None = None, /, *,
|
|
98
|
+
cls: None = None, /, *, alias: Any = NOT_SET
|
|
101
99
|
) -> Callable[[ClassT], ClassT]: ...
|
|
102
100
|
|
|
103
101
|
|
|
104
102
|
def transient(
|
|
105
|
-
cls: ClassT | None = None, /, *,
|
|
103
|
+
cls: ClassT | None = None, /, *, alias: Any = NOT_SET
|
|
106
104
|
) -> Callable[[ClassT], ClassT] | ClassT:
|
|
107
105
|
"""Decorator for marking a class as a transient dependency."""
|
|
108
106
|
|
|
109
107
|
def decorator(c: ClassT) -> ClassT:
|
|
110
108
|
metadata: ProvidedMetadata = {"scope": "transient"}
|
|
111
|
-
if
|
|
112
|
-
metadata["
|
|
109
|
+
if alias is not NOT_SET:
|
|
110
|
+
metadata["alias"] = alias
|
|
113
111
|
c.__provided__ = metadata # type: ignore[attr-defined]
|
|
114
112
|
return c
|
|
115
113
|
|
|
@@ -125,7 +123,7 @@ def request(cls: ClassT, /, *, from_context: bool = False) -> ClassT: ...
|
|
|
125
123
|
|
|
126
124
|
@overload
|
|
127
125
|
def request(
|
|
128
|
-
cls: None = None, /, *,
|
|
126
|
+
cls: None = None, /, *, alias: Any = NOT_SET, from_context: bool = False
|
|
129
127
|
) -> Callable[[ClassT], ClassT]: ...
|
|
130
128
|
|
|
131
129
|
|
|
@@ -133,15 +131,15 @@ def request(
|
|
|
133
131
|
cls: ClassT | None = None,
|
|
134
132
|
/,
|
|
135
133
|
*,
|
|
136
|
-
|
|
134
|
+
alias: Any = NOT_SET,
|
|
137
135
|
from_context: bool = False,
|
|
138
136
|
) -> Callable[[ClassT], ClassT] | ClassT:
|
|
139
137
|
"""Decorator for marking a class as a request-scoped dependency."""
|
|
140
138
|
|
|
141
139
|
def decorator(c: ClassT) -> ClassT:
|
|
142
140
|
metadata: ProvidedMetadata = {"scope": "request"}
|
|
143
|
-
if
|
|
144
|
-
metadata["
|
|
141
|
+
if alias is not NOT_SET:
|
|
142
|
+
metadata["alias"] = alias
|
|
145
143
|
if from_context:
|
|
146
144
|
metadata["from_context"] = from_context
|
|
147
145
|
c.__provided__ = metadata # type: ignore[attr-defined]
|
|
@@ -19,6 +19,14 @@ class Graph:
|
|
|
19
19
|
def __init__(self, container: Container) -> None:
|
|
20
20
|
self._container = container
|
|
21
21
|
|
|
22
|
+
def _get_aliases_for(self, dependency_type: Any) -> list[str]:
|
|
23
|
+
"""Get list of alias names that point to a dependency type."""
|
|
24
|
+
aliases: list[str] = []
|
|
25
|
+
for alias, canonical in self._container.aliases.items():
|
|
26
|
+
if canonical == dependency_type:
|
|
27
|
+
aliases.append(type_repr(alias).rsplit(".", 1)[-1])
|
|
28
|
+
return aliases
|
|
29
|
+
|
|
22
30
|
def draw(
|
|
23
31
|
self,
|
|
24
32
|
output_format: Literal["tree", "mermaid", "dot", "json"] = "tree",
|
|
@@ -127,14 +135,16 @@ class Graph:
|
|
|
127
135
|
}
|
|
128
136
|
)
|
|
129
137
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
aliases = self._get_aliases_for(provider.dependency_type)
|
|
139
|
+
provider_data: dict[str, Any] = {
|
|
140
|
+
"type": self._get_name(provider, full_path),
|
|
141
|
+
"scope": provider.scope,
|
|
142
|
+
"from_context": provider.from_context,
|
|
143
|
+
"dependencies": dependencies,
|
|
144
|
+
}
|
|
145
|
+
if aliases:
|
|
146
|
+
provider_data["aliases"] = aliases
|
|
147
|
+
providers.append(provider_data)
|
|
138
148
|
|
|
139
149
|
return json.dumps({"providers": providers}, indent=ident)
|
|
140
150
|
|
|
@@ -167,19 +177,20 @@ class Graph:
|
|
|
167
177
|
|
|
168
178
|
return "\n".join(lines)
|
|
169
179
|
|
|
170
|
-
@classmethod
|
|
171
180
|
def _format_tree_node(
|
|
172
|
-
|
|
181
|
+
self, provider: Provider, full_path: bool, param_name: str | None = None
|
|
173
182
|
) -> str:
|
|
174
|
-
name =
|
|
183
|
+
name = self._get_name(provider, full_path)
|
|
175
184
|
scope_label = Graph._get_scope_label(provider.scope, provider.from_context)
|
|
176
185
|
context_marker = " [context]" if provider.from_context else ""
|
|
186
|
+
aliases = self._get_aliases_for(provider.dependency_type)
|
|
187
|
+
alias_marker = f" [alias: {', '.join(aliases)}]" if aliases else ""
|
|
177
188
|
if param_name:
|
|
178
|
-
return f"{param_name}: {name} ({scope_label}){context_marker}"
|
|
179
|
-
return f"{name} ({scope_label}){context_marker}"
|
|
189
|
+
return f"{param_name}: {name} ({scope_label}){context_marker}{alias_marker}"
|
|
190
|
+
return f"{name} ({scope_label}){context_marker}{alias_marker}"
|
|
180
191
|
|
|
181
|
-
@staticmethod
|
|
182
192
|
def _render_tree_children(
|
|
193
|
+
self,
|
|
183
194
|
provider: Provider,
|
|
184
195
|
prefix: str,
|
|
185
196
|
visited: set[Any],
|
|
@@ -197,10 +208,10 @@ class Graph:
|
|
|
197
208
|
continue
|
|
198
209
|
is_last = i == len(deps) - 1
|
|
199
210
|
connector = "└── " if is_last else "├── "
|
|
200
|
-
node_text =
|
|
211
|
+
node_text = self._format_tree_node(dep_provider, full_path, param.name)
|
|
201
212
|
lines.append(f"{prefix}{connector}{node_text}")
|
|
202
213
|
extension = " " if is_last else "│ "
|
|
203
|
-
|
|
214
|
+
self._render_tree_children(
|
|
204
215
|
dep_provider, prefix + extension, visited, lines, full_path
|
|
205
216
|
)
|
|
206
217
|
|
|
@@ -51,12 +51,24 @@ class Resolver:
|
|
|
51
51
|
return bool(self._overrides) or getattr(self._container, "_test_mode", False)
|
|
52
52
|
|
|
53
53
|
def add_override(self, dependency_type: Any, instance: Any) -> None:
|
|
54
|
-
"""Add an override
|
|
54
|
+
"""Add an override for a type, its canonical type, and all aliases."""
|
|
55
55
|
self._overrides[dependency_type] = instance
|
|
56
|
+
canonical = self._container.aliases.get(dependency_type)
|
|
57
|
+
if canonical is not None:
|
|
58
|
+
self._overrides[canonical] = instance
|
|
59
|
+
for alias, canon in self._container.aliases.items():
|
|
60
|
+
if canon == dependency_type:
|
|
61
|
+
self._overrides[alias] = instance
|
|
56
62
|
|
|
57
63
|
def remove_override(self, dependency_type: Any) -> None:
|
|
58
|
-
"""Remove an override
|
|
64
|
+
"""Remove an override for a type, its canonical type, and all aliases."""
|
|
59
65
|
self._overrides.pop(dependency_type, None)
|
|
66
|
+
canonical = self._container.aliases.get(dependency_type)
|
|
67
|
+
if canonical is not None:
|
|
68
|
+
self._overrides.pop(canonical, None)
|
|
69
|
+
for alias, canon in self._container.aliases.items():
|
|
70
|
+
if canon == dependency_type:
|
|
71
|
+
self._overrides.pop(alias, None)
|
|
60
72
|
|
|
61
73
|
def clear_caches(self) -> None:
|
|
62
74
|
"""Clear all cached resolvers."""
|
|
@@ -105,6 +117,11 @@ class Resolver:
|
|
|
105
117
|
# Store the compiled functions in the cache
|
|
106
118
|
cache[provider.dependency_type] = compiled
|
|
107
119
|
|
|
120
|
+
# Also store under all aliases that point to this type
|
|
121
|
+
for alias, canonical in self._container.aliases.items():
|
|
122
|
+
if canonical == provider.dependency_type:
|
|
123
|
+
cache[alias] = compiled
|
|
124
|
+
|
|
108
125
|
return compiled
|
|
109
126
|
|
|
110
127
|
def _add_override_check(
|
|
@@ -8,7 +8,7 @@ from dataclasses import dataclass
|
|
|
8
8
|
from types import ModuleType
|
|
9
9
|
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
|
-
from ._decorators import Provided, is_injectable, is_provided
|
|
11
|
+
from ._decorators import Provided, get_alias_list, is_injectable, is_provided
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from ._container import Container
|
|
@@ -58,13 +58,15 @@ class Scanner:
|
|
|
58
58
|
|
|
59
59
|
# First: register @provided classes
|
|
60
60
|
for cls in provided_classes:
|
|
61
|
-
|
|
62
|
-
if not self._container.is_registered(dependency_type):
|
|
61
|
+
if not self._container.is_registered(cls):
|
|
63
62
|
scope = cls.__provided__["scope"]
|
|
64
63
|
from_context = cls.__provided__.get("from_context", False)
|
|
65
64
|
self._container.register(
|
|
66
|
-
|
|
65
|
+
cls, cls, scope=scope, from_context=from_context
|
|
67
66
|
)
|
|
67
|
+
# Create aliases if specified (alias → cls)
|
|
68
|
+
for alias_type in get_alias_list(cls.__provided__):
|
|
69
|
+
self._container.alias(alias_type, cls)
|
|
68
70
|
|
|
69
71
|
# Second: inject @injectable functions
|
|
70
72
|
for dependency in injectable_dependencies:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|