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.
- ps_dependency_injection-0.2.9/PKG-INFO +184 -0
- ps_dependency_injection-0.2.9/README.md +168 -0
- ps_dependency_injection-0.2.9/pyproject.toml +23 -0
- ps_dependency_injection-0.2.9/src/ps/di/__init__.py +10 -0
- ps_dependency_injection-0.2.9/src/ps/di/_di.py +197 -0
- ps_dependency_injection-0.2.9/src/ps/di/_enums.py +13 -0
- ps_dependency_injection-0.2.9/src/ps/di/_registration.py +52 -0
|
@@ -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,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,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]
|