anydi 0.69.0__tar.gz → 0.70.1__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.1}/PKG-INFO +1 -1
  2. {anydi-0.69.0 → anydi-0.70.1}/anydi/_container.py +70 -7
  3. {anydi-0.69.0 → anydi-0.70.1}/anydi/_decorators.py +22 -30
  4. {anydi-0.69.0 → anydi-0.70.1}/anydi/_graph.py +27 -16
  5. {anydi-0.69.0 → anydi-0.70.1}/anydi/_resolver.py +19 -2
  6. {anydi-0.69.0 → anydi-0.70.1}/anydi/_scanner.py +6 -3
  7. {anydi-0.69.0 → anydi-0.70.1}/anydi/_types.py +9 -0
  8. {anydi-0.69.0 → anydi-0.70.1}/pyproject.toml +1 -1
  9. {anydi-0.69.0 → anydi-0.70.1}/README.md +0 -0
  10. {anydi-0.69.0 → anydi-0.70.1}/anydi/__init__.py +0 -0
  11. {anydi-0.69.0 → anydi-0.70.1}/anydi/_async_lock.py +0 -0
  12. {anydi-0.69.0 → anydi-0.70.1}/anydi/_cli.py +0 -0
  13. {anydi-0.69.0 → anydi-0.70.1}/anydi/_context.py +0 -0
  14. {anydi-0.69.0 → anydi-0.70.1}/anydi/_injector.py +0 -0
  15. {anydi-0.69.0 → anydi-0.70.1}/anydi/_marker.py +0 -0
  16. {anydi-0.69.0 → anydi-0.70.1}/anydi/_module.py +0 -0
  17. {anydi-0.69.0 → anydi-0.70.1}/anydi/_provider.py +0 -0
  18. {anydi-0.69.0 → anydi-0.70.1}/anydi/ext/__init__.py +0 -0
  19. {anydi-0.69.0 → anydi-0.70.1}/anydi/ext/django/__init__.py +0 -0
  20. {anydi-0.69.0 → anydi-0.70.1}/anydi/ext/fastapi.py +0 -0
  21. {anydi-0.69.0 → anydi-0.70.1}/anydi/ext/faststream.py +0 -0
  22. {anydi-0.69.0 → anydi-0.70.1}/anydi/ext/pydantic_settings.py +0 -0
  23. {anydi-0.69.0 → anydi-0.70.1}/anydi/ext/pytest_plugin.py +0 -0
  24. {anydi-0.69.0 → anydi-0.70.1}/anydi/ext/starlette/__init__.py +0 -0
  25. {anydi-0.69.0 → anydi-0.70.1}/anydi/ext/starlette/middleware.py +0 -0
  26. {anydi-0.69.0 → anydi-0.70.1}/anydi/ext/typer.py +0 -0
  27. {anydi-0.69.0 → anydi-0.70.1}/anydi/py.typed +0 -0
  28. {anydi-0.69.0 → anydi-0.70.1}/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.1
4
4
  Summary: Dependency Injection library
5
5
  Keywords: dependency injection,dependencies,di,async,asyncio,application
6
6
  Author: Anton Ruhlov
@@ -25,7 +25,15 @@ from ._module import ModuleDef, ModuleRegistrar
25
25
  from ._provider import Provider, ProviderDef, ProviderKind, ProviderParameter
26
26
  from ._resolver import Resolver
27
27
  from ._scanner import PackageOrIterable, Scanner
28
- from ._types import NOT_SET, Event, Scope, is_event_type, is_iterator_type, is_none_type
28
+ from ._types import (
29
+ NOT_SET,
30
+ Event,
31
+ Scope,
32
+ is_event_type,
33
+ is_iterator_type,
34
+ is_none_type,
35
+ to_list,
36
+ )
29
37
 
30
38
  T = TypeVar("T", bound=Any)
31
39
  P = ParamSpec("P")
@@ -49,6 +57,7 @@ class Container:
49
57
  }
50
58
 
51
59
  self._resources: dict[str, list[Any]] = defaultdict(list)
60
+ self._aliases: dict[Any, Any] = {} # alias_type → canonical_type
52
61
  self._singleton_context = InstanceContext()
53
62
  self._scoped_context: dict[str, ContextVar[InstanceContext]] = {}
54
63
 
@@ -95,6 +104,11 @@ class Container:
95
104
  """Get the registered providers."""
96
105
  return self._providers
97
106
 
107
+ @property
108
+ def aliases(self) -> dict[Any, Any]:
109
+ """Get the registered aliases."""
110
+ return self._aliases
111
+
98
112
  @property
99
113
  def ready(self) -> bool:
100
114
  """Check if the container is ready."""
@@ -339,6 +353,7 @@ class Container:
339
353
  scope: Scope = "singleton",
340
354
  from_context: bool = False,
341
355
  override: bool = False,
356
+ alias: Any = NOT_SET,
342
357
  interface: Any = NOT_SET,
343
358
  call: Callable[..., Any] = NOT_SET,
344
359
  ) -> Provider:
@@ -366,13 +381,40 @@ class Container:
366
381
  dependency_type = interface
367
382
  if factory is NOT_SET:
368
383
  factory = call if call is not NOT_SET else dependency_type
369
- return self._register_provider(
384
+ provider = self._register_provider(
370
385
  dependency_type, factory, scope, from_context, override, None
371
386
  )
372
387
 
388
+ # Register aliases if specified
389
+ for alias_type in to_list(alias):
390
+ self.alias(alias_type, provider.dependency_type)
391
+
392
+ return provider
393
+
394
+ def alias(self, alias_type: Any, canonical_type: Any, /) -> None:
395
+ """Register an alias for a dependency type."""
396
+ if self.ready:
397
+ raise RuntimeError(
398
+ "Cannot register aliases after build() has been called. "
399
+ "All aliases must be registered before building the container."
400
+ )
401
+ if alias_type == canonical_type:
402
+ raise ValueError("Alias type cannot be the same as canonical type.")
403
+ if alias_type in self._aliases:
404
+ raise ValueError(
405
+ f"Alias `{type_repr(alias_type)}` is already registered "
406
+ f"for `{type_repr(self._aliases[alias_type])}`."
407
+ )
408
+ self._aliases[alias_type] = canonical_type
409
+
410
+ def _resolve_alias(self, dependency_type: Any) -> Any:
411
+ """Resolve an alias to its canonical type."""
412
+ return self._aliases.get(dependency_type, dependency_type)
413
+
373
414
  def is_registered(self, dependency_type: Any, /) -> bool:
374
415
  """Check if a provider is registered for the specified dependency type."""
375
- return dependency_type in self._providers
416
+ canonical = self._resolve_alias(dependency_type)
417
+ return canonical in self._providers
376
418
 
377
419
  def has_provider_for(self, dependency_type: Any, /) -> bool:
378
420
  """Check if a provider exists for the specified dependency type."""
@@ -400,12 +442,19 @@ class Container:
400
442
  self._delete_provider(provider)
401
443
 
402
444
  def provider(
403
- self, *, scope: Scope, override: bool = False
445
+ self, *, scope: Scope, override: bool = False, alias: Any = NOT_SET
404
446
  ) -> Callable[[Callable[P, T]], Callable[P, T]]:
405
447
  """Decorator to register a provider function with the specified scope."""
406
448
 
407
449
  def decorator(call: Callable[P, T]) -> Callable[P, T]:
408
- self._register_provider(NOT_SET, call, scope, False, override, None)
450
+ provider = self._register_provider(
451
+ NOT_SET, call, scope, False, override, None
452
+ )
453
+
454
+ # Register aliases if specified
455
+ for alias_type in to_list(alias):
456
+ self.alias(alias_type, provider.dependency_type)
457
+
409
458
  return call
410
459
 
411
460
  return decorator
@@ -570,9 +619,10 @@ class Container:
570
619
  return provider
571
620
 
572
621
  def _get_provider(self, dependency_type: Any) -> Provider:
573
- """Get provider by dependency type."""
622
+ """Get provider by dependency type, resolving aliases."""
623
+ canonical = self._resolve_alias(dependency_type)
574
624
  try:
575
- return self._providers[dependency_type]
625
+ return self._providers[canonical]
576
626
  except KeyError:
577
627
  raise LookupError(
578
628
  f"The provider for `{type_repr(dependency_type)}` has "
@@ -597,6 +647,11 @@ class Container:
597
647
  False,
598
648
  defaults,
599
649
  )
650
+ # Register aliases if specified
651
+ if not self.ready:
652
+ aliases = to_list(dependency_type.__provided__.get("alias"))
653
+ for alias_type in aliases:
654
+ self.alias(alias_type, dependency_type)
600
655
  registered = True
601
656
  else:
602
657
  raise LookupError(
@@ -677,6 +732,10 @@ class Container:
677
732
  False,
678
733
  None,
679
734
  )
735
+ # Register aliases if specified
736
+ aliases = to_list(dependency_type.__provided__.get("alias"))
737
+ for alias_type in aliases:
738
+ self._aliases[alias_type] = dependency_type
680
739
  # Recursively ensure the @provided class is resolved
681
740
  dep_provider = self._ensure_provider_resolved(
682
741
  dep_provider, resolving
@@ -977,6 +1036,10 @@ class Container:
977
1036
  False,
978
1037
  None,
979
1038
  )
1039
+ # Register aliases if specified
1040
+ provided_meta = param_dependency_type.__provided__
1041
+ for alias_type in to_list(provided_meta.get("alias")):
1042
+ self._aliases[alias_type] = param_dependency_type
980
1043
  elif param.has_default:
981
1044
  # Has default, can be missing
982
1045
  resolved_params.append(param)
@@ -29,32 +29,20 @@ 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]: ...
47
-
48
-
49
37
  def provided(
50
- dependency_type: Any = NOT_SET, /, *, scope: Scope, from_context: bool = False
38
+ *, scope: Scope, alias: Any = NOT_SET, from_context: bool = False
51
39
  ) -> Callable[[ClassT], ClassT]:
52
40
  """Decorator for marking a class as provided by AnyDI with a specific scope."""
53
41
 
54
42
  def decorator(cls: ClassT) -> ClassT:
55
43
  metadata: ProvidedMetadata = {"scope": scope}
56
- if dependency_type is not NOT_SET:
57
- metadata["dependency_type"] = dependency_type
44
+ if alias is not NOT_SET:
45
+ metadata["alias"] = alias
58
46
  if from_context:
59
47
  metadata["from_context"] = from_context
60
48
  cls.__provided__ = metadata # type: ignore[attr-defined]
@@ -69,19 +57,19 @@ def singleton(cls: ClassT, /) -> ClassT: ...
69
57
 
70
58
  @overload
71
59
  def singleton(
72
- cls: None = None, /, *, dependency_type: Any = NOT_SET
60
+ cls: None = None, /, *, alias: Any = NOT_SET
73
61
  ) -> Callable[[ClassT], ClassT]: ...
74
62
 
75
63
 
76
64
  def singleton(
77
- cls: ClassT | None = None, /, *, dependency_type: Any = NOT_SET
65
+ cls: ClassT | None = None, /, *, alias: Any = NOT_SET
78
66
  ) -> Callable[[ClassT], ClassT] | ClassT:
79
67
  """Decorator for marking a class as a singleton dependency."""
80
68
 
81
69
  def decorator(c: ClassT) -> ClassT:
82
70
  metadata: ProvidedMetadata = {"scope": "singleton"}
83
- if dependency_type is not NOT_SET:
84
- metadata["dependency_type"] = dependency_type
71
+ if alias is not NOT_SET:
72
+ metadata["alias"] = alias
85
73
  c.__provided__ = metadata # type: ignore[attr-defined]
86
74
  return c
87
75
 
@@ -97,19 +85,19 @@ def transient(cls: ClassT, /) -> ClassT: ...
97
85
 
98
86
  @overload
99
87
  def transient(
100
- cls: None = None, /, *, dependency_type: Any = NOT_SET
88
+ cls: None = None, /, *, alias: Any = NOT_SET
101
89
  ) -> Callable[[ClassT], ClassT]: ...
102
90
 
103
91
 
104
92
  def transient(
105
- cls: ClassT | None = None, /, *, dependency_type: Any = NOT_SET
93
+ cls: ClassT | None = None, /, *, alias: Any = NOT_SET
106
94
  ) -> Callable[[ClassT], ClassT] | ClassT:
107
95
  """Decorator for marking a class as a transient dependency."""
108
96
 
109
97
  def decorator(c: ClassT) -> ClassT:
110
98
  metadata: ProvidedMetadata = {"scope": "transient"}
111
- if dependency_type is not NOT_SET:
112
- metadata["dependency_type"] = dependency_type
99
+ if alias is not NOT_SET:
100
+ metadata["alias"] = alias
113
101
  c.__provided__ = metadata # type: ignore[attr-defined]
114
102
  return c
115
103
 
@@ -125,7 +113,7 @@ def request(cls: ClassT, /, *, from_context: bool = False) -> ClassT: ...
125
113
 
126
114
  @overload
127
115
  def request(
128
- cls: None = None, /, *, dependency_type: Any = NOT_SET, from_context: bool = False
116
+ cls: None = None, /, *, alias: Any = NOT_SET, from_context: bool = False
129
117
  ) -> Callable[[ClassT], ClassT]: ...
130
118
 
131
119
 
@@ -133,15 +121,15 @@ def request(
133
121
  cls: ClassT | None = None,
134
122
  /,
135
123
  *,
136
- dependency_type: Any = NOT_SET,
124
+ alias: Any = NOT_SET,
137
125
  from_context: bool = False,
138
126
  ) -> Callable[[ClassT], ClassT] | ClassT:
139
127
  """Decorator for marking a class as a request-scoped dependency."""
140
128
 
141
129
  def decorator(c: ClassT) -> ClassT:
142
130
  metadata: ProvidedMetadata = {"scope": "request"}
143
- if dependency_type is not NOT_SET:
144
- metadata["dependency_type"] = dependency_type
131
+ if alias is not NOT_SET:
132
+ metadata["alias"] = alias
145
133
  if from_context:
146
134
  metadata["from_context"] = from_context
147
135
  c.__provided__ = metadata # type: ignore[attr-defined]
@@ -164,10 +152,11 @@ def is_provided(cls: Any) -> TypeGuard[type[Provided]]:
164
152
  class ProviderMetadata(TypedDict):
165
153
  scope: Scope
166
154
  override: bool
155
+ alias: NotRequired[Any]
167
156
 
168
157
 
169
158
  def provider(
170
- *, scope: Scope, override: bool = False
159
+ *, scope: Scope, override: bool = False, alias: Any = NOT_SET
171
160
  ) -> Callable[
172
161
  [Callable[Concatenate[ModuleT, P], T]], Callable[Concatenate[ModuleT, P], T]
173
162
  ]:
@@ -176,7 +165,10 @@ def provider(
176
165
  def decorator(
177
166
  target: Callable[Concatenate[ModuleT, P], T],
178
167
  ) -> Callable[Concatenate[ModuleT, P], T]:
179
- target.__provider__ = ProviderMetadata(scope=scope, override=override) # type: ignore
168
+ metadata: ProviderMetadata = {"scope": scope, "override": override}
169
+ if alias is not NOT_SET:
170
+ metadata["alias"] = alias
171
+ target.__provider__ = metadata # type: ignore
180
172
  return target
181
173
 
182
174
  return decorator
@@ -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(
@@ -9,6 +9,7 @@ from types import ModuleType
9
9
  from typing import TYPE_CHECKING, Any
10
10
 
11
11
  from ._decorators import Provided, is_injectable, is_provided
12
+ from ._types import to_list
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from ._container import Container
@@ -58,13 +59,15 @@ class Scanner:
58
59
 
59
60
  # First: register @provided classes
60
61
  for cls in provided_classes:
61
- dependency_type = cls.__provided__.get("dependency_type", cls)
62
- if not self._container.is_registered(dependency_type):
62
+ if not self._container.is_registered(cls):
63
63
  scope = cls.__provided__["scope"]
64
64
  from_context = cls.__provided__.get("from_context", False)
65
65
  self._container.register(
66
- dependency_type, cls, scope=scope, from_context=from_context
66
+ cls, cls, scope=scope, from_context=from_context
67
67
  )
68
+ # Create aliases if specified (alias → cls)
69
+ for alias_type in to_list(cls.__provided__.get("alias")):
70
+ self._container.alias(alias_type, cls)
68
71
 
69
72
  # Second: inject @injectable functions
70
73
  for dependency in injectable_dependencies:
@@ -43,3 +43,12 @@ def is_none_type(tp: Any) -> bool:
43
43
  def is_iterator_type(tp: Any) -> bool:
44
44
  """Check if the given object is an iterator type."""
45
45
  return tp in (Iterator, AsyncIterator)
46
+
47
+
48
+ def to_list(value: Any) -> list[Any]:
49
+ """Convert a value to a list, handling None and sequences."""
50
+ if value is None or value is NOT_SET:
51
+ return []
52
+ if isinstance(value, list | tuple):
53
+ return list(value) # type: ignore[arg-type]
54
+ return [value]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anydi"
3
- version = "0.69.0"
3
+ version = "0.70.1"
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