ps-dependency-injection 0.2.9__tar.gz

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.
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: ps-dependency-injection
3
+ Version: 0.2.9
4
+ Summary: Lightweight, thread-safe dependency injection container for Python
5
+ Author: ztBlackGad
6
+ Requires-Python: >=3.10,<3.14
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Project-URL: Homepage, https://github.com/BlackGad/ps-poetry
13
+ Project-URL: Repository, https://github.com/BlackGad/ps-poetry
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Overview
17
+
18
+ PS DI is a lightweight, thread-safe dependency injection container for Python. It provides a `DI` class that manages service registration, resolution, and automatic constructor injection. Registrations support singleton and transient lifetimes, priority-based ordering, and resolution by type or string name.
19
+
20
+ # Installation
21
+
22
+ ```bash
23
+ pip install ps-dependency-injection
24
+ ```
25
+
26
+ Or with Poetry:
27
+
28
+ ```bash
29
+ poetry add ps-dependency-injection
30
+ ```
31
+
32
+ # Quick Start
33
+
34
+ ```python
35
+ from ps.di import DI, Lifetime
36
+
37
+ di = DI()
38
+
39
+ di.register(Logger).factory(Logger, "app")
40
+ di.register(UserRepository, Lifetime.TRANSIENT).implementation(UserRepository)
41
+
42
+ repo = di.resolve(UserRepository)
43
+ ```
44
+
45
+ [View full example](https://github.com/BlackGad/ps-poetry/blob/main/src/examples/ps-dependency-injection/basic_usage_example.py)
46
+
47
+ # Register Services
48
+
49
+ The `register` method accepts a type (or string key), an optional `Lifetime`, and an optional `Priority`. It returns a `Binding` object that configures how the service is created.
50
+
51
+ * `.factory(callable, *args, **kwargs)` — Registers a callable that produces the service. Typed parameters not covered by explicit arguments are resolved from the container at registration time, using the same injection rules as `satisfy`. Explicit positional and keyword arguments take precedence over container resolution.
52
+ * `.implementation(cls)` — Registers a class whose constructor is invoked via `spawn`, allowing the container to inject known dependencies automatically.
53
+
54
+ ```python
55
+ from ps.di import DI, Lifetime
56
+
57
+ di = DI()
58
+
59
+ # Singleton (default) — one shared instance
60
+ di.register(Logger).factory(Logger, "app")
61
+
62
+ # Transient — new instance on every resolve
63
+ di.register(UserRepository, Lifetime.TRANSIENT).implementation(UserRepository)
64
+ ```
65
+
66
+ `Lifetime` values:
67
+
68
+ * `SINGLETON` — The factory is called once; all subsequent resolves return the same instance. This is the default.
69
+ * `TRANSIENT` — The factory is called on every resolve, producing a new instance each time.
70
+
71
+ # Resolve Services
72
+
73
+ Use `resolve` to retrieve the highest-priority registration for a type, or `resolve_many` to retrieve all registrations ordered by priority.
74
+
75
+ ```python
76
+ service = di.resolve(Logger) # Logger | None
77
+ all_loggers = di.resolve_many(Logger) # list[Logger]
78
+ ```
79
+
80
+ `resolve` returns `None` when no registration exists for the requested type. `resolve_many` returns an empty list in that case.
81
+
82
+ Both methods also accept a string type name instead of a class:
83
+
84
+ ```python
85
+ service = di.resolve("Logger")
86
+ ```
87
+
88
+ String resolution matches against the `__name__` attribute of registered types and raises `ValueError` when no match is found.
89
+
90
+ # Priority
91
+
92
+ Each registration carries a `Priority` that determines its position relative to other registrations for the same type. Higher-priority registrations are resolved first by `resolve` and appear earlier in the list returned by `resolve_many`.
93
+
94
+ ```python
95
+ from ps.di import DI, Priority
96
+
97
+ di = DI()
98
+
99
+ di.register(NotificationService, priority=Priority.LOW).factory(NotificationService, "email")
100
+ di.register(NotificationService, priority=Priority.HIGH).factory(NotificationService, "sms")
101
+ di.register(NotificationService, priority=Priority.MEDIUM).factory(NotificationService, "push")
102
+
103
+ primary = di.resolve(NotificationService) # sms (HIGH wins)
104
+ ```
105
+
106
+ [View full example](https://github.com/BlackGad/ps-poetry/blob/main/src/examples/ps-dependency-injection/priority_example.py)
107
+
108
+ `Priority` values: `LOW` (default), `MEDIUM`, `HIGH`. When multiple registrations share the same priority, the most recently registered one wins.
109
+
110
+ # Spawn Objects
111
+
112
+ `spawn` instantiates a class without registering it, injecting constructor dependencies from the container automatically. It inspects type hints on `__init__` parameters and resolves them as follows:
113
+
114
+ * A parameter typed as `DI` or a subclass of `DI` receives the container itself.
115
+ * A parameter typed as `List[T]` receives the result of `resolve_many(T)`.
116
+ * A parameter typed as `Optional[T]` receives the result of `resolve(T)`, falling back to the default value when nothing is registered.
117
+ * Any other typed parameter receives the result of `resolve(T)`. If `resolve` returns `None` and no default exists, `spawn` raises `ValueError`.
118
+
119
+ Positional and keyword arguments passed to `spawn` override automatic resolution:
120
+
121
+ ```python
122
+ from ps.di import DI
123
+
124
+ di = DI()
125
+ di.register(Logger).factory(Logger, "app")
126
+
127
+ repo = di.spawn(UserRepository) # Logger injected from container
128
+ repo = di.spawn(UserRepository, logger=custom_logger) # explicit override
129
+ ```
130
+
131
+ # Satisfy Functions
132
+
133
+ `satisfy` binds a callable to dependencies resolved from the container at the time of the call, returning a new callable that accepts any remaining parameters at invocation time.
134
+
135
+ * Parameters with registered types are resolved from the container automatically.
136
+ * Parameters with defaults fall back to their default values when no registration exists.
137
+ * Parameters typed as `List[T]` receive all registered instances of `T`.
138
+ * Parameters typed as `Optional[T]` receive `None` when no registration exists.
139
+ * Parameters marked with `REQUIRED` are excluded from DI resolution and must be supplied by the caller.
140
+
141
+ ```python
142
+ from ps.di import DI, REQUIRED
143
+
144
+ log_message = di.satisfy(format_log, message=REQUIRED)
145
+
146
+ print(log_message(message="Application started"))
147
+ print(log_message(message="Low disk space", level="WARNING"))
148
+ ```
149
+
150
+ [View full example](https://github.com/BlackGad/ps-poetry/blob/main/src/examples/ps-dependency-injection/satisfy_example.py)
151
+
152
+ The returned callable accepts keyword arguments at invocation time. Any keyword argument passed at invocation time overrides the corresponding resolved value, including DI-resolved parameters.
153
+
154
+ # Scopes
155
+
156
+ `scope()` creates a child `DI` instance that inherits all registrations from the parent but maintains its own isolated registry. This is useful for per-request, per-session, or any other short-lived context that needs additional or overriding registrations without affecting the parent container.
157
+
158
+ Resolution in a scoped container follows these rules:
159
+
160
+ * `resolve` checks the scoped registry first; if nothing is registered, it falls through to the parent.
161
+ * `resolve_many` returns scoped registrations followed by parent registrations, with scoped results first.
162
+ * `spawn` and `satisfy` use the scoped resolver, so injected dependencies prefer scoped registrations.
163
+ * A parameter typed as `DI` receives the scoped instance, not the parent.
164
+
165
+ Scopes support the context manager protocol. Exiting the `with` block clears the scoped registry and releases all singleton instances held by the scope, enabling deterministic cleanup of resources such as database connections or file handles.
166
+
167
+ ```python
168
+ with di.scope() as request_scope:
169
+ request_scope.register(RequestContext).factory(RequestContext, request_id)
170
+ handler = request_scope.spawn(RequestHandler)
171
+ handler.handle()
172
+ # scoped singletons released here; parent container unaffected
173
+ ```
174
+
175
+ [View full example](https://github.com/BlackGad/ps-poetry/blob/main/src/examples/ps-dependency-injection/scope_example.py)
176
+
177
+ The root container supports the same context manager protocol. Exiting a `with di:` block clears all registrations and releases every singleton instance held by the container.
178
+
179
+ Scopes can be nested arbitrarily. Each level sees its own registrations plus all ancestor registrations, with closer scopes taking precedence.
180
+
181
+ # Thread Safety
182
+
183
+ All registration and resolution operations are protected by internal locks. Singleton creation uses double-checked locking so the factory is called exactly once even under concurrent access. Transient registrations produce independent instances per call with no shared mutable state.
184
+
@@ -0,0 +1,168 @@
1
+ # Overview
2
+
3
+ PS DI is a lightweight, thread-safe dependency injection container for Python. It provides a `DI` class that manages service registration, resolution, and automatic constructor injection. Registrations support singleton and transient lifetimes, priority-based ordering, and resolution by type or string name.
4
+
5
+ # Installation
6
+
7
+ ```bash
8
+ pip install ps-dependency-injection
9
+ ```
10
+
11
+ Or with Poetry:
12
+
13
+ ```bash
14
+ poetry add ps-dependency-injection
15
+ ```
16
+
17
+ # Quick Start
18
+
19
+ ```python
20
+ from ps.di import DI, Lifetime
21
+
22
+ di = DI()
23
+
24
+ di.register(Logger).factory(Logger, "app")
25
+ di.register(UserRepository, Lifetime.TRANSIENT).implementation(UserRepository)
26
+
27
+ repo = di.resolve(UserRepository)
28
+ ```
29
+
30
+ [View full example](https://github.com/BlackGad/ps-poetry/blob/main/src/examples/ps-dependency-injection/basic_usage_example.py)
31
+
32
+ # Register Services
33
+
34
+ The `register` method accepts a type (or string key), an optional `Lifetime`, and an optional `Priority`. It returns a `Binding` object that configures how the service is created.
35
+
36
+ * `.factory(callable, *args, **kwargs)` — Registers a callable that produces the service. Typed parameters not covered by explicit arguments are resolved from the container at registration time, using the same injection rules as `satisfy`. Explicit positional and keyword arguments take precedence over container resolution.
37
+ * `.implementation(cls)` — Registers a class whose constructor is invoked via `spawn`, allowing the container to inject known dependencies automatically.
38
+
39
+ ```python
40
+ from ps.di import DI, Lifetime
41
+
42
+ di = DI()
43
+
44
+ # Singleton (default) — one shared instance
45
+ di.register(Logger).factory(Logger, "app")
46
+
47
+ # Transient — new instance on every resolve
48
+ di.register(UserRepository, Lifetime.TRANSIENT).implementation(UserRepository)
49
+ ```
50
+
51
+ `Lifetime` values:
52
+
53
+ * `SINGLETON` — The factory is called once; all subsequent resolves return the same instance. This is the default.
54
+ * `TRANSIENT` — The factory is called on every resolve, producing a new instance each time.
55
+
56
+ # Resolve Services
57
+
58
+ Use `resolve` to retrieve the highest-priority registration for a type, or `resolve_many` to retrieve all registrations ordered by priority.
59
+
60
+ ```python
61
+ service = di.resolve(Logger) # Logger | None
62
+ all_loggers = di.resolve_many(Logger) # list[Logger]
63
+ ```
64
+
65
+ `resolve` returns `None` when no registration exists for the requested type. `resolve_many` returns an empty list in that case.
66
+
67
+ Both methods also accept a string type name instead of a class:
68
+
69
+ ```python
70
+ service = di.resolve("Logger")
71
+ ```
72
+
73
+ String resolution matches against the `__name__` attribute of registered types and raises `ValueError` when no match is found.
74
+
75
+ # Priority
76
+
77
+ Each registration carries a `Priority` that determines its position relative to other registrations for the same type. Higher-priority registrations are resolved first by `resolve` and appear earlier in the list returned by `resolve_many`.
78
+
79
+ ```python
80
+ from ps.di import DI, Priority
81
+
82
+ di = DI()
83
+
84
+ di.register(NotificationService, priority=Priority.LOW).factory(NotificationService, "email")
85
+ di.register(NotificationService, priority=Priority.HIGH).factory(NotificationService, "sms")
86
+ di.register(NotificationService, priority=Priority.MEDIUM).factory(NotificationService, "push")
87
+
88
+ primary = di.resolve(NotificationService) # sms (HIGH wins)
89
+ ```
90
+
91
+ [View full example](https://github.com/BlackGad/ps-poetry/blob/main/src/examples/ps-dependency-injection/priority_example.py)
92
+
93
+ `Priority` values: `LOW` (default), `MEDIUM`, `HIGH`. When multiple registrations share the same priority, the most recently registered one wins.
94
+
95
+ # Spawn Objects
96
+
97
+ `spawn` instantiates a class without registering it, injecting constructor dependencies from the container automatically. It inspects type hints on `__init__` parameters and resolves them as follows:
98
+
99
+ * A parameter typed as `DI` or a subclass of `DI` receives the container itself.
100
+ * A parameter typed as `List[T]` receives the result of `resolve_many(T)`.
101
+ * A parameter typed as `Optional[T]` receives the result of `resolve(T)`, falling back to the default value when nothing is registered.
102
+ * Any other typed parameter receives the result of `resolve(T)`. If `resolve` returns `None` and no default exists, `spawn` raises `ValueError`.
103
+
104
+ Positional and keyword arguments passed to `spawn` override automatic resolution:
105
+
106
+ ```python
107
+ from ps.di import DI
108
+
109
+ di = DI()
110
+ di.register(Logger).factory(Logger, "app")
111
+
112
+ repo = di.spawn(UserRepository) # Logger injected from container
113
+ repo = di.spawn(UserRepository, logger=custom_logger) # explicit override
114
+ ```
115
+
116
+ # Satisfy Functions
117
+
118
+ `satisfy` binds a callable to dependencies resolved from the container at the time of the call, returning a new callable that accepts any remaining parameters at invocation time.
119
+
120
+ * Parameters with registered types are resolved from the container automatically.
121
+ * Parameters with defaults fall back to their default values when no registration exists.
122
+ * Parameters typed as `List[T]` receive all registered instances of `T`.
123
+ * Parameters typed as `Optional[T]` receive `None` when no registration exists.
124
+ * Parameters marked with `REQUIRED` are excluded from DI resolution and must be supplied by the caller.
125
+
126
+ ```python
127
+ from ps.di import DI, REQUIRED
128
+
129
+ log_message = di.satisfy(format_log, message=REQUIRED)
130
+
131
+ print(log_message(message="Application started"))
132
+ print(log_message(message="Low disk space", level="WARNING"))
133
+ ```
134
+
135
+ [View full example](https://github.com/BlackGad/ps-poetry/blob/main/src/examples/ps-dependency-injection/satisfy_example.py)
136
+
137
+ The returned callable accepts keyword arguments at invocation time. Any keyword argument passed at invocation time overrides the corresponding resolved value, including DI-resolved parameters.
138
+
139
+ # Scopes
140
+
141
+ `scope()` creates a child `DI` instance that inherits all registrations from the parent but maintains its own isolated registry. This is useful for per-request, per-session, or any other short-lived context that needs additional or overriding registrations without affecting the parent container.
142
+
143
+ Resolution in a scoped container follows these rules:
144
+
145
+ * `resolve` checks the scoped registry first; if nothing is registered, it falls through to the parent.
146
+ * `resolve_many` returns scoped registrations followed by parent registrations, with scoped results first.
147
+ * `spawn` and `satisfy` use the scoped resolver, so injected dependencies prefer scoped registrations.
148
+ * A parameter typed as `DI` receives the scoped instance, not the parent.
149
+
150
+ Scopes support the context manager protocol. Exiting the `with` block clears the scoped registry and releases all singleton instances held by the scope, enabling deterministic cleanup of resources such as database connections or file handles.
151
+
152
+ ```python
153
+ with di.scope() as request_scope:
154
+ request_scope.register(RequestContext).factory(RequestContext, request_id)
155
+ handler = request_scope.spawn(RequestHandler)
156
+ handler.handle()
157
+ # scoped singletons released here; parent container unaffected
158
+ ```
159
+
160
+ [View full example](https://github.com/BlackGad/ps-poetry/blob/main/src/examples/ps-dependency-injection/scope_example.py)
161
+
162
+ The root container supports the same context manager protocol. Exiting a `with di:` block clears all registrations and releases every singleton instance held by the container.
163
+
164
+ Scopes can be nested arbitrarily. Each level sees its own registrations plus all ancestor registrations, with closer scopes taking precedence.
165
+
166
+ # Thread Safety
167
+
168
+ All registration and resolution operations are protected by internal locks. Singleton creation uses double-checked locking so the factory is called exactly once even under concurrent access. Transient registrations produce independent instances per call with no shared mutable state.
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "ps-dependency-injection"
3
+ description = "Lightweight, thread-safe dependency injection container for Python"
4
+ readme = "README.md"
5
+ requires-python = ">=3.10,<3.14"
6
+ version = "0.2.9"
7
+ authors = [
8
+ { name = "ztBlackGad" },
9
+ ]
10
+
11
+ [project.urls]
12
+ Homepage = "https://github.com/BlackGad/ps-poetry"
13
+ Repository = "https://github.com/BlackGad/ps-poetry"
14
+
15
+ [tool.poetry]
16
+ packages = [ { include = "ps/di", from = "src" } ]
17
+
18
+ [tool.ps-plugin]
19
+ host-project = "../.."
20
+
21
+ [build-system]
22
+ requires = ["poetry-core>=1.0.0"]
23
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,10 @@
1
+ from ._enums import Lifetime, Priority
2
+ from ._di import Binding, DI, REQUIRED
3
+
4
+ __all__ = [
5
+ "DI",
6
+ "Binding",
7
+ "Lifetime",
8
+ "Priority",
9
+ "REQUIRED",
10
+ ]
@@ -0,0 +1,197 @@
1
+ import inspect
2
+ import threading
3
+ from typing import Any, Callable, List, Optional, Self, Type, TypeVar, Union, cast, get_args, get_origin, get_type_hints
4
+
5
+ from ._enums import Lifetime, Priority
6
+ from ._registration import _Registration, _Registrations
7
+
8
+ T = TypeVar("T")
9
+ R = TypeVar("R")
10
+
11
+
12
+ class _Sentinel:
13
+ def __repr__(self) -> str:
14
+ return "REQUIRED"
15
+
16
+
17
+ REQUIRED: _Sentinel = _Sentinel()
18
+
19
+
20
+ class Binding[T]:
21
+ def __init__(self, di: "DI", cls: Type[T], lifetime: Lifetime, priority: Priority) -> None:
22
+ self._di = di
23
+ self._cls = cls
24
+ self._lifetime = lifetime
25
+ self._priority = priority
26
+
27
+ def implementation(self, impl: Type[T]) -> None:
28
+ self._di._register(self._cls, _Registration(self._lifetime, self._priority, lambda: self._di.spawn(impl)))
29
+
30
+ def factory(self, factory: Callable[..., T], *args: Any, **kwargs: Any) -> None:
31
+ explicit: dict[str, Any] = dict(kwargs)
32
+ if args:
33
+ sig = inspect.signature(factory)
34
+ params = list(sig.parameters.values())
35
+ explicit = {p.name: v for p, v in zip(params, args, strict=False)} | explicit
36
+ self._di._register(self._cls, _Registration(self._lifetime, self._priority, self._di.satisfy(factory, **explicit)))
37
+
38
+
39
+ class DI:
40
+ def __init__(self) -> None:
41
+ self._registry: dict[Type, _Registrations] = {}
42
+ self._lock_registry_access = threading.Lock()
43
+ self._signature_cache: dict[Any, inspect.Signature] = {}
44
+ self._type_name_cache: dict[str, Type] = {}
45
+
46
+ def _resolve_type(self, key: Type[T] | str) -> Type[T]:
47
+ if isinstance(key, str):
48
+ if key not in self._type_name_cache:
49
+ found = next((t for t in self._registry if t.__name__ == key), None)
50
+ if found is None:
51
+ raise ValueError(f"Cannot resolve type from string '{key}' - no matching type registered")
52
+ self._type_name_cache[key] = found
53
+ return self._type_name_cache[key]
54
+ return key
55
+
56
+ def register(self, cls: Type[T] | str, lifetime: Lifetime = Lifetime.SINGLETON, priority: Priority = Priority.LOW) -> Binding[T]:
57
+ resolved_cls = cast(Type[T], self._resolve_type(cls)) if isinstance(cls, str) else cls
58
+ return Binding(self, resolved_cls, lifetime=lifetime, priority=priority)
59
+
60
+ def __enter__(self) -> Self:
61
+ return self
62
+
63
+ def __exit__(self, *args: object) -> None:
64
+ with self._lock_registry_access:
65
+ self._registry.clear()
66
+ self._signature_cache.clear()
67
+ self._type_name_cache.clear()
68
+
69
+ def resolve(self, key: Type[T] | str) -> Optional[T]:
70
+ resolved_key = self._resolve_type(key) if isinstance(key, str) else key
71
+ with self._lock_registry_access:
72
+ registrations = self._registry.get(resolved_key)
73
+ if registrations is None:
74
+ return None
75
+ return cast(T, registrations.resolve_first())
76
+
77
+ def resolve_many(self, key: Type[T] | str) -> List[T]:
78
+ resolved_key = self._resolve_type(key) if isinstance(key, str) else key
79
+ with self._lock_registry_access:
80
+ registrations = self._registry.get(resolved_key)
81
+ if registrations is None:
82
+ return []
83
+ return registrations.resolve_all()
84
+
85
+ def _register(self, cls: Type[T], registration: _Registration[T]) -> None:
86
+ with self._lock_registry_access:
87
+ registrations = self._registry.setdefault(cls, _Registrations())
88
+ registrations.add_registration(registration)
89
+
90
+ def _try_resolve_annotation(self, annotation: Any, has_default: bool, default: Any) -> tuple[bool, Any]:
91
+ if annotation is DI or (isinstance(annotation, type) and issubclass(annotation, DI)):
92
+ return True, self
93
+
94
+ origin = get_origin(annotation)
95
+
96
+ if origin is list:
97
+ type_args = get_args(annotation)
98
+ return True, self.resolve_many(type_args[0]) if type_args else []
99
+
100
+ if origin is Union:
101
+ type_args = get_args(annotation)
102
+ if type(None) in type_args:
103
+ non_none_type = next(t for t in type_args if t is not type(None))
104
+ resolved = self.resolve(non_none_type)
105
+ if resolved is not None:
106
+ return True, resolved
107
+ return True, default if has_default else None
108
+
109
+ resolved = self.resolve(annotation)
110
+ if resolved is not None:
111
+ return True, resolved
112
+ if has_default:
113
+ return True, default
114
+ return False, None
115
+
116
+ def _resolve_kwargs(self, fn: Callable, skip_self: bool, explicit_kwargs: dict[str, Any]) -> dict[str, Any]:
117
+ if fn not in self._signature_cache:
118
+ self._signature_cache[fn] = inspect.signature(fn)
119
+ sig = self._signature_cache[fn]
120
+
121
+ hints_target = fn.__init__ if isinstance(fn, type) else fn
122
+ try:
123
+ type_hints = get_type_hints(hints_target)
124
+ except Exception:
125
+ type_hints = {}
126
+
127
+ required_params = {k for k, v in explicit_kwargs.items() if v is REQUIRED}
128
+ final_kwargs = {k: explicit_kwargs[k] for k in explicit_kwargs.keys() - required_params}
129
+ params = list(sig.parameters.values())[1 if skip_self else 0:]
130
+
131
+ for param in params:
132
+ if param.name in final_kwargs or param.name in required_params:
133
+ continue
134
+
135
+ annotation = type_hints.get(param.name, param.annotation)
136
+ if annotation == inspect.Parameter.empty:
137
+ continue
138
+
139
+ has_default = param.default != inspect.Parameter.empty
140
+ ok, value = self._try_resolve_annotation(annotation, has_default, param.default)
141
+ if not ok:
142
+ raise ValueError(f"Cannot resolve required dependency {annotation} for parameter {param.name}")
143
+ final_kwargs[param.name] = value
144
+
145
+ return final_kwargs
146
+
147
+ def spawn(self, cls: Type[T], *args: Any, **kwargs: Any) -> T:
148
+ fn = cls.__init__
149
+ if args:
150
+ if fn not in self._signature_cache:
151
+ self._signature_cache[fn] = inspect.signature(fn)
152
+ params = list(self._signature_cache[fn].parameters.values())[1:]
153
+ kwargs = {p.name: v for p, v in zip(params, args, strict=False)} | kwargs
154
+ return cls(**self._resolve_kwargs(fn, skip_self=True, explicit_kwargs=kwargs))
155
+
156
+ def satisfy(self, fn: Callable[..., R], **kwargs: Any) -> Callable[..., R]:
157
+ resolved_kwargs = self._resolve_kwargs(fn, skip_self=False, explicit_kwargs=kwargs)
158
+
159
+ def wrapper(**override: Any) -> R:
160
+ return fn(**resolved_kwargs | override)
161
+
162
+ return wrapper
163
+
164
+ def scope(self) -> "DI":
165
+ return _ScopedDI(self)
166
+
167
+
168
+ class _ScopedDI(DI):
169
+ def __init__(self, parent: DI) -> None:
170
+ super().__init__()
171
+ self._parent = parent
172
+
173
+ def _resolve_type(self, key: Type[T] | str) -> Type[T]:
174
+ if isinstance(key, str):
175
+ if key not in self._type_name_cache:
176
+ found = next((t for t in self._registry if t.__name__ == key), None)
177
+ if found is None:
178
+ return self._parent._resolve_type(key)
179
+ self._type_name_cache[key] = found
180
+ return self._type_name_cache[key]
181
+ return key
182
+
183
+ def resolve(self, key: Type[T] | str) -> Optional[T]:
184
+ resolved_key = self._resolve_type(key) if isinstance(key, str) else key
185
+ with self._lock_registry_access:
186
+ registrations = self._registry.get(resolved_key)
187
+ if registrations is not None:
188
+ return cast(T, registrations.resolve_first())
189
+ return self._parent.resolve(key)
190
+
191
+ def resolve_many(self, key: Type[T] | str) -> List[T]:
192
+ resolved_key = self._resolve_type(key) if isinstance(key, str) else key
193
+ with self._lock_registry_access:
194
+ registrations = self._registry.get(resolved_key)
195
+ scoped_results: List[T] = registrations.resolve_all() if registrations is not None else []
196
+ parent_results: List[T] = self._parent.resolve_many(key)
197
+ return scoped_results + parent_results
@@ -0,0 +1,13 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class Lifetime(IntEnum):
5
+ UNKNOWN = 0
6
+ SINGLETON = 1
7
+ TRANSIENT = 2
8
+
9
+
10
+ class Priority(IntEnum):
11
+ LOW = 0
12
+ MEDIUM = 1
13
+ HIGH = 2
@@ -0,0 +1,52 @@
1
+ import threading
2
+ from typing import Callable, List, Optional
3
+
4
+ from ._enums import Lifetime, Priority
5
+
6
+
7
+ class _Registration[T]:
8
+ def __init__(self, lifetime: Lifetime, priority: Priority, factory: Callable[[], T]) -> None:
9
+ self.lifetime = lifetime
10
+ self.priority = priority
11
+ self.factory = factory
12
+ self.instance: Optional[T] = None
13
+ self._lock_singleton_creation = threading.Lock()
14
+
15
+ def resolve(self) -> T:
16
+ match self.lifetime:
17
+ case Lifetime.SINGLETON:
18
+ if self.instance is None:
19
+ with self._lock_singleton_creation:
20
+ if self.instance is None:
21
+ self.instance = self.factory()
22
+ return self.instance
23
+ case Lifetime.TRANSIENT:
24
+ return self.factory()
25
+ case _:
26
+ raise ValueError("Unknown lifetime for registration.")
27
+
28
+
29
+ class _Registrations:
30
+ def __init__(self) -> None:
31
+ self._registrations: List[_Registration] = []
32
+ self._lock_registrations_access = threading.Lock()
33
+
34
+ def add_registration(self, registration: _Registration) -> None:
35
+ with self._lock_registrations_access:
36
+ insert_pos = 0
37
+ for i, existing in enumerate(self._registrations):
38
+ if registration.priority >= existing.priority:
39
+ insert_pos = i
40
+ break
41
+ insert_pos = i + 1
42
+ self._registrations.insert(insert_pos, registration)
43
+
44
+ def resolve_first(self) -> object:
45
+ with self._lock_registrations_access:
46
+ registration = self._registrations[0]
47
+ return registration.resolve()
48
+
49
+ def resolve_all(self) -> List:
50
+ with self._lock_registrations_access:
51
+ registrations = list(self._registrations)
52
+ return [registration.resolve() for registration in registrations]