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/decorators.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Decorator for marking classes as DI components.
|
|
2
|
+
|
|
3
|
+
The @component decorator enables automatic discovery and registration of
|
|
4
|
+
classes with the dependency injection container. Decorated classes are
|
|
5
|
+
found by Container.scan() and registered with their specified lifecycle
|
|
6
|
+
scope (SINGLETON or FACTORY).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|
10
|
+
|
|
11
|
+
from dioxide.scope import Scope
|
|
12
|
+
|
|
13
|
+
T = TypeVar('T')
|
|
14
|
+
|
|
15
|
+
# Global registry for @component decorated classes
|
|
16
|
+
_component_registry: set[type[Any]] = set()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@overload
|
|
20
|
+
def component(cls: type[T]) -> type[T]: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@overload
|
|
24
|
+
def component(
|
|
25
|
+
cls: None = None,
|
|
26
|
+
*,
|
|
27
|
+
scope: Scope = Scope.SINGLETON,
|
|
28
|
+
) -> Any: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def component(
|
|
32
|
+
cls: type[T] | None = None,
|
|
33
|
+
*,
|
|
34
|
+
scope: Scope = Scope.SINGLETON,
|
|
35
|
+
) -> type[T] | Any:
|
|
36
|
+
"""Mark a class as a dependency injection component.
|
|
37
|
+
|
|
38
|
+
This decorator enables automatic discovery and registration with the
|
|
39
|
+
Container. When Container.scan() is called, all @component decorated
|
|
40
|
+
classes are registered with their dependencies automatically resolved
|
|
41
|
+
based on constructor type hints.
|
|
42
|
+
|
|
43
|
+
The decorator can be used with or without parentheses:
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
Basic usage with default SINGLETON scope:
|
|
47
|
+
>>> from dioxide import component, Container
|
|
48
|
+
>>>
|
|
49
|
+
>>> @component
|
|
50
|
+
... class Database:
|
|
51
|
+
... def connect(self):
|
|
52
|
+
... return 'Connected'
|
|
53
|
+
|
|
54
|
+
With explicit SINGLETON scope:
|
|
55
|
+
>>> from dioxide import component, Scope
|
|
56
|
+
>>>
|
|
57
|
+
>>> @component(scope=Scope.SINGLETON)
|
|
58
|
+
... class ConfigService:
|
|
59
|
+
... def __init__(self):
|
|
60
|
+
... self.settings = {'debug': True}
|
|
61
|
+
|
|
62
|
+
With FACTORY scope for per-request instances (old syntax):
|
|
63
|
+
>>> @component(scope=Scope.FACTORY)
|
|
64
|
+
... class RequestHandler:
|
|
65
|
+
... def __init__(self):
|
|
66
|
+
... self.request_id = id(self)
|
|
67
|
+
|
|
68
|
+
With FACTORY scope (MLP attribute syntax):
|
|
69
|
+
>>> @component.factory
|
|
70
|
+
... class RequestHandler:
|
|
71
|
+
... def __init__(self):
|
|
72
|
+
... self.request_id = id(self)
|
|
73
|
+
|
|
74
|
+
With constructor-based dependency injection:
|
|
75
|
+
>>> @component
|
|
76
|
+
... class UserService:
|
|
77
|
+
... def __init__(self, db: Database):
|
|
78
|
+
... self.db = db
|
|
79
|
+
|
|
80
|
+
Auto-discovery and resolution:
|
|
81
|
+
>>> container = Container()
|
|
82
|
+
>>> container.scan() # Discovers all @component classes
|
|
83
|
+
>>> service = container.resolve(UserService)
|
|
84
|
+
>>> assert isinstance(service.db, Database)
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
cls: The class being decorated (provided when used without parentheses).
|
|
88
|
+
scope: Lifecycle scope controlling instance creation and caching.
|
|
89
|
+
Defaults to Scope.SINGLETON. Use Scope.FACTORY for new instances
|
|
90
|
+
on each resolve() call.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The decorated class with dioxide metadata attached. The class can be
|
|
94
|
+
used normally and will be discovered by Container.scan().
|
|
95
|
+
|
|
96
|
+
Note:
|
|
97
|
+
- Dependencies are resolved from constructor (__init__) type hints
|
|
98
|
+
- Classes without __init__ or without type hints are supported
|
|
99
|
+
- The decorator does not modify class behavior, only adds metadata
|
|
100
|
+
- Manual registration takes precedence over decorator-based registration
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def decorator(target_cls: type[T]) -> type[T]:
|
|
104
|
+
# Store DI metadata on the class
|
|
105
|
+
target_cls.__dioxide_scope__ = scope # type: ignore[attr-defined]
|
|
106
|
+
# Add to global registry for auto-discovery
|
|
107
|
+
_component_registry.add(target_cls)
|
|
108
|
+
return target_cls
|
|
109
|
+
|
|
110
|
+
# Support both @component and @component()
|
|
111
|
+
if cls is None:
|
|
112
|
+
# Called with arguments: @component(scope=...)
|
|
113
|
+
return decorator
|
|
114
|
+
else:
|
|
115
|
+
# Called without arguments: @component
|
|
116
|
+
return decorator(cls)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Add factory attribute for MLP API: @component.factory
|
|
120
|
+
def _factory_decorator(cls: type[T]) -> type[T]:
|
|
121
|
+
"""Factory scope decorator for @component.factory syntax.
|
|
122
|
+
|
|
123
|
+
This is the MLP API for creating factory-scoped components.
|
|
124
|
+
Equivalent to @component(scope=Scope.FACTORY) but more ergonomic.
|
|
125
|
+
|
|
126
|
+
Usage:
|
|
127
|
+
>>> @component.factory
|
|
128
|
+
... class RequestHandler:
|
|
129
|
+
... pass
|
|
130
|
+
"""
|
|
131
|
+
# Store DI metadata on the class
|
|
132
|
+
cls.__dioxide_scope__ = Scope.FACTORY # type: ignore[attr-defined]
|
|
133
|
+
# Add to global registry for auto-discovery
|
|
134
|
+
_component_registry.add(cls)
|
|
135
|
+
return cls
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Attach factory attribute to component function
|
|
139
|
+
component.factory = _factory_decorator # type: ignore[attr-defined]
|
|
140
|
+
|
|
141
|
+
# Expose type annotation for static type checkers
|
|
142
|
+
if TYPE_CHECKING:
|
|
143
|
+
# Annotate component.factory for mypy
|
|
144
|
+
component.factory = _factory_decorator # type: ignore[attr-defined]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _get_registered_components() -> set[type[Any]]:
|
|
148
|
+
"""Get all registered component classes.
|
|
149
|
+
|
|
150
|
+
Internal function used by Container.scan() to discover @component
|
|
151
|
+
decorated classes. Returns a copy of the registry to prevent
|
|
152
|
+
external modification.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Set of all classes that have been decorated with @component.
|
|
156
|
+
|
|
157
|
+
Note:
|
|
158
|
+
This is an internal API primarily for testing. Users should
|
|
159
|
+
rely on Container.scan() for component discovery.
|
|
160
|
+
"""
|
|
161
|
+
return _component_registry.copy()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _clear_registry() -> None:
|
|
165
|
+
"""Clear the component registry.
|
|
166
|
+
|
|
167
|
+
Internal function used in test cleanup to reset the global registry
|
|
168
|
+
state between tests. Should not be used in production code.
|
|
169
|
+
|
|
170
|
+
Note:
|
|
171
|
+
This is an internal testing API. Clearing the registry does not
|
|
172
|
+
affect already-configured Container instances.
|
|
173
|
+
"""
|
|
174
|
+
_component_registry.clear()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def implements(
|
|
178
|
+
protocol_class: type[Any],
|
|
179
|
+
*,
|
|
180
|
+
scope: Scope = Scope.SINGLETON,
|
|
181
|
+
) -> Any:
|
|
182
|
+
"""Mark a class as implementing a protocol.
|
|
183
|
+
|
|
184
|
+
This decorator marks a concrete class as an implementation of a protocol,
|
|
185
|
+
enabling protocol-based dependency resolution. The container will register
|
|
186
|
+
the implementation so it can be resolved by the protocol type.
|
|
187
|
+
|
|
188
|
+
Usage:
|
|
189
|
+
Protocol implementation:
|
|
190
|
+
>>> from typing import Protocol
|
|
191
|
+
>>> from dioxide import component
|
|
192
|
+
>>>
|
|
193
|
+
>>> class EmailProvider(Protocol):
|
|
194
|
+
... async def send(self, to: str, subject: str, body: str) -> None: ...
|
|
195
|
+
>>>
|
|
196
|
+
>>> @component.implements(EmailProvider)
|
|
197
|
+
... class SendGridEmail:
|
|
198
|
+
... async def send(self, to: str, subject: str, body: str) -> None:
|
|
199
|
+
... pass # Implementation
|
|
200
|
+
|
|
201
|
+
Multiple implementations with profiles:
|
|
202
|
+
>>> from dioxide import profile
|
|
203
|
+
>>>
|
|
204
|
+
>>> @component.implements(EmailProvider)
|
|
205
|
+
>>> @profile.production
|
|
206
|
+
... class SendGridEmail:
|
|
207
|
+
... async def send(self, to: str, subject: str, body: str) -> None:
|
|
208
|
+
... pass # Real implementation
|
|
209
|
+
>>>
|
|
210
|
+
>>> @component.implements(EmailProvider)
|
|
211
|
+
>>> @profile.test
|
|
212
|
+
... class InMemoryEmail:
|
|
213
|
+
... def __init__(self) -> None:
|
|
214
|
+
... self.sent_emails: list[dict[str, str]] = []
|
|
215
|
+
...
|
|
216
|
+
... async def send(self, to: str, subject: str, body: str) -> None:
|
|
217
|
+
... self.sent_emails.append({'to': to, 'subject': subject, 'body': body})
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
protocol_class: The protocol class that this implementation satisfies.
|
|
221
|
+
scope: Lifecycle scope controlling instance creation and caching.
|
|
222
|
+
Defaults to Scope.SINGLETON. Use Scope.FACTORY for new instances
|
|
223
|
+
on each resolve() call.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
A decorator function that marks the class as implementing the protocol
|
|
227
|
+
and registers it with the container.
|
|
228
|
+
|
|
229
|
+
Note:
|
|
230
|
+
- The implementation class must satisfy the protocol's interface
|
|
231
|
+
- Multiple implementations of the same protocol are allowed
|
|
232
|
+
- Use @profile decorators to select implementations per environment
|
|
233
|
+
- The protocol itself is not registered, only the implementation
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def decorator(target_cls: type[T]) -> type[T]:
|
|
237
|
+
# Store protocol metadata on the class
|
|
238
|
+
target_cls.__dioxide_implements__ = protocol_class # type: ignore[attr-defined]
|
|
239
|
+
# Store DI scope metadata
|
|
240
|
+
target_cls.__dioxide_scope__ = scope # type: ignore[attr-defined]
|
|
241
|
+
# Add to global registry for auto-discovery
|
|
242
|
+
_component_registry.add(target_cls)
|
|
243
|
+
return target_cls
|
|
244
|
+
|
|
245
|
+
return decorator
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# Attach implements as an attribute to component for @component.implements() syntax
|
|
249
|
+
component.implements = implements # type: ignore[attr-defined]
|
dioxide/profile.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Profile decorator for environment-specific component implementations.
|
|
2
|
+
|
|
3
|
+
The profile system enables "fakes at the seams" testing by allowing different
|
|
4
|
+
implementations of the same protocol for different environments (production, test, dev).
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from typing import Protocol
|
|
8
|
+
>>> from dioxide import component, profile
|
|
9
|
+
>>>
|
|
10
|
+
>>> class EmailProvider(Protocol):
|
|
11
|
+
... async def send(self, to: str, subject: str, body: str) -> None: ...
|
|
12
|
+
>>>
|
|
13
|
+
>>> @component.implements(EmailProvider)
|
|
14
|
+
>>> @profile.production
|
|
15
|
+
>>> class SendGridEmail:
|
|
16
|
+
... async def send(self, to: str, subject: str, body: str) -> None:
|
|
17
|
+
... # Real SendGrid implementation
|
|
18
|
+
... pass
|
|
19
|
+
>>>
|
|
20
|
+
>>> @component.implements(EmailProvider)
|
|
21
|
+
>>> @profile.test
|
|
22
|
+
>>> class FakeEmail:
|
|
23
|
+
... def __init__(self) -> None:
|
|
24
|
+
... self.sent_emails: list[dict[str, str]] = []
|
|
25
|
+
...
|
|
26
|
+
... async def send(self, to: str, subject: str, body: str) -> None:
|
|
27
|
+
... self.sent_emails.append({'to': to, 'subject': subject, 'body': body})
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from typing import TYPE_CHECKING, TypeVar, overload
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Callable
|
|
36
|
+
|
|
37
|
+
T = TypeVar('T')
|
|
38
|
+
|
|
39
|
+
# Attribute name for storing profiles on decorated classes
|
|
40
|
+
PROFILE_ATTRIBUTE = '__dioxide_profiles__'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Profile:
|
|
44
|
+
"""Profile decorator for marking components with environment profiles.
|
|
45
|
+
|
|
46
|
+
Supports both attribute access (@profile.production) and callable syntax
|
|
47
|
+
(@profile("prod", "staging")) for maximum flexibility.
|
|
48
|
+
|
|
49
|
+
Pre-defined profiles:
|
|
50
|
+
- production: Production environment
|
|
51
|
+
- test: Test environment
|
|
52
|
+
- development: Development environment
|
|
53
|
+
|
|
54
|
+
Custom profiles via __getattr__:
|
|
55
|
+
- @profile.staging
|
|
56
|
+
- @profile.custom_name
|
|
57
|
+
|
|
58
|
+
Multiple profiles:
|
|
59
|
+
- @profile("prod", "staging")
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
production: Decorator for production profile
|
|
63
|
+
test: Decorator for test profile
|
|
64
|
+
development: Decorator for development profile
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self) -> None:
|
|
68
|
+
"""Initialize Profile decorator."""
|
|
69
|
+
# Pre-defined profile decorators
|
|
70
|
+
self.production = self._create_profile_decorator('production')
|
|
71
|
+
self.test = self._create_profile_decorator('test')
|
|
72
|
+
self.development = self._create_profile_decorator('development')
|
|
73
|
+
|
|
74
|
+
def __getattr__(self, name: str) -> Callable[[type[T]], type[T]]:
|
|
75
|
+
"""Support custom profiles via attribute access.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
name: Profile name (e.g., "staging")
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Decorator function for the custom profile
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> @profile.staging
|
|
85
|
+
>>> class StagingService:
|
|
86
|
+
... pass
|
|
87
|
+
"""
|
|
88
|
+
return self._create_profile_decorator(name)
|
|
89
|
+
|
|
90
|
+
@overload
|
|
91
|
+
def __call__(self, cls: type[T], /) -> type[T]: ...
|
|
92
|
+
|
|
93
|
+
@overload
|
|
94
|
+
def __call__(self, *profile_names: str) -> Callable[[type[T]], type[T]]: ...
|
|
95
|
+
|
|
96
|
+
def __call__(self, *args: type[T] | str) -> type[T] | Callable[[type[T]], type[T]]:
|
|
97
|
+
"""Support callable syntax for multiple profiles.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
*args: Either a single class (when used as @profile) or
|
|
101
|
+
multiple profile names (when used as @profile("prod", "staging"))
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Decorated class or decorator function
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: If no profile names provided or empty strings
|
|
108
|
+
TypeError: If profile names are not strings
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> @profile("prod", "staging")
|
|
112
|
+
>>> class SharedService:
|
|
113
|
+
... pass
|
|
114
|
+
"""
|
|
115
|
+
# Check if called with a class directly (e.g., @profile without parens)
|
|
116
|
+
# This shouldn't happen in normal usage, but handle it gracefully
|
|
117
|
+
if len(args) == 1 and isinstance(args[0], type):
|
|
118
|
+
# Default to production if used as bare @profile
|
|
119
|
+
cls = args[0]
|
|
120
|
+
return self._create_profile_decorator('production')(cls)
|
|
121
|
+
|
|
122
|
+
# Called as @profile("name1", "name2", ...)
|
|
123
|
+
if not args:
|
|
124
|
+
raise ValueError('At least one profile name required')
|
|
125
|
+
|
|
126
|
+
# Validate all arguments are strings
|
|
127
|
+
for name in args:
|
|
128
|
+
if not isinstance(name, str):
|
|
129
|
+
raise TypeError('Profile names must be strings')
|
|
130
|
+
if not name:
|
|
131
|
+
raise ValueError('Profile names cannot be empty')
|
|
132
|
+
|
|
133
|
+
# Normalize to lowercase - all args are strings at this point
|
|
134
|
+
normalized_names = [name.lower() for name in args] # type: ignore[union-attr]
|
|
135
|
+
|
|
136
|
+
return self._create_multi_profile_decorator(normalized_names)
|
|
137
|
+
|
|
138
|
+
def _create_profile_decorator(self, profile_name: str) -> Callable[[type[T]], type[T]]:
|
|
139
|
+
"""Create a decorator for a single profile.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
profile_name: Name of the profile
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Decorator function
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def decorator(cls: type[T]) -> type[T]:
|
|
149
|
+
"""Add profile to class metadata.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
cls: Class to decorate
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Same class with profile metadata added
|
|
156
|
+
"""
|
|
157
|
+
# Get existing profiles or create new set
|
|
158
|
+
existing_profiles: frozenset[str] = getattr(cls, PROFILE_ATTRIBUTE, frozenset())
|
|
159
|
+
|
|
160
|
+
# Add new profile (frozenset is immutable, so create new one)
|
|
161
|
+
new_profiles = existing_profiles | {profile_name.lower()}
|
|
162
|
+
|
|
163
|
+
# Store as frozenset (immutable)
|
|
164
|
+
setattr(cls, PROFILE_ATTRIBUTE, frozenset(new_profiles))
|
|
165
|
+
|
|
166
|
+
return cls
|
|
167
|
+
|
|
168
|
+
return decorator
|
|
169
|
+
|
|
170
|
+
def _create_multi_profile_decorator(self, profile_names: list[str]) -> Callable[[type[T]], type[T]]:
|
|
171
|
+
"""Create a decorator for multiple profiles.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
profile_names: List of profile names (already normalized)
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Decorator function
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def decorator(cls: type[T]) -> type[T]:
|
|
181
|
+
"""Add all profiles to class metadata.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
cls: Class to decorate
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Same class with profile metadata added
|
|
188
|
+
"""
|
|
189
|
+
# Get existing profiles or create new set
|
|
190
|
+
existing_profiles: frozenset[str] = getattr(cls, PROFILE_ATTRIBUTE, frozenset())
|
|
191
|
+
|
|
192
|
+
# Add all new profiles (deduplicated by set)
|
|
193
|
+
new_profiles = existing_profiles | set(profile_names)
|
|
194
|
+
|
|
195
|
+
# Store as frozenset (immutable)
|
|
196
|
+
setattr(cls, PROFILE_ATTRIBUTE, frozenset(new_profiles))
|
|
197
|
+
|
|
198
|
+
return cls
|
|
199
|
+
|
|
200
|
+
return decorator
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Global singleton instance for use as decorator
|
|
204
|
+
profile = Profile()
|
|
205
|
+
|
|
206
|
+
__all__ = ['PROFILE_ATTRIBUTE', 'Profile', 'profile']
|
dioxide/py.typed
ADDED
|
File without changes
|
dioxide/scope.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Dependency injection scopes.
|
|
2
|
+
|
|
3
|
+
This module defines the lifecycle scopes available for components in the
|
|
4
|
+
dependency injection container.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Scope(str, Enum):
|
|
11
|
+
"""Component lifecycle scope.
|
|
12
|
+
|
|
13
|
+
Defines how instances of a component are created and cached:
|
|
14
|
+
|
|
15
|
+
- SINGLETON: One shared instance for the lifetime of the container.
|
|
16
|
+
The factory is called once and the result is cached. Subsequent
|
|
17
|
+
resolve() calls return the same instance.
|
|
18
|
+
|
|
19
|
+
- FACTORY: New instance created on each resolve() call. The factory
|
|
20
|
+
is invoked every time the component is requested, creating a fresh
|
|
21
|
+
instance.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> from dioxide import Container, component, Scope
|
|
25
|
+
>>>
|
|
26
|
+
>>> @component # Default: SINGLETON scope
|
|
27
|
+
... class Database:
|
|
28
|
+
... pass
|
|
29
|
+
>>>
|
|
30
|
+
>>> @component(scope=Scope.FACTORY)
|
|
31
|
+
... class RequestHandler:
|
|
32
|
+
... request_id: int = 0
|
|
33
|
+
...
|
|
34
|
+
... def __init__(self):
|
|
35
|
+
... RequestHandler.request_id += 1
|
|
36
|
+
... self.id = RequestHandler.request_id
|
|
37
|
+
>>>
|
|
38
|
+
>>> container = Container()
|
|
39
|
+
>>> container.scan()
|
|
40
|
+
>>>
|
|
41
|
+
>>> # Singleton: same instance every time
|
|
42
|
+
>>> db1 = container.resolve(Database)
|
|
43
|
+
>>> db2 = container.resolve(Database)
|
|
44
|
+
>>> assert db1 is db2
|
|
45
|
+
>>>
|
|
46
|
+
>>> # Factory: new instance every time
|
|
47
|
+
>>> handler1 = container.resolve(RequestHandler)
|
|
48
|
+
>>> handler2 = container.resolve(RequestHandler)
|
|
49
|
+
>>> assert handler1 is not handler2
|
|
50
|
+
>>> assert handler1.id != handler2.id
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
SINGLETON = 'singleton'
|
|
54
|
+
"""One shared instance for the lifetime of the container.
|
|
55
|
+
|
|
56
|
+
The component factory is called once and the result is cached.
|
|
57
|
+
All subsequent resolve() calls return the same instance.
|
|
58
|
+
|
|
59
|
+
Use for:
|
|
60
|
+
- Database connections
|
|
61
|
+
- Configuration objects
|
|
62
|
+
- Services with shared state
|
|
63
|
+
- Expensive-to-create objects
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
FACTORY = 'factory'
|
|
67
|
+
"""New instance created on each resolve() call.
|
|
68
|
+
|
|
69
|
+
The component factory is invoked every time the component is
|
|
70
|
+
requested, creating a fresh instance.
|
|
71
|
+
|
|
72
|
+
Use for:
|
|
73
|
+
- Request handlers
|
|
74
|
+
- Transient data objects
|
|
75
|
+
- Stateful components that shouldn't be shared
|
|
76
|
+
- Objects with per-request lifecycle
|
|
77
|
+
"""
|