anydi 0.67.1__py3-none-any.whl → 0.68.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/_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__ = ("_instances", "_stack", "_async_stack", "_lock", "_async_lock")
18
+ __slots__ = ("_items", "_stack", "_async_stack", "_lock", "_async_lock")
19
19
 
20
20
  def __init__(self) -> None:
21
- self._instances: dict[Any, Any] = {}
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, interface: Any, default: Any = NOT_SET) -> Any:
27
+ def get(self, key: Any, default: Any = NOT_SET) -> Any:
28
28
  """Get an instance from the context."""
29
- return self._instances.get(interface, default)
29
+ return self._items.get(key, default)
30
30
 
31
- def set(self, interface: Any, value: Any) -> None:
31
+ def set(self, key: Any, value: Any) -> None:
32
32
  """Set an instance in the context."""
33
- self._instances[interface] = value
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, interface: Any, value: Any) -> None:
48
- self._instances[interface] = value
47
+ def __setitem__(self, key: Any, value: Any) -> None:
48
+ self._items[key] = value
49
49
 
50
- def __getitem__(self, interface: Any) -> Any:
51
- return self._instances[interface]
50
+ def __getitem__(self, key: Any) -> Any:
51
+ return self._items[key]
52
52
 
53
- def __contains__(self, interface: Any) -> bool:
54
- return interface in self._instances
53
+ def __contains__(self, key: Any) -> bool:
54
+ return key in self._items
55
55
 
56
- def __delitem__(self, interface: Any) -> None:
57
- self._instances.pop(interface, None)
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(*, scope: Scope) -> Callable[[ClassT], ClassT]:
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
- cls.__provided__ = ProvidedMetadata(scope=scope)
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
- # Scoped decorators for class-level providers
44
- transient = provided(scope="transient")
45
- request = provided(scope="request")
46
- singleton = provided(scope="singleton")
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
- interface, should_inject, _ = self.validate_parameter(parameter, call=call)
72
+ dependency_type, should_inject, _ = self.validate_parameter(
73
+ parameter, call=call
74
+ )
73
75
  if should_inject:
74
- injected_params[parameter.name] = interface
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
- interface = parameter.annotation
84
+ dependency_type = parameter.annotation
83
85
 
84
86
  marker = parameter.default
85
87
  if not is_marker(marker):
86
- return interface, False, None
88
+ return dependency_type, False, None
87
89
 
88
- if interface is inspect.Parameter.empty:
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 interface
94
- parameter.default.interface = interface
95
+ # Set inject marker dependency type
96
+ parameter.default.dependency_type = dependency_type
95
97
 
96
- if not self.container.has_provider_for(interface):
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(interface)}`."
102
+ f"`{type_repr(dependency_type)}`."
101
103
  )
102
104
 
103
- return interface, True, marker
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__ = ("_interface", "_attrs", "_preferred_owner", "_current_owner")
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, interface: Any = NOT_SET) -> None:
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._interface = interface
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 interface(self) -> Any:
57
- if self._interface is NOT_SET:
58
- raise TypeError("Interface is not set.")
59
- return self._interface
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
- @interface.setter
62
- def interface(self, interface: Any) -> None:
63
- self._interface = interface
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
- if hasattr(_marker_cls, "__class_getitem__"):
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
- call: Callable[..., Any]
51
+ dependency_type: Any
52
+ factory: Callable[..., Any]
49
53
  scope: Scope
50
- interface: Any
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
- @dataclass(frozen=True, slots=True)
72
+
73
+ @dataclass(slots=True)
62
74
  class ProviderDef:
63
- call: Callable[..., Any]
64
- scope: Scope
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