dioxide 0.0.2a1__cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.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.
- dioxide/__init__.py +51 -0
- dioxide/_dioxide_core.abi3.so +0 -0
- dioxide/_dioxide_core.pyi +18 -0
- dioxide/container.py +606 -0
- dioxide/decorators.py +249 -0
- dioxide/profile.py +206 -0
- dioxide/py.typed +0 -0
- dioxide/scope.py +77 -0
- dioxide-0.0.2a1.dist-info/METADATA +324 -0
- dioxide-0.0.2a1.dist-info/RECORD +12 -0
- dioxide-0.0.2a1.dist-info/WHEEL +4 -0
- dioxide-0.0.2a1.dist-info/licenses/LICENSE +21 -0
dioxide/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""dioxide: Fast, Rust-backed declarative dependency injection for Python.
|
|
2
|
+
|
|
3
|
+
dioxide is a modern dependency injection framework that combines:
|
|
4
|
+
- Declarative Python API with @component decorators
|
|
5
|
+
- High-performance Rust-backed container implementation
|
|
6
|
+
- Type-safe dependency resolution with IDE autocomplete support
|
|
7
|
+
- Support for SINGLETON and FACTORY component lifecycles
|
|
8
|
+
|
|
9
|
+
Quick Start (using global singleton container):
|
|
10
|
+
>>> from dioxide import container, component
|
|
11
|
+
>>>
|
|
12
|
+
>>> @component
|
|
13
|
+
... class Database:
|
|
14
|
+
... pass
|
|
15
|
+
>>>
|
|
16
|
+
>>> @component
|
|
17
|
+
... class UserService:
|
|
18
|
+
... def __init__(self, db: Database):
|
|
19
|
+
... self.db = db
|
|
20
|
+
>>>
|
|
21
|
+
>>> container.scan()
|
|
22
|
+
>>> service = container.resolve(UserService)
|
|
23
|
+
>>> # Or use bracket syntax:
|
|
24
|
+
>>> service = container[UserService]
|
|
25
|
+
>>> assert isinstance(service.db, Database)
|
|
26
|
+
|
|
27
|
+
Advanced: Creating separate containers for testing isolation:
|
|
28
|
+
>>> from dioxide import Container, component
|
|
29
|
+
>>>
|
|
30
|
+
>>> test_container = Container()
|
|
31
|
+
>>> test_container.scan()
|
|
32
|
+
>>> service = test_container.resolve(UserService)
|
|
33
|
+
|
|
34
|
+
For more information, see the README and documentation.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from .container import Container, container
|
|
38
|
+
from .decorators import _clear_registry, _get_registered_components, component
|
|
39
|
+
from .profile import profile
|
|
40
|
+
from .scope import Scope
|
|
41
|
+
|
|
42
|
+
__version__ = '0.1.0'
|
|
43
|
+
__all__ = [
|
|
44
|
+
'Container',
|
|
45
|
+
'Scope',
|
|
46
|
+
'_clear_registry',
|
|
47
|
+
'_get_registered_components',
|
|
48
|
+
'component',
|
|
49
|
+
'container',
|
|
50
|
+
'profile',
|
|
51
|
+
]
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Type stubs for Rust core module."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar('T')
|
|
7
|
+
|
|
8
|
+
class Container:
|
|
9
|
+
"""Rust-backed container implementation."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None: ...
|
|
12
|
+
def register_instance(self, py_type: type[T], instance: T) -> None: ...
|
|
13
|
+
def register_class(self, py_type: type[T], implementation: type[T]) -> None: ...
|
|
14
|
+
def register_singleton_factory(self, py_type: type[T], factory: Callable[[], T]) -> None: ...
|
|
15
|
+
def register_transient_factory(self, py_type: type[T], factory: Callable[[], T]) -> None: ...
|
|
16
|
+
def resolve(self, py_type: type[T]) -> T: ...
|
|
17
|
+
def is_empty(self) -> bool: ...
|
|
18
|
+
def __len__(self) -> int: ...
|
dioxide/container.py
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"""Dependency injection container.
|
|
2
|
+
|
|
3
|
+
The Container class is the heart of dioxide's dependency injection system.
|
|
4
|
+
It manages component registration, dependency resolution, and lifecycle scopes.
|
|
5
|
+
The container supports both automatic discovery via @component decorators and
|
|
6
|
+
manual registration for fine-grained control.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any, TypeVar, get_type_hints
|
|
12
|
+
|
|
13
|
+
from dioxide._dioxide_core import Container as RustContainer
|
|
14
|
+
|
|
15
|
+
T = TypeVar('T')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Container:
|
|
19
|
+
"""Dependency injection container.
|
|
20
|
+
|
|
21
|
+
The Container manages component registration and dependency resolution
|
|
22
|
+
for your application. It supports both automatic discovery via the
|
|
23
|
+
@component decorator and manual registration for fine-grained control.
|
|
24
|
+
|
|
25
|
+
The container is backed by a high-performance Rust implementation that
|
|
26
|
+
handles provider caching, singleton management, and type resolution.
|
|
27
|
+
|
|
28
|
+
Features:
|
|
29
|
+
- Type-safe dependency resolution with full IDE support
|
|
30
|
+
- Automatic dependency injection based on type hints
|
|
31
|
+
- SINGLETON and FACTORY lifecycle scopes
|
|
32
|
+
- Thread-safe singleton caching (Rust-backed)
|
|
33
|
+
- Automatic discovery via @component decorator
|
|
34
|
+
- Manual registration for non-decorated classes
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
Automatic discovery with @component:
|
|
38
|
+
>>> from dioxide import Container, component
|
|
39
|
+
>>>
|
|
40
|
+
>>> @component
|
|
41
|
+
... class Database:
|
|
42
|
+
... def query(self, sql):
|
|
43
|
+
... return f'Executing: {sql}'
|
|
44
|
+
>>>
|
|
45
|
+
>>> @component
|
|
46
|
+
... class UserService:
|
|
47
|
+
... def __init__(self, db: Database):
|
|
48
|
+
... self.db = db
|
|
49
|
+
>>>
|
|
50
|
+
>>> container = Container()
|
|
51
|
+
>>> container.scan() # Auto-discover @component classes
|
|
52
|
+
>>> service = container.resolve(UserService)
|
|
53
|
+
>>> result = service.db.query('SELECT * FROM users')
|
|
54
|
+
|
|
55
|
+
Manual registration:
|
|
56
|
+
>>> from dioxide import Container
|
|
57
|
+
>>>
|
|
58
|
+
>>> class Config:
|
|
59
|
+
... def __init__(self, env: str):
|
|
60
|
+
... self.env = env
|
|
61
|
+
>>>
|
|
62
|
+
>>> container = Container()
|
|
63
|
+
>>> container.register_singleton(Config, lambda: Config('production'))
|
|
64
|
+
>>> config = container.resolve(Config)
|
|
65
|
+
>>> assert config.env == 'production'
|
|
66
|
+
|
|
67
|
+
Factory scope for per-request objects:
|
|
68
|
+
>>> from dioxide import Container, component, Scope
|
|
69
|
+
>>>
|
|
70
|
+
>>> @component(scope=Scope.FACTORY)
|
|
71
|
+
... class RequestContext:
|
|
72
|
+
... def __init__(self):
|
|
73
|
+
... self.id = id(self)
|
|
74
|
+
>>>
|
|
75
|
+
>>> container = Container()
|
|
76
|
+
>>> container.scan()
|
|
77
|
+
>>> ctx1 = container.resolve(RequestContext)
|
|
78
|
+
>>> ctx2 = container.resolve(RequestContext)
|
|
79
|
+
>>> assert ctx1 is not ctx2 # Different instances
|
|
80
|
+
|
|
81
|
+
Note:
|
|
82
|
+
The container should be created once at application startup and
|
|
83
|
+
reused throughout the application lifecycle. Each container maintains
|
|
84
|
+
its own singleton cache and registration state.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self) -> None:
|
|
88
|
+
"""Initialize a new dependency injection container.
|
|
89
|
+
|
|
90
|
+
Creates a new container with an empty registry. The container is
|
|
91
|
+
ready to accept registrations via scan() for @component classes
|
|
92
|
+
or via manual registration methods.
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> from dioxide import Container
|
|
96
|
+
>>> container = Container()
|
|
97
|
+
>>> assert container.is_empty()
|
|
98
|
+
"""
|
|
99
|
+
self._rust_core = RustContainer()
|
|
100
|
+
|
|
101
|
+
def register_instance(self, component_type: type[T], instance: T) -> None:
|
|
102
|
+
"""Register a pre-created instance for a given type.
|
|
103
|
+
|
|
104
|
+
This method registers an already-instantiated object that will be
|
|
105
|
+
returned whenever the type is resolved. Useful for registering
|
|
106
|
+
configuration objects or external dependencies.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
component_type: The type to register. This is used as the lookup
|
|
110
|
+
key when resolving dependencies.
|
|
111
|
+
instance: The pre-created instance to return for this type. Must
|
|
112
|
+
be an instance of component_type or a compatible type.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
KeyError: If the type is already registered in this container.
|
|
116
|
+
Each type can only be registered once.
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> from dioxide import Container
|
|
120
|
+
>>>
|
|
121
|
+
>>> class Config:
|
|
122
|
+
... def __init__(self, debug: bool):
|
|
123
|
+
... self.debug = debug
|
|
124
|
+
>>>
|
|
125
|
+
>>> container = Container()
|
|
126
|
+
>>> config_instance = Config(debug=True)
|
|
127
|
+
>>> container.register_instance(Config, config_instance)
|
|
128
|
+
>>> resolved = container.resolve(Config)
|
|
129
|
+
>>> assert resolved is config_instance
|
|
130
|
+
>>> assert resolved.debug is True
|
|
131
|
+
"""
|
|
132
|
+
self._rust_core.register_instance(component_type, instance)
|
|
133
|
+
|
|
134
|
+
def register_class(self, component_type: type[T], implementation: type[T]) -> None:
|
|
135
|
+
"""Register a class to instantiate for a given type.
|
|
136
|
+
|
|
137
|
+
Registers a class that will be instantiated with no arguments when
|
|
138
|
+
the type is resolved. The class's __init__ method will be called
|
|
139
|
+
without parameters.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
component_type: The type to register. This is used as the lookup
|
|
143
|
+
key when resolving dependencies.
|
|
144
|
+
implementation: The class to instantiate. Must have a no-argument
|
|
145
|
+
__init__ method (or no __init__ at all).
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
KeyError: If the type is already registered in this container.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> from dioxide import Container
|
|
152
|
+
>>>
|
|
153
|
+
>>> class DatabaseConnection:
|
|
154
|
+
... def __init__(self):
|
|
155
|
+
... self.connected = True
|
|
156
|
+
>>>
|
|
157
|
+
>>> container = Container()
|
|
158
|
+
>>> container.register_class(DatabaseConnection, DatabaseConnection)
|
|
159
|
+
>>> db = container.resolve(DatabaseConnection)
|
|
160
|
+
>>> assert db.connected is True
|
|
161
|
+
|
|
162
|
+
Note:
|
|
163
|
+
For classes requiring constructor arguments, use
|
|
164
|
+
register_singleton_factory() or register_transient_factory()
|
|
165
|
+
with a lambda that provides the arguments.
|
|
166
|
+
"""
|
|
167
|
+
self._rust_core.register_class(component_type, implementation)
|
|
168
|
+
|
|
169
|
+
def register_singleton_factory(self, component_type: type[T], factory: Callable[[], T]) -> None:
|
|
170
|
+
"""Register a singleton factory function for a given type.
|
|
171
|
+
|
|
172
|
+
The factory will be called once when the type is first resolved,
|
|
173
|
+
and the result will be cached. All subsequent resolve() calls for
|
|
174
|
+
this type will return the same cached instance.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
component_type: The type to register. This is used as the lookup
|
|
178
|
+
key when resolving dependencies.
|
|
179
|
+
factory: A callable that takes no arguments and returns an instance
|
|
180
|
+
of component_type. Called exactly once, on first resolve().
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
KeyError: If the type is already registered in this container.
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> from dioxide import Container
|
|
187
|
+
>>>
|
|
188
|
+
>>> class ExpensiveService:
|
|
189
|
+
... def __init__(self, config_path: str):
|
|
190
|
+
... self.config_path = config_path
|
|
191
|
+
... self.initialized = True
|
|
192
|
+
>>>
|
|
193
|
+
>>> container = Container()
|
|
194
|
+
>>> container.register_singleton_factory(ExpensiveService, lambda: ExpensiveService('/etc/config.yaml'))
|
|
195
|
+
>>> service1 = container.resolve(ExpensiveService)
|
|
196
|
+
>>> service2 = container.resolve(ExpensiveService)
|
|
197
|
+
>>> assert service1 is service2 # Same instance
|
|
198
|
+
|
|
199
|
+
Note:
|
|
200
|
+
This is the recommended registration method for most services,
|
|
201
|
+
as it provides lazy initialization and instance sharing.
|
|
202
|
+
"""
|
|
203
|
+
self._rust_core.register_singleton_factory(component_type, factory)
|
|
204
|
+
|
|
205
|
+
def register_transient_factory(self, component_type: type[T], factory: Callable[[], T]) -> None:
|
|
206
|
+
"""Register a transient factory function for a given type.
|
|
207
|
+
|
|
208
|
+
The factory will be called every time the type is resolved, creating
|
|
209
|
+
a new instance for each resolve() call. Use this for stateful objects
|
|
210
|
+
that should not be shared.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
component_type: The type to register. This is used as the lookup
|
|
214
|
+
key when resolving dependencies.
|
|
215
|
+
factory: A callable that takes no arguments and returns an instance
|
|
216
|
+
of component_type. Called on every resolve() to create a fresh
|
|
217
|
+
instance.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
KeyError: If the type is already registered in this container.
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
>>> from dioxide import Container
|
|
224
|
+
>>>
|
|
225
|
+
>>> class RequestHandler:
|
|
226
|
+
... _counter = 0
|
|
227
|
+
...
|
|
228
|
+
... def __init__(self):
|
|
229
|
+
... RequestHandler._counter += 1
|
|
230
|
+
... self.request_id = RequestHandler._counter
|
|
231
|
+
>>>
|
|
232
|
+
>>> container = Container()
|
|
233
|
+
>>> container.register_transient_factory(RequestHandler, lambda: RequestHandler())
|
|
234
|
+
>>> handler1 = container.resolve(RequestHandler)
|
|
235
|
+
>>> handler2 = container.resolve(RequestHandler)
|
|
236
|
+
>>> assert handler1 is not handler2 # Different instances
|
|
237
|
+
>>> assert handler1.request_id != handler2.request_id
|
|
238
|
+
|
|
239
|
+
Note:
|
|
240
|
+
Use this for objects with per-request or per-operation lifecycle.
|
|
241
|
+
For shared services, use register_singleton_factory() instead.
|
|
242
|
+
"""
|
|
243
|
+
self._rust_core.register_transient_factory(component_type, factory)
|
|
244
|
+
|
|
245
|
+
def register_singleton(self, component_type: type[T], factory: Callable[[], T]) -> None:
|
|
246
|
+
"""Register a singleton provider manually.
|
|
247
|
+
|
|
248
|
+
Convenience method that calls register_singleton_factory(). The factory
|
|
249
|
+
will be called once when the type is first resolved, and the result
|
|
250
|
+
will be cached for the lifetime of the container.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
component_type: The type to register. This is used as the lookup
|
|
254
|
+
key when resolving dependencies.
|
|
255
|
+
factory: A callable that takes no arguments and returns an instance
|
|
256
|
+
of component_type. Called exactly once, on first resolve().
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
KeyError: If the type is already registered in this container.
|
|
260
|
+
|
|
261
|
+
Example:
|
|
262
|
+
>>> from dioxide import Container
|
|
263
|
+
>>>
|
|
264
|
+
>>> class Config:
|
|
265
|
+
... def __init__(self, db_url: str):
|
|
266
|
+
... self.db_url = db_url
|
|
267
|
+
>>>
|
|
268
|
+
>>> container = Container()
|
|
269
|
+
>>> container.register_singleton(Config, lambda: Config('postgresql://localhost'))
|
|
270
|
+
>>> config = container.resolve(Config)
|
|
271
|
+
>>> assert config.db_url == 'postgresql://localhost'
|
|
272
|
+
|
|
273
|
+
Note:
|
|
274
|
+
This is an alias for register_singleton_factory() provided for
|
|
275
|
+
convenience and clarity.
|
|
276
|
+
"""
|
|
277
|
+
self.register_singleton_factory(component_type, factory)
|
|
278
|
+
|
|
279
|
+
def register_factory(self, component_type: type[T], factory: Callable[[], T]) -> None:
|
|
280
|
+
"""Register a transient (factory) provider manually.
|
|
281
|
+
|
|
282
|
+
Convenience method that calls register_transient_factory(). The factory
|
|
283
|
+
will be called every time the type is resolved, creating a new instance
|
|
284
|
+
for each resolve() call.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
component_type: The type to register. This is used as the lookup
|
|
288
|
+
key when resolving dependencies.
|
|
289
|
+
factory: A callable that takes no arguments and returns an instance
|
|
290
|
+
of component_type. Called on every resolve() to create a fresh
|
|
291
|
+
instance.
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
KeyError: If the type is already registered in this container.
|
|
295
|
+
|
|
296
|
+
Example:
|
|
297
|
+
>>> from dioxide import Container
|
|
298
|
+
>>>
|
|
299
|
+
>>> class Transaction:
|
|
300
|
+
... _id_counter = 0
|
|
301
|
+
...
|
|
302
|
+
... def __init__(self):
|
|
303
|
+
... Transaction._id_counter += 1
|
|
304
|
+
... self.tx_id = Transaction._id_counter
|
|
305
|
+
>>>
|
|
306
|
+
>>> container = Container()
|
|
307
|
+
>>> container.register_factory(Transaction, lambda: Transaction())
|
|
308
|
+
>>> tx1 = container.resolve(Transaction)
|
|
309
|
+
>>> tx2 = container.resolve(Transaction)
|
|
310
|
+
>>> assert tx1.tx_id != tx2.tx_id # Different instances
|
|
311
|
+
|
|
312
|
+
Note:
|
|
313
|
+
This is an alias for register_transient_factory() provided for
|
|
314
|
+
convenience and clarity.
|
|
315
|
+
"""
|
|
316
|
+
self.register_transient_factory(component_type, factory)
|
|
317
|
+
|
|
318
|
+
def resolve(self, component_type: type[T]) -> T:
|
|
319
|
+
"""Resolve a component instance.
|
|
320
|
+
|
|
321
|
+
Retrieves or creates an instance of the requested type based on its
|
|
322
|
+
registration. For singletons, returns the cached instance (creating
|
|
323
|
+
it on first call). For factories, creates a new instance every time.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
component_type: The type to resolve. Must have been previously
|
|
327
|
+
registered via scan() or manual registration methods.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
An instance of the requested type. For SINGLETON scope, the same
|
|
331
|
+
instance is returned on every call. For FACTORY scope, a new
|
|
332
|
+
instance is created on each call.
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
KeyError: If the type is not registered in this container.
|
|
336
|
+
|
|
337
|
+
Example:
|
|
338
|
+
>>> from dioxide import Container, component
|
|
339
|
+
>>>
|
|
340
|
+
>>> @component
|
|
341
|
+
... class Logger:
|
|
342
|
+
... def log(self, msg: str):
|
|
343
|
+
... print(f'LOG: {msg}')
|
|
344
|
+
>>>
|
|
345
|
+
>>> @component
|
|
346
|
+
... class Application:
|
|
347
|
+
... def __init__(self, logger: Logger):
|
|
348
|
+
... self.logger = logger
|
|
349
|
+
>>>
|
|
350
|
+
>>> container = Container()
|
|
351
|
+
>>> container.scan()
|
|
352
|
+
>>> app = container.resolve(Application)
|
|
353
|
+
>>> app.logger.log('Application started')
|
|
354
|
+
|
|
355
|
+
Note:
|
|
356
|
+
Type annotations in constructors enable automatic dependency
|
|
357
|
+
injection. The container recursively resolves all dependencies.
|
|
358
|
+
"""
|
|
359
|
+
return self._rust_core.resolve(component_type)
|
|
360
|
+
|
|
361
|
+
def __getitem__(self, component_type: type[T]) -> T:
|
|
362
|
+
"""Resolve a component using bracket syntax.
|
|
363
|
+
|
|
364
|
+
Provides an alternative, more Pythonic syntax for resolving components.
|
|
365
|
+
This method is equivalent to calling resolve() and simply delegates to it.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
component_type: The type to resolve. Must have been previously
|
|
369
|
+
registered via scan() or manual registration methods.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
An instance of the requested type. For SINGLETON scope, the same
|
|
373
|
+
instance is returned on every call. For FACTORY scope, a new
|
|
374
|
+
instance is created on each call.
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
KeyError: If the type is not registered in this container.
|
|
378
|
+
|
|
379
|
+
Example:
|
|
380
|
+
>>> from dioxide import container, component
|
|
381
|
+
>>>
|
|
382
|
+
>>> @component
|
|
383
|
+
... class Logger:
|
|
384
|
+
... def log(self, msg: str):
|
|
385
|
+
... print(f'LOG: {msg}')
|
|
386
|
+
>>>
|
|
387
|
+
>>> container.scan()
|
|
388
|
+
>>> logger = container[Logger] # Bracket syntax
|
|
389
|
+
>>> logger.log('Using bracket notation')
|
|
390
|
+
|
|
391
|
+
Note:
|
|
392
|
+
This is purely a convenience method. Both container[Type] and
|
|
393
|
+
container.resolve(Type) work identically and return the same
|
|
394
|
+
instance for singleton-scoped components.
|
|
395
|
+
"""
|
|
396
|
+
return self.resolve(component_type)
|
|
397
|
+
|
|
398
|
+
def is_empty(self) -> bool:
|
|
399
|
+
"""Check if container has no registered providers.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
True if no types have been registered, False if at least one
|
|
403
|
+
type has been registered.
|
|
404
|
+
|
|
405
|
+
Example:
|
|
406
|
+
>>> from dioxide import Container
|
|
407
|
+
>>>
|
|
408
|
+
>>> container = Container()
|
|
409
|
+
>>> assert container.is_empty()
|
|
410
|
+
>>>
|
|
411
|
+
>>> container.scan() # Register @component classes
|
|
412
|
+
>>> # If any @component classes exist, container is no longer empty
|
|
413
|
+
"""
|
|
414
|
+
return self._rust_core.is_empty()
|
|
415
|
+
|
|
416
|
+
def __len__(self) -> int:
|
|
417
|
+
"""Get count of registered providers.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
The number of types that have been registered in this container.
|
|
421
|
+
|
|
422
|
+
Example:
|
|
423
|
+
>>> from dioxide import Container, component
|
|
424
|
+
>>>
|
|
425
|
+
>>> @component
|
|
426
|
+
... class ServiceA:
|
|
427
|
+
... pass
|
|
428
|
+
>>>
|
|
429
|
+
>>> @component
|
|
430
|
+
... class ServiceB:
|
|
431
|
+
... pass
|
|
432
|
+
>>>
|
|
433
|
+
>>> container = Container()
|
|
434
|
+
>>> assert len(container) == 0
|
|
435
|
+
>>> container.scan()
|
|
436
|
+
>>> assert len(container) == 2
|
|
437
|
+
"""
|
|
438
|
+
return len(self._rust_core)
|
|
439
|
+
|
|
440
|
+
def scan(self, package: str | None = None, profile: str | None = None) -> None:
|
|
441
|
+
"""Discover and register all @component decorated classes.
|
|
442
|
+
|
|
443
|
+
Scans the global component registry for all classes decorated with
|
|
444
|
+
@component and registers them with the container. Dependencies are
|
|
445
|
+
automatically resolved based on constructor type hints.
|
|
446
|
+
|
|
447
|
+
This is the primary method for setting up the container in a
|
|
448
|
+
declarative style. Call it once after all components are imported.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
package: Optional package name to scan. If None, scans all registered
|
|
452
|
+
components. If provided, only scans components from the specified
|
|
453
|
+
package. (Not yet implemented - reserved for future use)
|
|
454
|
+
profile: Optional profile name to filter components. If None, registers
|
|
455
|
+
all components regardless of profile. If provided, only registers
|
|
456
|
+
components that have the matching profile in their __dioxide_profiles__
|
|
457
|
+
attribute. Profile names are normalized to lowercase for matching.
|
|
458
|
+
|
|
459
|
+
Registration behavior:
|
|
460
|
+
- SINGLETON scope (default): Creates singleton factory with caching
|
|
461
|
+
- FACTORY scope: Creates transient factory for new instances
|
|
462
|
+
- Manual registrations take precedence over @component decorators
|
|
463
|
+
- Already-registered types are silently skipped
|
|
464
|
+
- Profile filtering applies to components with @profile decorator
|
|
465
|
+
|
|
466
|
+
Example:
|
|
467
|
+
>>> from dioxide import Container, component, Scope, profile
|
|
468
|
+
>>>
|
|
469
|
+
>>> @component
|
|
470
|
+
... class Database:
|
|
471
|
+
... def __init__(self):
|
|
472
|
+
... self.connected = True
|
|
473
|
+
>>>
|
|
474
|
+
>>> @component
|
|
475
|
+
... class UserRepository:
|
|
476
|
+
... def __init__(self, db: Database):
|
|
477
|
+
... self.db = db
|
|
478
|
+
>>>
|
|
479
|
+
>>> @component(scope=Scope.FACTORY)
|
|
480
|
+
>>> @profile.production
|
|
481
|
+
... class RequestHandler:
|
|
482
|
+
... def __init__(self, repo: UserRepository):
|
|
483
|
+
... self.repo = repo
|
|
484
|
+
>>>
|
|
485
|
+
>>> container = Container()
|
|
486
|
+
>>> container.scan() # Scans all components
|
|
487
|
+
>>>
|
|
488
|
+
>>> # Or with profile filtering
|
|
489
|
+
>>> prod_container = Container()
|
|
490
|
+
>>> prod_container.scan(profile='production') # Only production components
|
|
491
|
+
|
|
492
|
+
Note:
|
|
493
|
+
- Ensure all component classes are imported before calling scan()
|
|
494
|
+
- Constructor dependencies must have type hints
|
|
495
|
+
- Circular dependencies will cause infinite recursion
|
|
496
|
+
- Manual registrations (register_*) take precedence over scan()
|
|
497
|
+
- Profile names are case-insensitive (normalized to lowercase)
|
|
498
|
+
"""
|
|
499
|
+
from dioxide.decorators import _get_registered_components
|
|
500
|
+
from dioxide.profile import PROFILE_ATTRIBUTE
|
|
501
|
+
from dioxide.scope import Scope
|
|
502
|
+
|
|
503
|
+
# Normalize profile to lowercase if provided
|
|
504
|
+
normalized_profile = profile.lower() if profile else None
|
|
505
|
+
|
|
506
|
+
for component_class in _get_registered_components():
|
|
507
|
+
# Apply profile filtering if profile parameter provided
|
|
508
|
+
if normalized_profile is not None:
|
|
509
|
+
# Get component's profiles (if any)
|
|
510
|
+
component_profiles: frozenset[str] = getattr(component_class, PROFILE_ATTRIBUTE, frozenset())
|
|
511
|
+
|
|
512
|
+
# Skip if component doesn't have the requested profile
|
|
513
|
+
if normalized_profile not in component_profiles:
|
|
514
|
+
continue
|
|
515
|
+
|
|
516
|
+
# Create a factory that auto-injects dependencies
|
|
517
|
+
factory = self._create_auto_injecting_factory(component_class)
|
|
518
|
+
|
|
519
|
+
# Check the scope
|
|
520
|
+
scope = getattr(component_class, '__dioxide_scope__', Scope.SINGLETON)
|
|
521
|
+
|
|
522
|
+
# Check if this class implements a protocol
|
|
523
|
+
protocol_class = getattr(component_class, '__dioxide_implements__', None)
|
|
524
|
+
|
|
525
|
+
# Register the implementation under its concrete type
|
|
526
|
+
try:
|
|
527
|
+
if scope == Scope.SINGLETON:
|
|
528
|
+
# Register as singleton factory (Rust will cache the result)
|
|
529
|
+
self.register_singleton_factory(component_class, factory)
|
|
530
|
+
else:
|
|
531
|
+
# Register as transient factory (Rust creates new instance each time)
|
|
532
|
+
self.register_transient_factory(component_class, factory)
|
|
533
|
+
except KeyError:
|
|
534
|
+
# Already registered manually - skip it (manual takes precedence)
|
|
535
|
+
pass
|
|
536
|
+
|
|
537
|
+
# If this class implements a protocol, also register it under the protocol type
|
|
538
|
+
# IMPORTANT: For singleton scope, both protocol and concrete class must resolve
|
|
539
|
+
# to the same instance. We achieve this by creating a factory that resolves
|
|
540
|
+
# the concrete class (which is already cached by Rust if singleton).
|
|
541
|
+
if protocol_class is not None:
|
|
542
|
+
# Create a factory that resolves via the concrete class
|
|
543
|
+
# This ensures singleton instances are shared between protocol and concrete type
|
|
544
|
+
def create_protocol_factory(impl_class: type[Any]) -> Callable[[], Any]:
|
|
545
|
+
"""Create factory that resolves the concrete implementation."""
|
|
546
|
+
return lambda: self.resolve(impl_class)
|
|
547
|
+
|
|
548
|
+
protocol_factory = create_protocol_factory(component_class)
|
|
549
|
+
try:
|
|
550
|
+
if scope == Scope.SINGLETON:
|
|
551
|
+
self.register_singleton_factory(protocol_class, protocol_factory)
|
|
552
|
+
else:
|
|
553
|
+
self.register_transient_factory(protocol_class, protocol_factory)
|
|
554
|
+
except KeyError:
|
|
555
|
+
# Protocol already has an implementation registered - skip it
|
|
556
|
+
# (This will happen with multiple implementations - we'll handle
|
|
557
|
+
# profile-based selection in a future iteration)
|
|
558
|
+
pass
|
|
559
|
+
|
|
560
|
+
def _create_auto_injecting_factory(self, cls: type[T]) -> Callable[[], T]:
|
|
561
|
+
"""Create a factory function that auto-injects dependencies from type hints.
|
|
562
|
+
|
|
563
|
+
Internal method used by scan() to create factory functions that
|
|
564
|
+
automatically resolve constructor dependencies and instantiate classes.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
cls: The class to create a factory for. Must be a class type.
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
A factory function that:
|
|
571
|
+
- Inspects the class's __init__ type hints
|
|
572
|
+
- Resolves each dependency from the container
|
|
573
|
+
- Instantiates the class with resolved dependencies
|
|
574
|
+
- Returns the fully-constructed instance
|
|
575
|
+
|
|
576
|
+
Note:
|
|
577
|
+
- If the class has no __init__ or no type hints, returns the class itself
|
|
578
|
+
- Only parameters with type hints are resolved from the container
|
|
579
|
+
- Parameters without type hints are skipped (not passed to __init__)
|
|
580
|
+
"""
|
|
581
|
+
try:
|
|
582
|
+
init_signature = inspect.signature(cls.__init__)
|
|
583
|
+
# Pass the class's module globals to resolve forward references
|
|
584
|
+
type_hints = get_type_hints(cls.__init__, globalns=cls.__init__.__globals__)
|
|
585
|
+
except (ValueError, AttributeError, NameError):
|
|
586
|
+
# No __init__ or no type hints, or can't resolve type hints - just instantiate directly
|
|
587
|
+
return cls
|
|
588
|
+
|
|
589
|
+
# Build factory that resolves dependencies
|
|
590
|
+
def factory() -> T:
|
|
591
|
+
kwargs: dict[str, Any] = {}
|
|
592
|
+
for param_name in init_signature.parameters:
|
|
593
|
+
if param_name == 'self':
|
|
594
|
+
continue
|
|
595
|
+
if param_name in type_hints:
|
|
596
|
+
dependency_type = type_hints[param_name]
|
|
597
|
+
kwargs[param_name] = self.resolve(dependency_type)
|
|
598
|
+
return cls(**kwargs)
|
|
599
|
+
|
|
600
|
+
return factory
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# Global singleton container instance for simplified API
|
|
604
|
+
# This provides the MLP-style ergonomic API while keeping Container class
|
|
605
|
+
# available for advanced use cases (testing isolation, multi-tenant apps)
|
|
606
|
+
container: Container = Container()
|