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.
Files changed (28) hide show
  1. {anydi-0.69.0 → anydi-0.70.0}/PKG-INFO +1 -1
  2. {anydi-0.69.0 → anydi-0.70.0}/anydi/_container.py +44 -4
  3. {anydi-0.69.0 → anydi-0.70.0}/anydi/_decorators.py +24 -26
  4. {anydi-0.69.0 → anydi-0.70.0}/anydi/_graph.py +27 -16
  5. {anydi-0.69.0 → anydi-0.70.0}/anydi/_resolver.py +19 -2
  6. {anydi-0.69.0 → anydi-0.70.0}/anydi/_scanner.py +6 -4
  7. {anydi-0.69.0 → anydi-0.70.0}/pyproject.toml +1 -1
  8. {anydi-0.69.0 → anydi-0.70.0}/README.md +0 -0
  9. {anydi-0.69.0 → anydi-0.70.0}/anydi/__init__.py +0 -0
  10. {anydi-0.69.0 → anydi-0.70.0}/anydi/_async_lock.py +0 -0
  11. {anydi-0.69.0 → anydi-0.70.0}/anydi/_cli.py +0 -0
  12. {anydi-0.69.0 → anydi-0.70.0}/anydi/_context.py +0 -0
  13. {anydi-0.69.0 → anydi-0.70.0}/anydi/_injector.py +0 -0
  14. {anydi-0.69.0 → anydi-0.70.0}/anydi/_marker.py +0 -0
  15. {anydi-0.69.0 → anydi-0.70.0}/anydi/_module.py +0 -0
  16. {anydi-0.69.0 → anydi-0.70.0}/anydi/_provider.py +0 -0
  17. {anydi-0.69.0 → anydi-0.70.0}/anydi/_types.py +0 -0
  18. {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/__init__.py +0 -0
  19. {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/django/__init__.py +0 -0
  20. {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/fastapi.py +0 -0
  21. {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/faststream.py +0 -0
  22. {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/pydantic_settings.py +0 -0
  23. {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/pytest_plugin.py +0 -0
  24. {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/starlette/__init__.py +0 -0
  25. {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/starlette/middleware.py +0 -0
  26. {anydi-0.69.0 → anydi-0.70.0}/anydi/ext/typer.py +0 -0
  27. {anydi-0.69.0 → anydi-0.70.0}/anydi/py.typed +0 -0
  28. {anydi-0.69.0 → anydi-0.70.0}/anydi/testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anydi
3
- Version: 0.69.0
3
+ Version: 0.70.0
4
4
  Summary: Dependency Injection library
5
5
  Keywords: dependency injection,dependencies,di,async,asyncio,application
6
6
  Author: Anton Ruhlov
@@ -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
- return dependency_type in self._providers
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[dependency_type]
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
- @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]: ...
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
- dependency_type: Any = NOT_SET, /, *, scope: Scope, from_context: bool = False
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 dependency_type is not NOT_SET:
57
- metadata["dependency_type"] = dependency_type
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, /, *, dependency_type: Any = NOT_SET
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, /, *, dependency_type: Any = NOT_SET
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 dependency_type is not NOT_SET:
84
- metadata["dependency_type"] = dependency_type
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, /, *, dependency_type: Any = NOT_SET
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, /, *, dependency_type: Any = NOT_SET
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 dependency_type is not NOT_SET:
112
- metadata["dependency_type"] = dependency_type
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, /, *, dependency_type: Any = NOT_SET, from_context: bool = False
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
- dependency_type: Any = NOT_SET,
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 dependency_type is not NOT_SET:
144
- metadata["dependency_type"] = dependency_type
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
- 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
+ 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
- cls, provider: Provider, full_path: bool, param_name: str | None = None
181
+ self, provider: Provider, full_path: bool, param_name: str | None = None
173
182
  ) -> str:
174
- name = cls._get_name(provider, full_path)
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 = Graph._format_tree_node(dep_provider, full_path, param.name)
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
- Graph._render_tree_children(
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 instance for a dependency type."""
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 instance for a dependency type."""
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
- dependency_type = cls.__provided__.get("dependency_type", cls)
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
- dependency_type, cls, scope=scope, from_context=from_context
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:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anydi"
3
- version = "0.69.0"
3
+ version = "0.70.0"
4
4
  description = "Dependency Injection library"
5
5
  authors = [{ name = "Anton Ruhlov", email = "antonruhlov@gmail.com" }]
6
6
  requires-python = ">=3.10.0, <3.15"
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