hdmi 0.1.0rc3__py3-none-any.whl → 0.2.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.
hdmi/builders/default.py
CHANGED
|
@@ -9,13 +9,11 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Type, get_type_hints
|
|
|
9
9
|
|
|
10
10
|
from hdmi.utils.typing import extract_type_from_optional
|
|
11
11
|
from hdmi.types.definitions import ServiceDefinition
|
|
12
|
-
from hdmi.exceptions import ScopeViolationError
|
|
12
|
+
from hdmi.exceptions import ScopeViolationError, CircularDependencyError
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from hdmi.containers import Container
|
|
16
16
|
|
|
17
|
-
# Removed - no longer using scope hierarchy with boolean flags
|
|
18
|
-
|
|
19
17
|
|
|
20
18
|
class ContainerBuilder:
|
|
21
19
|
"""Mutable builder for configuring dependency injection services.
|
|
@@ -85,12 +83,53 @@ class ContainerBuilder:
|
|
|
85
83
|
"""
|
|
86
84
|
from hdmi.containers import Container
|
|
87
85
|
|
|
86
|
+
# Check for circular dependencies
|
|
87
|
+
self._check_circular_dependencies()
|
|
88
|
+
|
|
88
89
|
# Validate scope hierarchy for all registrations
|
|
89
90
|
self._validate_scopes()
|
|
90
91
|
|
|
91
92
|
# Create and return the validated Container
|
|
92
93
|
return Container(self._definitions)
|
|
93
94
|
|
|
95
|
+
def _check_circular_dependencies(self) -> None:
|
|
96
|
+
"""Check for circular dependencies in the dependency graph.
|
|
97
|
+
|
|
98
|
+
Uses depth-first search to detect cycles. Fails fast on the first cycle found.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
CircularDependencyError: If a circular dependency is detected
|
|
102
|
+
"""
|
|
103
|
+
visited = set()
|
|
104
|
+
path = []
|
|
105
|
+
|
|
106
|
+
def visit(service_type: Type) -> None:
|
|
107
|
+
if service_type in path:
|
|
108
|
+
# Found a cycle - build the cycle path
|
|
109
|
+
cycle_start = path.index(service_type)
|
|
110
|
+
cycle_path = path[cycle_start:] + [service_type]
|
|
111
|
+
path_str = " → ".join(cls.__name__ for cls in cycle_path)
|
|
112
|
+
raise CircularDependencyError(f"Circular dependency detected: {path_str}")
|
|
113
|
+
|
|
114
|
+
if service_type in visited:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
path.append(service_type)
|
|
118
|
+
|
|
119
|
+
# Get dependencies that will be injected
|
|
120
|
+
dependencies = self._get_dependencies(service_type)
|
|
121
|
+
|
|
122
|
+
for dep_type in dependencies.values():
|
|
123
|
+
if dep_type in self._definitions:
|
|
124
|
+
visit(dep_type)
|
|
125
|
+
|
|
126
|
+
path.pop()
|
|
127
|
+
visited.add(service_type)
|
|
128
|
+
|
|
129
|
+
# Visit all registered services
|
|
130
|
+
for service_type in self._definitions:
|
|
131
|
+
visit(service_type)
|
|
132
|
+
|
|
94
133
|
def _validate_scopes(self) -> None:
|
|
95
134
|
"""Validate that scope rules are respected.
|
|
96
135
|
|
|
@@ -104,32 +143,57 @@ class ContainerBuilder:
|
|
|
104
143
|
ScopeViolationError: If a non-scoped service depends on a scoped service
|
|
105
144
|
"""
|
|
106
145
|
for service_type, definition in self._definitions.items():
|
|
107
|
-
# Get dependencies from type annotations
|
|
108
146
|
dependencies = self._get_dependencies(service_type)
|
|
109
147
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if dep_type not in self._definitions:
|
|
148
|
+
for dependency_name, dependency_type in dependencies.items():
|
|
149
|
+
if dependency_type not in self._definitions:
|
|
113
150
|
# Will be caught later by unresolvable dependency check
|
|
114
151
|
continue
|
|
115
152
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
self._validate_scope_compatibility(
|
|
154
|
+
service_type, definition, dependency_type, self._definitions[dependency_type]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def _validate_scope_compatibility(
|
|
158
|
+
self,
|
|
159
|
+
service_type: Type,
|
|
160
|
+
service_definition: ServiceDefinition,
|
|
161
|
+
dependency_type: Type,
|
|
162
|
+
dependency_definition: ServiceDefinition,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Validate that a service's scope is compatible with its dependency's scope.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
service_type: The type of the service being validated
|
|
168
|
+
service_definition: The definition of the service
|
|
169
|
+
dependency_type: The type of the dependency
|
|
170
|
+
dependency_definition: The definition of the dependency
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
ScopeViolationError: If a non-scoped service depends on a scoped service
|
|
174
|
+
"""
|
|
175
|
+
# Non-scoped services cannot depend on scoped services
|
|
176
|
+
if not service_definition.scoped and dependency_definition.scoped:
|
|
177
|
+
service_description = self._format_service_description(service_type, service_definition)
|
|
178
|
+
dependency_description = self._format_service_description(dependency_type, dependency_definition)
|
|
179
|
+
|
|
180
|
+
raise ScopeViolationError(
|
|
181
|
+
f"{service_description} cannot depend on {dependency_description}. "
|
|
182
|
+
f"Non-scoped services cannot depend on scoped services because "
|
|
183
|
+
f"scoped services only exist within a scope context."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def _format_service_description(self, service_type: Type, definition: ServiceDefinition) -> str:
|
|
187
|
+
"""Format a service type and definition for error messages.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
service_type: The service type
|
|
191
|
+
definition: The service definition
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
A formatted string describing the service and its scope
|
|
195
|
+
"""
|
|
196
|
+
return f"{service_type.__name__} (scoped={definition.scoped}, transient={definition.transient})"
|
|
133
197
|
|
|
134
198
|
def _get_dependencies(self, service_type: Type) -> dict[str, Type]:
|
|
135
199
|
"""Get dependencies that will actually be injected.
|
|
@@ -159,29 +223,46 @@ class ContainerBuilder:
|
|
|
159
223
|
if param_name not in hints:
|
|
160
224
|
continue
|
|
161
225
|
|
|
162
|
-
|
|
163
|
-
has_default = param.default is not inspect.Parameter.empty
|
|
164
|
-
|
|
165
|
-
# Extract actual type from Optional/Union types (e.g., Config | None -> Config)
|
|
166
|
-
dependency_type = extract_type_from_optional(type_hint)
|
|
226
|
+
dependency_type = self._extract_dependency_type(hints[param_name])
|
|
167
227
|
if dependency_type is None:
|
|
168
|
-
# Can't determine single type (e.g., Union[A, B] or just None)
|
|
169
228
|
continue
|
|
170
229
|
|
|
171
|
-
|
|
172
|
-
is_registered = dependency_type in self._definitions
|
|
173
|
-
|
|
174
|
-
if has_default:
|
|
175
|
-
# Optional dependency - only include if registered AND autowire=True
|
|
176
|
-
if is_registered:
|
|
177
|
-
dep_definition = self._definitions[dependency_type]
|
|
178
|
-
if dep_definition.autowire:
|
|
179
|
-
# Will be injected - include in dependencies
|
|
180
|
-
dependencies[param_name] = dependency_type
|
|
181
|
-
# else: skip (autowire=False, won't be injected)
|
|
182
|
-
# else: skip (not registered, won't be injected)
|
|
183
|
-
else:
|
|
184
|
-
# Required dependency - always include (will always be injected)
|
|
230
|
+
if self._should_inject_dependency(dependency_type, param.default is not inspect.Parameter.empty):
|
|
185
231
|
dependencies[param_name] = dependency_type
|
|
186
232
|
|
|
187
233
|
return dependencies
|
|
234
|
+
|
|
235
|
+
def _extract_dependency_type(self, type_hint: Type) -> Type | None:
|
|
236
|
+
"""Extract the concrete type from a type hint.
|
|
237
|
+
|
|
238
|
+
Handles Optional types by extracting the non-None type.
|
|
239
|
+
Returns None if no single concrete type can be determined.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
type_hint: The type annotation to process
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
The extracted concrete type, or None if not determinable
|
|
246
|
+
"""
|
|
247
|
+
return extract_type_from_optional(type_hint)
|
|
248
|
+
|
|
249
|
+
def _should_inject_dependency(self, dependency_type: Type, is_optional: bool) -> bool:
|
|
250
|
+
"""Determine if a dependency should be injected.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
dependency_type: The type of the dependency
|
|
254
|
+
is_optional: Whether the parameter has a default value
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
True if the dependency should be injected, False otherwise
|
|
258
|
+
"""
|
|
259
|
+
if not is_optional:
|
|
260
|
+
# Required dependencies are always injected
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
# Optional dependencies need to be registered AND have autowire=True
|
|
264
|
+
if dependency_type not in self._definitions:
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
definition = self._definitions[dependency_type]
|
|
268
|
+
return definition.autowire
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hdmi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A dependency injection framework for Python with dynamic late-binding resolution
|
|
5
5
|
Author: Romain Dorgueil
|
|
6
6
|
Author-email: Romain Dorgueil <romain@makersquad.fr>
|
|
@@ -41,6 +41,11 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
|
|
42
42
|
# hdmi - Dependency Management Interface
|
|
43
43
|
|
|
44
|
+
> **Warning: Pre-Alpha Software**
|
|
45
|
+
>
|
|
46
|
+
> hdmi is experimental software in active development. Breaking changes may occur
|
|
47
|
+
> until version 1.0. Use with care in production environments.
|
|
48
|
+
|
|
44
49
|
A lightweight dependency injection framework for Python 3.13+ with:
|
|
45
50
|
|
|
46
51
|
- **Type-driven dependency discovery** - Uses Python's standard type annotations
|
|
@@ -53,6 +58,7 @@ A lightweight dependency injection framework for Python 3.13+ with:
|
|
|
53
58
|
### Simple Example (Singleton Services)
|
|
54
59
|
|
|
55
60
|
```python
|
|
61
|
+
import asyncio
|
|
56
62
|
from hdmi import ContainerBuilder
|
|
57
63
|
|
|
58
64
|
# Define your services
|
|
@@ -68,34 +74,43 @@ class UserService:
|
|
|
68
74
|
def __init__(self, repo: UserRepository):
|
|
69
75
|
self.repo = repo
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
builder
|
|
74
|
-
builder.register(
|
|
75
|
-
builder.register(
|
|
77
|
+
async def main():
|
|
78
|
+
# Configure the container (all singletons by default)
|
|
79
|
+
builder = ContainerBuilder()
|
|
80
|
+
builder.register(DatabaseConnection)
|
|
81
|
+
builder.register(UserRepository)
|
|
82
|
+
builder.register(UserService)
|
|
76
83
|
|
|
77
|
-
# Build validates the dependency graph
|
|
78
|
-
container = builder.build()
|
|
84
|
+
# Build validates the dependency graph
|
|
85
|
+
container = builder.build()
|
|
79
86
|
|
|
80
|
-
# Resolve services lazily - dependencies are auto-wired!
|
|
81
|
-
user_service = container.get(UserService)
|
|
87
|
+
# Resolve services lazily - dependencies are auto-wired!
|
|
88
|
+
user_service = await container.get(UserService)
|
|
89
|
+
|
|
90
|
+
asyncio.run(main())
|
|
82
91
|
```
|
|
83
92
|
|
|
84
93
|
### Using Scoped Services
|
|
85
94
|
|
|
86
95
|
```python
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
import asyncio
|
|
97
|
+
from hdmi import ContainerBuilder
|
|
98
|
+
|
|
99
|
+
async def main():
|
|
100
|
+
# For request-scoped services (e.g., web requests)
|
|
101
|
+
builder = ContainerBuilder()
|
|
102
|
+
builder.register(DatabaseConnection) # singleton (default)
|
|
103
|
+
builder.register(UserRepository, scoped=True) # One per request
|
|
104
|
+
builder.register(UserService, transient=True) # New each time
|
|
105
|
+
|
|
106
|
+
container = builder.build()
|
|
92
107
|
|
|
93
|
-
|
|
108
|
+
# Scoped services must be resolved within a scope context
|
|
109
|
+
async with container.scope() as scoped:
|
|
110
|
+
user_service = await scoped.get(UserService)
|
|
111
|
+
# All scoped dependencies share the same instance within this scope
|
|
94
112
|
|
|
95
|
-
|
|
96
|
-
with container.scope() as scoped:
|
|
97
|
-
user_service = scoped.get(UserService)
|
|
98
|
-
# All scoped dependencies share the same instance within this scope
|
|
113
|
+
asyncio.run(main())
|
|
99
114
|
```
|
|
100
115
|
|
|
101
116
|
## Key Features
|
|
@@ -118,12 +133,12 @@ Services have four lifecycles that are validated at build time:
|
|
|
118
133
|
The only invalid dependency is when a non-scoped service (singleton or transient) depends on a scoped service.
|
|
119
134
|
|
|
120
135
|
```python
|
|
121
|
-
#
|
|
136
|
+
# Valid: Scoped -> Singleton
|
|
122
137
|
builder = ContainerBuilder()
|
|
123
138
|
builder.register(DatabaseConnection) # singleton (default)
|
|
124
139
|
builder.register(UserRepository, scoped=True)
|
|
125
140
|
|
|
126
|
-
#
|
|
141
|
+
# Invalid: Singleton -> Scoped (raises ScopeViolationError)
|
|
127
142
|
builder = ContainerBuilder()
|
|
128
143
|
builder.register(RequestHandler, scoped=True)
|
|
129
144
|
builder.register(SingletonService) # singleton depends on scoped!
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
hdmi/__init__.py,sha256=bUVZmkoCG1PgkbS7XrwwXylowF56s7aS_mCA-yHDWu4,748
|
|
2
2
|
hdmi/builders/__init__.py,sha256=uN2EzCY9iFzmcxWrqikw5WDFvr_HuD2PxxYV3C43eKs,293
|
|
3
|
-
hdmi/builders/default.py,sha256=
|
|
3
|
+
hdmi/builders/default.py,sha256=Oyiz23IZs0Ks1rLYDZw2D6Sl-pQVxkJpcTwThIu7qHQ,10140
|
|
4
4
|
hdmi/containers/__init__.py,sha256=TD8ReZks3E8CRaThWUzbZmZ4TleSmfMok_5njgNmyew,429
|
|
5
5
|
hdmi/containers/default.py,sha256=jGbOU7a11p2NkWVja6tgBPb_Ngd3DOV-TP0GzhSs7Ds,9782
|
|
6
6
|
hdmi/containers/scoped.py,sha256=c8OlWzQ2oiUxB1fQoB3kcSrGBX8pOw7l_Cg8QfaWzto,4415
|
|
@@ -11,6 +11,6 @@ hdmi/types/containers.py,sha256=pdEdu1QynRUDYhYz7h5EyGpB0H2FD8eCP0dXTKaLu5c,906
|
|
|
11
11
|
hdmi/types/definitions.py,sha256=gqyhBneTex1efrAdBzn7R3bBU8YrOtoZE15cdmQFMTE,1802
|
|
12
12
|
hdmi/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
hdmi/utils/typing.py,sha256=1JdUYZoE0SdX1jci0iGQLJkvBYodImoCQV8S_JcJvyw,1384
|
|
14
|
-
hdmi-0.
|
|
15
|
-
hdmi-0.
|
|
16
|
-
hdmi-0.
|
|
14
|
+
hdmi-0.2.0.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
|
|
15
|
+
hdmi-0.2.0.dist-info/METADATA,sha256=jOpv684i50C6OMUwvvvUlhdjcwHefvDCWktafcZyA-0,5858
|
|
16
|
+
hdmi-0.2.0.dist-info/RECORD,,
|