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/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
+ """