soupape 0.1.0__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.
- soupape-0.1.0/PKG-INFO +277 -0
- soupape-0.1.0/README.md +269 -0
- soupape-0.1.0/pyproject.toml +34 -0
- soupape-0.1.0/soupape/__init__.py +4 -0
- soupape-0.1.0/soupape/collection.py +128 -0
- soupape-0.1.0/soupape/errors.py +46 -0
- soupape-0.1.0/soupape/injector/__init__.py +3 -0
- soupape-0.1.0/soupape/injector/async_injector.py +158 -0
- soupape-0.1.0/soupape/injector/base.py +109 -0
- soupape-0.1.0/soupape/injector/sync_injector.py +126 -0
- soupape-0.1.0/soupape/instances.py +60 -0
- soupape-0.1.0/soupape/metadata.py +37 -0
- soupape-0.1.0/soupape/post_init.py +17 -0
- soupape-0.1.0/soupape/py.typed +0 -0
- soupape-0.1.0/soupape/resolvers/__init__.py +3 -0
- soupape-0.1.0/soupape/resolvers/async_default.py +31 -0
- soupape-0.1.0/soupape/resolvers/default.py +47 -0
- soupape-0.1.0/soupape/resolvers/sync_default.py +28 -0
- soupape-0.1.0/soupape/types.py +97 -0
soupape-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: soupape
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Requires-Dist: peritype>=0.1.5
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# Soupape
|
|
10
|
+
|
|
11
|
+
Soupape is a dependency injection and inversion of control library in pure Python.
|
|
12
|
+
It allows you to manage the dependencies of your services in your application in a clean and efficient way.
|
|
13
|
+
Soupape is a standalone library that does not rely on any framework and can be used in any Python project.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```shell
|
|
18
|
+
$ pip install soupape # or use your preferred package manager
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
### Service Registration and Injection
|
|
24
|
+
|
|
25
|
+
Let's first write some services.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from my_app.models import User
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HttpService:
|
|
34
|
+
async def get(self, url: str) -> dict[str, Any]: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UserService:
|
|
38
|
+
async def get_user(self, user_id: int) -> User: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AuthService:
|
|
42
|
+
def __init__(self, http: HttpService, user_service: UserService) -> None:
|
|
43
|
+
self.http = http
|
|
44
|
+
self.user_service = user_service
|
|
45
|
+
|
|
46
|
+
async def authenticate(self, token: str) -> User: ...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Now, we can register them in the service collection.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from soupape import ServiceCollection
|
|
53
|
+
|
|
54
|
+
from my_app.services import AuthService, HttpService, UserService
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def define_services() -> ServiceCollection:
|
|
58
|
+
services = ServiceCollection()
|
|
59
|
+
services.add_singleton(HttpService)
|
|
60
|
+
services.add_scoped(UserService)
|
|
61
|
+
services.add_scoped(AuthService)
|
|
62
|
+
return services
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def main():
|
|
66
|
+
services = define_services()
|
|
67
|
+
async with AsyncInjector(services) as injector:
|
|
68
|
+
async with injector.get_scoped_injector() as scoped_injector:
|
|
69
|
+
auth_service = await scoped_injector.require(AuthService)
|
|
70
|
+
token = ... # obtain token from somewhere
|
|
71
|
+
user = await auth_service.authenticate(token)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Let's break down what we did here:
|
|
75
|
+
- We created a 'HttpService' as a singleton, meaning there will be only one instance of it throughout the main injector's lifetime.
|
|
76
|
+
- We created 'UserService' and 'AuthService' as scoped services, meaning a new instance will be created for each scoped injector.
|
|
77
|
+
|
|
78
|
+
A `SyncInjector` also exists for synchronous code only.
|
|
79
|
+
In the example above, a synchronous injector could be used because none of the services require asynchronous initialization.
|
|
80
|
+
See below for more details on initialization.
|
|
81
|
+
|
|
82
|
+
### Type hints
|
|
83
|
+
|
|
84
|
+
Soupape uses type hints to resolve dependencies.
|
|
85
|
+
This library makes all type hints mandatory for service constructors and resolver functions.
|
|
86
|
+
|
|
87
|
+
Errors will be raised if type hints are missing.
|
|
88
|
+
|
|
89
|
+
### Service Lifetimes
|
|
90
|
+
|
|
91
|
+
Soupape supports three service lifetimes:
|
|
92
|
+
- **Transient**:
|
|
93
|
+
- A new instance is created every time the service is requested.
|
|
94
|
+
- **Singleton**:
|
|
95
|
+
- A single instance is created and shared throughout the lifetime of the main injector.
|
|
96
|
+
- A singleton service instance is kept alive in the main injector, even when it is created in a scoped injection session.
|
|
97
|
+
- Singleton services are disposed of when the main injector is closed.
|
|
98
|
+
- Singleton services can only depend on singleton or transient services.
|
|
99
|
+
- **Scoped**:
|
|
100
|
+
- The main injector cannot create scoped services, only scoped injectors can.
|
|
101
|
+
- A new instance is created in the scoped injection session.
|
|
102
|
+
- When using multi-level scoped injectors, a scoped service instance is kept alive in the scoped injection session where it was created.
|
|
103
|
+
A child injection session will use the instances from its parent sessions.
|
|
104
|
+
Be careful which scoped injector you request a scoped service from.
|
|
105
|
+
- Scoped services are disposed of when the scoped injection session they were created in is closed.
|
|
106
|
+
- Scoped services can depend on singleton, transient, or scoped services.
|
|
107
|
+
|
|
108
|
+
### Context manager services
|
|
109
|
+
|
|
110
|
+
When registered through the default resolver, services can implement the context manager protocol (sync or async) to manage resources.
|
|
111
|
+
|
|
112
|
+
The `__enter__` (or `__aenter__`) method will be called when the service is created, and the `__exit__` (or `__aexit__`) method will be called when the injection session that created the service is closed.
|
|
113
|
+
|
|
114
|
+
The `SyncInjector` will raise an error during service injection if any dependency implements the async context manager protocol.
|
|
115
|
+
The `AsyncInjector` can handle both sync and async context managers.
|
|
116
|
+
If a service implements both protocols, the async one will be used and the sync one will be ignored.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from typing import Self
|
|
120
|
+
from types import TracebackType
|
|
121
|
+
|
|
122
|
+
from soupape import AsyncInjector, ServiceCollection
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ServiceWithResources:
|
|
126
|
+
def __init__(self) -> None:
|
|
127
|
+
self.resource = None
|
|
128
|
+
|
|
129
|
+
async def __aenter__(self) -> Self:
|
|
130
|
+
self.resource = await acquire_resource()
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
async def __aexit__(
|
|
134
|
+
self,
|
|
135
|
+
exc_type: type[BaseException] | None,
|
|
136
|
+
exc_value: BaseException | None,
|
|
137
|
+
traceback: TracebackType | None
|
|
138
|
+
) -> None:
|
|
139
|
+
await release_resource(self.resource)
|
|
140
|
+
self.resource = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
services = ServiceCollection()
|
|
144
|
+
services.add_scoped(ServiceWithResources)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def main():
|
|
148
|
+
async with AsyncInjector(services) as injector:
|
|
149
|
+
async with injector.get_scoped_injector() as scoped_injector:
|
|
150
|
+
service = await scoped_injector.require(ServiceWithResources)
|
|
151
|
+
assert service.resource is not None
|
|
152
|
+
assert service.resource is None
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
When using custom resolver functions, see below, you are responsible for managing the context manager protocol if needed.
|
|
156
|
+
|
|
157
|
+
### Post init methods
|
|
158
|
+
|
|
159
|
+
Another way to organize service initialization is to use post init methods.
|
|
160
|
+
|
|
161
|
+
A post init method can be synchronous or asynchronous.
|
|
162
|
+
These methods will be called after the service is created, but before it is returned to the caller.
|
|
163
|
+
They will be called in the order they are defined in the class.
|
|
164
|
+
Post init methods in parent classes will be called before those in child classes.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from soupape import AsyncInjector, ServiceCollection, post_init
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ServiceWithPostInit:
|
|
171
|
+
def __init__(self) -> None:
|
|
172
|
+
self.state = 'created'
|
|
173
|
+
|
|
174
|
+
@post_init
|
|
175
|
+
async def _init_state(self) -> None:
|
|
176
|
+
self.state = 'initialized
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
When using custom resolver functions, post init methods will be ignored.
|
|
180
|
+
|
|
181
|
+
### Custom resolver functions
|
|
182
|
+
|
|
183
|
+
You can register your services using your own resolver functions.
|
|
184
|
+
It can be useful when you need to pass some parameters to the service constructor that are not managed by the injector.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from soupape import AsyncInjector, ServiceCollection
|
|
188
|
+
|
|
189
|
+
from my_app.models import User
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class UserRepository:
|
|
193
|
+
async def get_user(self, user_id: int) -> User: ...
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class CurrentUserService:
|
|
197
|
+
def __init__(self, current_user: User) -> None:
|
|
198
|
+
self._current_user = current_user
|
|
199
|
+
|
|
200
|
+
def get_user(self) -> User:
|
|
201
|
+
return self._current_user
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def current_user_service_resolver(
|
|
205
|
+
user_repository: UserRepository
|
|
206
|
+
) -> CurrentUserService:
|
|
207
|
+
user_id = ... # obtain user id from somewhere
|
|
208
|
+
current_user = await user_repository.get_user(user_id)
|
|
209
|
+
return CurrentUserService(current_user)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
services = ServiceCollection()
|
|
213
|
+
services.add_scoped(UserRepository)
|
|
214
|
+
services.add_scoped(current_user_service_resolver)
|
|
215
|
+
|
|
216
|
+
async def main():
|
|
217
|
+
async with AsyncInjector(services) as injector:
|
|
218
|
+
async with injector.get_scoped_injector() as scoped_injector:
|
|
219
|
+
current_user_service = await scoped_injector.require(CurrentUserService)
|
|
220
|
+
user = current_user_service.get_user()
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Again, type hints are mandatory for the resolver function parameters and return type.
|
|
224
|
+
The registration and the dependency resolution are linked through the return type hint of the resolver function that must match.
|
|
225
|
+
|
|
226
|
+
When using custom resolver functions, Soupape does not manage the context manager protocol for you.
|
|
227
|
+
|
|
228
|
+
You can use a context manager in the resolver function, as shown below.
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
async def service_with_resources_resolver() -> ServiceWithResources:
|
|
232
|
+
async with ServiceWithResources() as service:
|
|
233
|
+
return service
|
|
234
|
+
|
|
235
|
+
services = ServiceCollection()
|
|
236
|
+
services.add_scoped(service_with_resources_resolver)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Generator resolver functions
|
|
240
|
+
|
|
241
|
+
Resolver functions can use the `yield` statement instead of context managers to execute instructions after the injection session is closed.
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from collections.abc import AsyncGenerator
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class Service:
|
|
248
|
+
def __init__(self) -> None:
|
|
249
|
+
self.state = 'created'
|
|
250
|
+
|
|
251
|
+
async def initialize(self) -> None:
|
|
252
|
+
self.state = 'initialized'
|
|
253
|
+
|
|
254
|
+
async def cleanup(self) -> None:
|
|
255
|
+
self.state = 'closed'
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def service_resolver() -> AsyncGenerator[Service]:
|
|
259
|
+
service = Service()
|
|
260
|
+
await service.initialize()
|
|
261
|
+
try:
|
|
262
|
+
yield service
|
|
263
|
+
finally:
|
|
264
|
+
await service.cleanup()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
services = ServiceCollection()
|
|
268
|
+
services.add_scoped(service_resolver)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
async def main():
|
|
272
|
+
async with AsyncInjector(services) as injector:
|
|
273
|
+
async with injector.get_scoped_injector() as scoped_injector:
|
|
274
|
+
service = await scoped_injector.require(Service)
|
|
275
|
+
assert service.state == 'initialized'
|
|
276
|
+
assert service.state == 'closed'
|
|
277
|
+
```
|
soupape-0.1.0/README.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Soupape
|
|
2
|
+
|
|
3
|
+
Soupape is a dependency injection and inversion of control library in pure Python.
|
|
4
|
+
It allows you to manage the dependencies of your services in your application in a clean and efficient way.
|
|
5
|
+
Soupape is a standalone library that does not rely on any framework and can be used in any Python project.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
$ pip install soupape # or use your preferred package manager
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
### Service Registration and Injection
|
|
16
|
+
|
|
17
|
+
Let's first write some services.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from my_app.models import User
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HttpService:
|
|
26
|
+
async def get(self, url: str) -> dict[str, Any]: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class UserService:
|
|
30
|
+
async def get_user(self, user_id: int) -> User: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AuthService:
|
|
34
|
+
def __init__(self, http: HttpService, user_service: UserService) -> None:
|
|
35
|
+
self.http = http
|
|
36
|
+
self.user_service = user_service
|
|
37
|
+
|
|
38
|
+
async def authenticate(self, token: str) -> User: ...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Now, we can register them in the service collection.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from soupape import ServiceCollection
|
|
45
|
+
|
|
46
|
+
from my_app.services import AuthService, HttpService, UserService
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def define_services() -> ServiceCollection:
|
|
50
|
+
services = ServiceCollection()
|
|
51
|
+
services.add_singleton(HttpService)
|
|
52
|
+
services.add_scoped(UserService)
|
|
53
|
+
services.add_scoped(AuthService)
|
|
54
|
+
return services
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def main():
|
|
58
|
+
services = define_services()
|
|
59
|
+
async with AsyncInjector(services) as injector:
|
|
60
|
+
async with injector.get_scoped_injector() as scoped_injector:
|
|
61
|
+
auth_service = await scoped_injector.require(AuthService)
|
|
62
|
+
token = ... # obtain token from somewhere
|
|
63
|
+
user = await auth_service.authenticate(token)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Let's break down what we did here:
|
|
67
|
+
- We created a 'HttpService' as a singleton, meaning there will be only one instance of it throughout the main injector's lifetime.
|
|
68
|
+
- We created 'UserService' and 'AuthService' as scoped services, meaning a new instance will be created for each scoped injector.
|
|
69
|
+
|
|
70
|
+
A `SyncInjector` also exists for synchronous code only.
|
|
71
|
+
In the example above, a synchronous injector could be used because none of the services require asynchronous initialization.
|
|
72
|
+
See below for more details on initialization.
|
|
73
|
+
|
|
74
|
+
### Type hints
|
|
75
|
+
|
|
76
|
+
Soupape uses type hints to resolve dependencies.
|
|
77
|
+
This library makes all type hints mandatory for service constructors and resolver functions.
|
|
78
|
+
|
|
79
|
+
Errors will be raised if type hints are missing.
|
|
80
|
+
|
|
81
|
+
### Service Lifetimes
|
|
82
|
+
|
|
83
|
+
Soupape supports three service lifetimes:
|
|
84
|
+
- **Transient**:
|
|
85
|
+
- A new instance is created every time the service is requested.
|
|
86
|
+
- **Singleton**:
|
|
87
|
+
- A single instance is created and shared throughout the lifetime of the main injector.
|
|
88
|
+
- A singleton service instance is kept alive in the main injector, even when it is created in a scoped injection session.
|
|
89
|
+
- Singleton services are disposed of when the main injector is closed.
|
|
90
|
+
- Singleton services can only depend on singleton or transient services.
|
|
91
|
+
- **Scoped**:
|
|
92
|
+
- The main injector cannot create scoped services, only scoped injectors can.
|
|
93
|
+
- A new instance is created in the scoped injection session.
|
|
94
|
+
- When using multi-level scoped injectors, a scoped service instance is kept alive in the scoped injection session where it was created.
|
|
95
|
+
A child injection session will use the instances from its parent sessions.
|
|
96
|
+
Be careful which scoped injector you request a scoped service from.
|
|
97
|
+
- Scoped services are disposed of when the scoped injection session they were created in is closed.
|
|
98
|
+
- Scoped services can depend on singleton, transient, or scoped services.
|
|
99
|
+
|
|
100
|
+
### Context manager services
|
|
101
|
+
|
|
102
|
+
When registered through the default resolver, services can implement the context manager protocol (sync or async) to manage resources.
|
|
103
|
+
|
|
104
|
+
The `__enter__` (or `__aenter__`) method will be called when the service is created, and the `__exit__` (or `__aexit__`) method will be called when the injection session that created the service is closed.
|
|
105
|
+
|
|
106
|
+
The `SyncInjector` will raise an error during service injection if any dependency implements the async context manager protocol.
|
|
107
|
+
The `AsyncInjector` can handle both sync and async context managers.
|
|
108
|
+
If a service implements both protocols, the async one will be used and the sync one will be ignored.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from typing import Self
|
|
112
|
+
from types import TracebackType
|
|
113
|
+
|
|
114
|
+
from soupape import AsyncInjector, ServiceCollection
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ServiceWithResources:
|
|
118
|
+
def __init__(self) -> None:
|
|
119
|
+
self.resource = None
|
|
120
|
+
|
|
121
|
+
async def __aenter__(self) -> Self:
|
|
122
|
+
self.resource = await acquire_resource()
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
async def __aexit__(
|
|
126
|
+
self,
|
|
127
|
+
exc_type: type[BaseException] | None,
|
|
128
|
+
exc_value: BaseException | None,
|
|
129
|
+
traceback: TracebackType | None
|
|
130
|
+
) -> None:
|
|
131
|
+
await release_resource(self.resource)
|
|
132
|
+
self.resource = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
services = ServiceCollection()
|
|
136
|
+
services.add_scoped(ServiceWithResources)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def main():
|
|
140
|
+
async with AsyncInjector(services) as injector:
|
|
141
|
+
async with injector.get_scoped_injector() as scoped_injector:
|
|
142
|
+
service = await scoped_injector.require(ServiceWithResources)
|
|
143
|
+
assert service.resource is not None
|
|
144
|
+
assert service.resource is None
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
When using custom resolver functions, see below, you are responsible for managing the context manager protocol if needed.
|
|
148
|
+
|
|
149
|
+
### Post init methods
|
|
150
|
+
|
|
151
|
+
Another way to organize service initialization is to use post init methods.
|
|
152
|
+
|
|
153
|
+
A post init method can be synchronous or asynchronous.
|
|
154
|
+
These methods will be called after the service is created, but before it is returned to the caller.
|
|
155
|
+
They will be called in the order they are defined in the class.
|
|
156
|
+
Post init methods in parent classes will be called before those in child classes.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from soupape import AsyncInjector, ServiceCollection, post_init
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ServiceWithPostInit:
|
|
163
|
+
def __init__(self) -> None:
|
|
164
|
+
self.state = 'created'
|
|
165
|
+
|
|
166
|
+
@post_init
|
|
167
|
+
async def _init_state(self) -> None:
|
|
168
|
+
self.state = 'initialized
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
When using custom resolver functions, post init methods will be ignored.
|
|
172
|
+
|
|
173
|
+
### Custom resolver functions
|
|
174
|
+
|
|
175
|
+
You can register your services using your own resolver functions.
|
|
176
|
+
It can be useful when you need to pass some parameters to the service constructor that are not managed by the injector.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from soupape import AsyncInjector, ServiceCollection
|
|
180
|
+
|
|
181
|
+
from my_app.models import User
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class UserRepository:
|
|
185
|
+
async def get_user(self, user_id: int) -> User: ...
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class CurrentUserService:
|
|
189
|
+
def __init__(self, current_user: User) -> None:
|
|
190
|
+
self._current_user = current_user
|
|
191
|
+
|
|
192
|
+
def get_user(self) -> User:
|
|
193
|
+
return self._current_user
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def current_user_service_resolver(
|
|
197
|
+
user_repository: UserRepository
|
|
198
|
+
) -> CurrentUserService:
|
|
199
|
+
user_id = ... # obtain user id from somewhere
|
|
200
|
+
current_user = await user_repository.get_user(user_id)
|
|
201
|
+
return CurrentUserService(current_user)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
services = ServiceCollection()
|
|
205
|
+
services.add_scoped(UserRepository)
|
|
206
|
+
services.add_scoped(current_user_service_resolver)
|
|
207
|
+
|
|
208
|
+
async def main():
|
|
209
|
+
async with AsyncInjector(services) as injector:
|
|
210
|
+
async with injector.get_scoped_injector() as scoped_injector:
|
|
211
|
+
current_user_service = await scoped_injector.require(CurrentUserService)
|
|
212
|
+
user = current_user_service.get_user()
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Again, type hints are mandatory for the resolver function parameters and return type.
|
|
216
|
+
The registration and the dependency resolution are linked through the return type hint of the resolver function that must match.
|
|
217
|
+
|
|
218
|
+
When using custom resolver functions, Soupape does not manage the context manager protocol for you.
|
|
219
|
+
|
|
220
|
+
You can use a context manager in the resolver function, as shown below.
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
async def service_with_resources_resolver() -> ServiceWithResources:
|
|
224
|
+
async with ServiceWithResources() as service:
|
|
225
|
+
return service
|
|
226
|
+
|
|
227
|
+
services = ServiceCollection()
|
|
228
|
+
services.add_scoped(service_with_resources_resolver)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Generator resolver functions
|
|
232
|
+
|
|
233
|
+
Resolver functions can use the `yield` statement instead of context managers to execute instructions after the injection session is closed.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from collections.abc import AsyncGenerator
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Service:
|
|
240
|
+
def __init__(self) -> None:
|
|
241
|
+
self.state = 'created'
|
|
242
|
+
|
|
243
|
+
async def initialize(self) -> None:
|
|
244
|
+
self.state = 'initialized'
|
|
245
|
+
|
|
246
|
+
async def cleanup(self) -> None:
|
|
247
|
+
self.state = 'closed'
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def service_resolver() -> AsyncGenerator[Service]:
|
|
251
|
+
service = Service()
|
|
252
|
+
await service.initialize()
|
|
253
|
+
try:
|
|
254
|
+
yield service
|
|
255
|
+
finally:
|
|
256
|
+
await service.cleanup()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
services = ServiceCollection()
|
|
260
|
+
services.add_scoped(service_resolver)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
async def main():
|
|
264
|
+
async with AsyncInjector(services) as injector:
|
|
265
|
+
async with injector.get_scoped_injector() as scoped_injector:
|
|
266
|
+
service = await scoped_injector.require(Service)
|
|
267
|
+
assert service.state == 'initialized'
|
|
268
|
+
assert service.state == 'closed'
|
|
269
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "soupape"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = ["peritype>=0.1.5"]
|
|
8
|
+
|
|
9
|
+
[build-system]
|
|
10
|
+
requires = ["uv_build>=0.8.4,<0.9.0"]
|
|
11
|
+
build-backend = "uv_build"
|
|
12
|
+
|
|
13
|
+
[tool.uv.build-backend]
|
|
14
|
+
package = "soupape"
|
|
15
|
+
module-root = ""
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = ["ruff>=0.12.7"]
|
|
19
|
+
test = [
|
|
20
|
+
"pytest>=8.4.1",
|
|
21
|
+
"pytest-asyncio>=1.3.0",
|
|
22
|
+
"pytest-cov>=6.2.1",
|
|
23
|
+
"typing-extensions>=4.15.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.ruff]
|
|
27
|
+
line-length = 120
|
|
28
|
+
|
|
29
|
+
[tool.ruff.lint]
|
|
30
|
+
extend-select = ["E", "W", "F", "B", "Q", "I", "N", "RUF", "UP"]
|
|
31
|
+
fixable = ["ALL"]
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint.per-file-ignores]
|
|
34
|
+
"**/__init__.py" = ["I001"]
|