wireup 2.0.1__tar.gz → 2.2.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.
- {wireup-2.0.1 → wireup-2.2.0}/PKG-INFO +32 -17
- {wireup-2.0.1 → wireup-2.2.0}/pyproject.toml +6 -1
- {wireup-2.0.1 → wireup-2.2.0}/readme.md +26 -14
- wireup-2.2.0/wireup/_decorators.py +156 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/errors.py +17 -5
- {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/aiohttp.py +8 -3
- wireup-2.2.0/wireup/integration/click.py +26 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/django/apps.py +4 -4
- {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/fastapi.py +25 -70
- wireup-2.2.0/wireup/integration/starlette.py +115 -0
- wireup-2.2.0/wireup/ioc/_exit_stack.py +91 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/container/__init__.py +32 -13
- wireup-2.2.0/wireup/ioc/container/async_container.py +84 -0
- wireup-2.2.0/wireup/ioc/container/base_container.py +93 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/container/sync_container.py +19 -5
- wireup-2.2.0/wireup/ioc/factory_compiler.py +159 -0
- wireup-2.2.0/wireup/ioc/override_manager.py +139 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/service_registry.py +151 -17
- {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/types.py +1 -6
- wireup-2.2.0/wireup/ioc/util.py +198 -0
- wireup-2.0.1/wireup/_async_to_sync.py +0 -34
- wireup-2.0.1/wireup/_decorators.py +0 -110
- wireup-2.0.1/wireup/ioc/_exit_stack.py +0 -54
- wireup-2.0.1/wireup/ioc/container/async_container.py +0 -49
- wireup-2.0.1/wireup/ioc/container/base_container.py +0 -191
- wireup-2.0.1/wireup/ioc/override_manager.py +0 -75
- wireup-2.0.1/wireup/ioc/util.py +0 -81
- wireup-2.0.1/wireup/ioc/validation.py +0 -131
- {wireup-2.0.1 → wireup-2.2.0}/wireup/__init__.py +0 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/_annotations.py +0 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/_discovery.py +0 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/__init__.py +0 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/django/__init__.py +0 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/integration/flask.py +0 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/__init__.py +0 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/ioc/parameter.py +0 -0
- {wireup-2.0.1 → wireup-2.2.0}/wireup/py.typed +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: wireup
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Python Dependency Injection Library
|
|
5
|
-
Home-page: https://github.com/maldoinc/wireup
|
|
6
5
|
License: MIT
|
|
7
6
|
Keywords: flask,django,injector,dependency injection,dependency injection container,dependency injector
|
|
8
7
|
Author: Aldo Mateli
|
|
@@ -26,12 +25,16 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
26
25
|
Classifier: Programming Language :: Python :: 3.11
|
|
27
26
|
Classifier: Programming Language :: Python :: 3.12
|
|
28
27
|
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
29
29
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
30
30
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
31
31
|
Classifier: Typing :: Typed
|
|
32
|
+
Provides-Extra: eval-type
|
|
33
|
+
Requires-Dist: eval-type-backport (>=0.2.0,<0.3.0) ; extra == "eval-type"
|
|
32
34
|
Requires-Dist: typing_extensions (>=4.7,<5.0)
|
|
33
35
|
Project-URL: Changelog, https://github.com/maldoinc/wireup/releases
|
|
34
36
|
Project-URL: Documentation, https://maldoinc.github.io/wireup/
|
|
37
|
+
Project-URL: Homepage, https://github.com/maldoinc/wireup
|
|
35
38
|
Project-URL: Repository, https://github.com/maldoinc/wireup
|
|
36
39
|
Description-Content-Type: text/markdown
|
|
37
40
|
|
|
@@ -43,8 +46,7 @@ Description-Content-Type: text/markdown
|
|
|
43
46
|
[](https://github.com/maldoinc/wireup)
|
|
44
47
|
[](https://pypi.org/project/wireup/)
|
|
45
48
|
[](https://pypi.org/project/wireup/)
|
|
46
|
-
|
|
47
|
-
<p><a target="_blank" href="https://maldoinc.github.io/wireup">📚 Documentation</a> | <a target="_blank" href="https://github.com/maldoinc/wireup-demo">🎮 Demo Application</a></p>
|
|
49
|
+
[](https://maldoinc.github.io/wireup)
|
|
48
50
|
</div>
|
|
49
51
|
|
|
50
52
|
Dependency Injection (DI) is a design pattern where dependencies are provided externally rather than created within objects. Wireup automates dependency management using Python's type system, with support for async, generators, modern Python features and integrations for FastAPI, Django, Flask and AIOHTTP out of the box.
|
|
@@ -74,19 +76,32 @@ user_service = container.get(UserService) # ✅ Dependencies resolved.
|
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
<details>
|
|
77
|
-
<summary>
|
|
79
|
+
<summary>No annotations</summary>
|
|
80
|
+
|
|
81
|
+
Keep domain objects clean of framework annotations by using factories.
|
|
78
82
|
|
|
79
83
|
```python
|
|
80
|
-
|
|
84
|
+
# Clean domain objects: No annotations
|
|
81
85
|
class Database:
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
class UserService:
|
|
89
|
+
def __init__(self, db: Database) -> None:
|
|
90
|
+
self.db = db
|
|
91
|
+
|
|
92
|
+
# Register services via factories
|
|
93
|
+
@service
|
|
94
|
+
def database_factory() -> Database:
|
|
95
|
+
return Database()
|
|
96
|
+
|
|
97
|
+
@service
|
|
98
|
+
def user_service_factory(db: Database) -> UserService:
|
|
99
|
+
return UserService(db)
|
|
84
100
|
|
|
85
101
|
container = wireup.create_sync_container(
|
|
86
|
-
services=[
|
|
87
|
-
parameters={"db_url": os.environ["APP_DB_URL"]}
|
|
102
|
+
services=[database_factory, user_service_factory]
|
|
88
103
|
)
|
|
89
|
-
|
|
104
|
+
user_service = container.get(UserService) # ✅ Dependencies resolved.
|
|
90
105
|
```
|
|
91
106
|
|
|
92
107
|
</details>
|
|
@@ -228,7 +243,7 @@ Wireup provides its own Dependency Injection mechanism and is not tied to specif
|
|
|
228
243
|
|
|
229
244
|
Share the service layer between your web application and its accompanying CLI using Wireup.
|
|
230
245
|
|
|
231
|
-
### 🔌 Native Integration with Django, FastAPI, Flask and
|
|
246
|
+
### 🔌 Native Integration with Django, FastAPI, Flask, AIOHTTP, Click and Starlette
|
|
232
247
|
|
|
233
248
|
Integrate with popular frameworks for a smoother developer experience.
|
|
234
249
|
Integrations manage request scopes, injection in endpoints, and lifecycle of services.
|
|
@@ -244,6 +259,10 @@ def users_list(user_service: Injected[UserService]):
|
|
|
244
259
|
wireup.integration.fastapi.setup(container, app)
|
|
245
260
|
```
|
|
246
261
|
|
|
262
|
+
**Supported Frameworks:** FastAPI (with zero-overhead class-based handlers), Django, Flask, AIOHTTP, Starlette, and Click.
|
|
263
|
+
|
|
264
|
+
[View all integrations →](https://maldoinc.github.io/wireup/latest/integrations/)
|
|
265
|
+
|
|
247
266
|
### 🧪 Simplified Testing
|
|
248
267
|
|
|
249
268
|
Wireup does not patch your services and lets you test them in isolation.
|
|
@@ -263,7 +282,3 @@ with container.override.service(target=Database, new=in_memory_database):
|
|
|
263
282
|
|
|
264
283
|
For more information [check out the documentation](https://maldoinc.github.io/wireup)
|
|
265
284
|
|
|
266
|
-
## 🎮 Demo application
|
|
267
|
-
|
|
268
|
-
A demo flask application is available at [maldoinc/wireup-demo](https://github.com/maldoinc/wireup-demo)
|
|
269
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "wireup"
|
|
3
|
-
version = "2.0
|
|
3
|
+
version = "2.2.0"
|
|
4
4
|
description = "Python Dependency Injection Library"
|
|
5
5
|
authors = ["Aldo Mateli <aldo.mateli@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -46,6 +46,10 @@ Changelog = "https://github.com/maldoinc/wireup/releases"
|
|
|
46
46
|
[tool.poetry.dependencies]
|
|
47
47
|
python = "^3.8"
|
|
48
48
|
typing_extensions = "^4.7"
|
|
49
|
+
eval-type-backport = { version = "^0.2.0", optional = true }
|
|
50
|
+
|
|
51
|
+
[tool.poetry.extras]
|
|
52
|
+
eval-type = ["eval-type-backport"]
|
|
49
53
|
|
|
50
54
|
[tool.poetry.group.dev.dependencies]
|
|
51
55
|
ruff = "0.11.0"
|
|
@@ -131,3 +135,4 @@ exclude = "wireup.integration"
|
|
|
131
135
|
|
|
132
136
|
[tool.pytest.ini_options]
|
|
133
137
|
asyncio_mode = "auto"
|
|
138
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
[](https://github.com/maldoinc/wireup)
|
|
7
7
|
[](https://pypi.org/project/wireup/)
|
|
8
8
|
[](https://pypi.org/project/wireup/)
|
|
9
|
-
|
|
10
|
-
<p><a target="_blank" href="https://maldoinc.github.io/wireup">📚 Documentation</a> | <a target="_blank" href="https://github.com/maldoinc/wireup-demo">🎮 Demo Application</a></p>
|
|
9
|
+
[](https://maldoinc.github.io/wireup)
|
|
11
10
|
</div>
|
|
12
11
|
|
|
13
12
|
Dependency Injection (DI) is a design pattern where dependencies are provided externally rather than created within objects. Wireup automates dependency management using Python's type system, with support for async, generators, modern Python features and integrations for FastAPI, Django, Flask and AIOHTTP out of the box.
|
|
@@ -37,19 +36,32 @@ user_service = container.get(UserService) # ✅ Dependencies resolved.
|
|
|
37
36
|
```
|
|
38
37
|
|
|
39
38
|
<details>
|
|
40
|
-
<summary>
|
|
39
|
+
<summary>No annotations</summary>
|
|
40
|
+
|
|
41
|
+
Keep domain objects clean of framework annotations by using factories.
|
|
41
42
|
|
|
42
43
|
```python
|
|
43
|
-
|
|
44
|
+
# Clean domain objects: No annotations
|
|
44
45
|
class Database:
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
class UserService:
|
|
49
|
+
def __init__(self, db: Database) -> None:
|
|
50
|
+
self.db = db
|
|
51
|
+
|
|
52
|
+
# Register services via factories
|
|
53
|
+
@service
|
|
54
|
+
def database_factory() -> Database:
|
|
55
|
+
return Database()
|
|
56
|
+
|
|
57
|
+
@service
|
|
58
|
+
def user_service_factory(db: Database) -> UserService:
|
|
59
|
+
return UserService(db)
|
|
47
60
|
|
|
48
61
|
container = wireup.create_sync_container(
|
|
49
|
-
services=[
|
|
50
|
-
parameters={"db_url": os.environ["APP_DB_URL"]}
|
|
62
|
+
services=[database_factory, user_service_factory]
|
|
51
63
|
)
|
|
52
|
-
|
|
64
|
+
user_service = container.get(UserService) # ✅ Dependencies resolved.
|
|
53
65
|
```
|
|
54
66
|
|
|
55
67
|
</details>
|
|
@@ -191,7 +203,7 @@ Wireup provides its own Dependency Injection mechanism and is not tied to specif
|
|
|
191
203
|
|
|
192
204
|
Share the service layer between your web application and its accompanying CLI using Wireup.
|
|
193
205
|
|
|
194
|
-
### 🔌 Native Integration with Django, FastAPI, Flask and
|
|
206
|
+
### 🔌 Native Integration with Django, FastAPI, Flask, AIOHTTP, Click and Starlette
|
|
195
207
|
|
|
196
208
|
Integrate with popular frameworks for a smoother developer experience.
|
|
197
209
|
Integrations manage request scopes, injection in endpoints, and lifecycle of services.
|
|
@@ -207,6 +219,10 @@ def users_list(user_service: Injected[UserService]):
|
|
|
207
219
|
wireup.integration.fastapi.setup(container, app)
|
|
208
220
|
```
|
|
209
221
|
|
|
222
|
+
**Supported Frameworks:** FastAPI (with zero-overhead class-based handlers), Django, Flask, AIOHTTP, Starlette, and Click.
|
|
223
|
+
|
|
224
|
+
[View all integrations →](https://maldoinc.github.io/wireup/latest/integrations/)
|
|
225
|
+
|
|
210
226
|
### 🧪 Simplified Testing
|
|
211
227
|
|
|
212
228
|
Wireup does not patch your services and lets you test them in isolation.
|
|
@@ -225,7 +241,3 @@ with container.override.service(target=Database, new=in_memory_database):
|
|
|
225
241
|
## 📚 Documentation
|
|
226
242
|
|
|
227
243
|
For more information [check out the documentation](https://maldoinc.github.io/wireup)
|
|
228
|
-
|
|
229
|
-
## 🎮 Demo application
|
|
230
|
-
|
|
231
|
-
A demo flask application is available at [maldoinc/wireup-demo](https://github.com/maldoinc/wireup-demo)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import functools
|
|
5
|
+
import inspect
|
|
6
|
+
from contextlib import AsyncExitStack, ExitStack
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
8
|
+
|
|
9
|
+
from wireup.errors import WireupError
|
|
10
|
+
from wireup.ioc.container.async_container import AsyncContainer, ScopedAsyncContainer, async_container_force_sync_scope
|
|
11
|
+
from wireup.ioc.container.sync_container import SyncContainer
|
|
12
|
+
from wireup.ioc.types import AnnotatedParameter, ParameterWrapper
|
|
13
|
+
from wireup.ioc.util import (
|
|
14
|
+
get_inject_annotated_parameters,
|
|
15
|
+
get_valid_injection_annotated_parameters,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from wireup.ioc.container.sync_container import ScopedSyncContainer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def inject_from_container_unchecked(
|
|
23
|
+
scoped_container_supplier: Callable[[], ScopedSyncContainer | ScopedAsyncContainer],
|
|
24
|
+
) -> Callable[..., Any]:
|
|
25
|
+
"""Inject dependencies into the decorated function. The "unchecked" part of the name refers to the fact that
|
|
26
|
+
this cannot perform validation on the parameters to inject on module import time due to the absence of a container
|
|
27
|
+
instance."""
|
|
28
|
+
|
|
29
|
+
def _decorator(target: Callable[..., Any]) -> Callable[..., Any]:
|
|
30
|
+
return inject_from_container_util(
|
|
31
|
+
target=target,
|
|
32
|
+
names_to_inject=get_inject_annotated_parameters(target),
|
|
33
|
+
container=None,
|
|
34
|
+
scoped_container_supplier=scoped_container_supplier,
|
|
35
|
+
middleware=None,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return _decorator
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def inject_from_container(
|
|
42
|
+
container: SyncContainer | AsyncContainer,
|
|
43
|
+
scoped_container_supplier: Callable[[], ScopedSyncContainer | ScopedAsyncContainer] | None = None,
|
|
44
|
+
middleware: Callable[
|
|
45
|
+
[ScopedSyncContainer | ScopedAsyncContainer, tuple[Any, ...], dict[str, Any]],
|
|
46
|
+
contextlib.AbstractContextManager[None],
|
|
47
|
+
]
|
|
48
|
+
| None = None,
|
|
49
|
+
) -> Callable[..., Any]:
|
|
50
|
+
"""Inject dependencies into the decorated function based on annotations.
|
|
51
|
+
|
|
52
|
+
:param container: The main container instance created via `wireup.create_sync_container` or
|
|
53
|
+
`wireup.create_async_container`.
|
|
54
|
+
:param scoped_container_supplier: An optional callable that returns the current scoped container instance.
|
|
55
|
+
If provided, it will be used to create scoped dependencies. If not provided, the container will automatically
|
|
56
|
+
enter a scope. Provide a scoped_container_supplier if you need to manage the container's scope manually. For
|
|
57
|
+
example, in web frameworks, you might enter the scope at the start of a request in middleware so that other
|
|
58
|
+
middlewares can access the scoped container if needed.
|
|
59
|
+
:param middleware: A context manager that wraps the execution of the target function.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def _decorator(target: Callable[..., Any]) -> Callable[..., Any]:
|
|
63
|
+
if inspect.iscoroutinefunction(target) and isinstance(container, SyncContainer):
|
|
64
|
+
msg = (
|
|
65
|
+
"Sync container cannot perform injection on async targets. "
|
|
66
|
+
"Create an async container via wireup.create_async_container."
|
|
67
|
+
)
|
|
68
|
+
raise WireupError(msg)
|
|
69
|
+
|
|
70
|
+
return inject_from_container_util(
|
|
71
|
+
target=target,
|
|
72
|
+
names_to_inject=get_valid_injection_annotated_parameters(container, target),
|
|
73
|
+
container=container,
|
|
74
|
+
scoped_container_supplier=scoped_container_supplier,
|
|
75
|
+
middleware=middleware,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return _decorator
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def inject_from_container_util( # noqa: C901
|
|
82
|
+
target: Callable[..., Any],
|
|
83
|
+
names_to_inject: dict[str, AnnotatedParameter],
|
|
84
|
+
container: SyncContainer | AsyncContainer | None,
|
|
85
|
+
scoped_container_supplier: Callable[[], ScopedSyncContainer | ScopedAsyncContainer] | None = None,
|
|
86
|
+
middleware: Callable[
|
|
87
|
+
[ScopedSyncContainer | ScopedAsyncContainer, tuple[Any, ...], dict[str, Any]],
|
|
88
|
+
contextlib.AbstractContextManager[None],
|
|
89
|
+
]
|
|
90
|
+
| None = None,
|
|
91
|
+
) -> Callable[..., Any]:
|
|
92
|
+
if not (container or scoped_container_supplier):
|
|
93
|
+
msg = "Container or scoped_container_supplier must be provided for injection."
|
|
94
|
+
raise WireupError(msg)
|
|
95
|
+
|
|
96
|
+
if not names_to_inject:
|
|
97
|
+
return target
|
|
98
|
+
|
|
99
|
+
if inspect.iscoroutinefunction(target):
|
|
100
|
+
|
|
101
|
+
@functools.wraps(target)
|
|
102
|
+
async def _inject_async_target(*args: Any, **kwargs: Any) -> Any:
|
|
103
|
+
async with AsyncExitStack() as cm:
|
|
104
|
+
if scoped_container_supplier:
|
|
105
|
+
scoped_container = scoped_container_supplier()
|
|
106
|
+
elif container:
|
|
107
|
+
scoped_container = await cm.enter_async_context(container.enter_scope()) # type: ignore[reportArgumentType, arg-type, unused-ignore]
|
|
108
|
+
else:
|
|
109
|
+
msg = "scoped_container_supplier or container must be provided for injection."
|
|
110
|
+
raise ValueError(msg)
|
|
111
|
+
|
|
112
|
+
if middleware:
|
|
113
|
+
cm.enter_context(middleware(scoped_container, args, kwargs))
|
|
114
|
+
|
|
115
|
+
injected_names = {
|
|
116
|
+
name: scoped_container.params.get(param.annotation.param)
|
|
117
|
+
if isinstance(param.annotation, ParameterWrapper)
|
|
118
|
+
else await scoped_container.get(param.klass, qualifier=param.qualifier_value)
|
|
119
|
+
for name, param in names_to_inject.items()
|
|
120
|
+
if param.annotation
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return await target(*args, **{**kwargs, **injected_names})
|
|
124
|
+
|
|
125
|
+
return _inject_async_target
|
|
126
|
+
|
|
127
|
+
@functools.wraps(target)
|
|
128
|
+
def _inject_target(*args: Any, **kwargs: Any) -> Any:
|
|
129
|
+
with ExitStack() as cm:
|
|
130
|
+
if scoped_container_supplier:
|
|
131
|
+
scoped_container = scoped_container_supplier()
|
|
132
|
+
elif container:
|
|
133
|
+
scoped_container = cm.enter_context(
|
|
134
|
+
container.enter_scope()
|
|
135
|
+
if isinstance(container, SyncContainer)
|
|
136
|
+
else async_container_force_sync_scope(container)
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
msg = "scoped_container_supplier or container must be provided for injection."
|
|
140
|
+
raise ValueError(msg)
|
|
141
|
+
|
|
142
|
+
if middleware:
|
|
143
|
+
cm.enter_context(middleware(scoped_container, args, kwargs))
|
|
144
|
+
|
|
145
|
+
get = scoped_container._synchronous_get
|
|
146
|
+
|
|
147
|
+
injected_names = {
|
|
148
|
+
name: scoped_container.params.get(param.annotation.param)
|
|
149
|
+
if isinstance(param.annotation, ParameterWrapper)
|
|
150
|
+
else get(param.klass, qualifier=param.qualifier_value)
|
|
151
|
+
for name, param in names_to_inject.items()
|
|
152
|
+
if param.annotation
|
|
153
|
+
}
|
|
154
|
+
return target(*args, **{**kwargs, **injected_names})
|
|
155
|
+
|
|
156
|
+
return _inject_target
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
from typing import TYPE_CHECKING, Any
|
|
4
5
|
|
|
5
6
|
if TYPE_CHECKING:
|
|
@@ -90,9 +91,20 @@ class UnknownOverrideRequestedError(WireupError):
|
|
|
90
91
|
super().__init__(f"Cannot override unknown {klass} with qualifier '{qualifier}'.")
|
|
91
92
|
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
"""Contains a list of exceptions raised while closing the container."""
|
|
94
|
+
if sys.version_info >= (3, 11):
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
class ContainerCloseError(ExceptionGroup, WireupError): # noqa: F821
|
|
97
|
+
"""Contains a list of exceptions raised while closing the container."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, message: str, errors: list[Exception]) -> None:
|
|
100
|
+
self.errors = errors
|
|
101
|
+
super().__init__(message, errors)
|
|
102
|
+
|
|
103
|
+
else:
|
|
104
|
+
|
|
105
|
+
class ContainerCloseError(WireupError):
|
|
106
|
+
"""Contains a list of exceptions raised while closing the container."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, message: str, errors: list[Exception]) -> None:
|
|
109
|
+
self.errors = errors
|
|
110
|
+
super().__init__(f"{message}: {errors}")
|
|
@@ -73,10 +73,13 @@ async def _instantiate_class_based_handlers(
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def _get_startup_event(
|
|
76
|
-
container: wireup.AsyncContainer,
|
|
76
|
+
container: wireup.AsyncContainer,
|
|
77
|
+
handlers: Optional[Iterable[Type[_WireupHandler]]],
|
|
77
78
|
) -> Callable[[web.Application], Awaitable[None]]:
|
|
78
79
|
for handler_type in handlers or []:
|
|
79
|
-
container._registry.
|
|
80
|
+
container._registry.extend(impls=[ServiceDeclaration(handler_type)])
|
|
81
|
+
container._compiler.compile()
|
|
82
|
+
container._scoped_compiler.compile()
|
|
80
83
|
|
|
81
84
|
async def _inner(app: web.Application) -> None:
|
|
82
85
|
if handlers:
|
|
@@ -88,7 +91,9 @@ def _get_startup_event(
|
|
|
88
91
|
|
|
89
92
|
|
|
90
93
|
def setup(
|
|
91
|
-
container: wireup.AsyncContainer,
|
|
94
|
+
container: wireup.AsyncContainer,
|
|
95
|
+
app: web.Application,
|
|
96
|
+
handlers: Optional[Iterable[Type[_WireupHandler]]] = None,
|
|
92
97
|
) -> None:
|
|
93
98
|
"""Integrate Wireup with AIOHTTP.
|
|
94
99
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from click import Group
|
|
2
|
+
|
|
3
|
+
from wireup import SyncContainer, inject_from_container
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _inject_commands(container: SyncContainer, group: Group) -> None:
|
|
7
|
+
for command in group.commands.values():
|
|
8
|
+
if fn := command.callback:
|
|
9
|
+
command.callback = inject_from_container(container)(fn)
|
|
10
|
+
|
|
11
|
+
if isinstance(command, Group):
|
|
12
|
+
_inject_commands(container, command)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def setup(container: SyncContainer, command: Group) -> None:
|
|
16
|
+
"""Integrate Wireup with Click by injecting dependencies into Click commands.
|
|
17
|
+
|
|
18
|
+
:command: The Click command group to inject dependencies into
|
|
19
|
+
"""
|
|
20
|
+
_inject_commands(container, command)
|
|
21
|
+
command.wireup_container = container # type: ignore[reportAttributeAccessIssue]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_app_container(app: Group) -> SyncContainer:
|
|
25
|
+
"""Retrieve the Wireup container associated with a Click command group."""
|
|
26
|
+
return app.wireup_container # type: ignore[reportAttributeAccessIssue]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import functools
|
|
3
2
|
import importlib
|
|
3
|
+
import inspect
|
|
4
4
|
from contextvars import ContextVar
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from types import ModuleType
|
|
@@ -21,7 +21,7 @@ from wireup.errors import WireupError
|
|
|
21
21
|
from wireup.ioc.container.async_container import AsyncContainer, ScopedAsyncContainer, async_container_force_sync_scope
|
|
22
22
|
from wireup.ioc.container.sync_container import ScopedSyncContainer
|
|
23
23
|
from wireup.ioc.types import ParameterWrapper
|
|
24
|
-
from wireup.ioc.
|
|
24
|
+
from wireup.ioc.util import get_valid_injection_annotated_parameters
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
27
27
|
from wireup.integration.django import WireupSettings
|
|
@@ -38,7 +38,7 @@ def wireup_middleware(
|
|
|
38
38
|
) -> Callable[[HttpRequest], Union[HttpResponse, Awaitable[HttpResponse]]]:
|
|
39
39
|
container = get_app_container()
|
|
40
40
|
|
|
41
|
-
if
|
|
41
|
+
if inspect.iscoroutinefunction(get_response):
|
|
42
42
|
|
|
43
43
|
async def async_inner(request: HttpRequest) -> HttpResponse:
|
|
44
44
|
async with container.enter_scope() as scoped:
|
|
@@ -65,7 +65,7 @@ def wireup_middleware(
|
|
|
65
65
|
return sync_inner
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
@service
|
|
68
|
+
@service(lifetime="scoped")
|
|
69
69
|
def _django_request_factory() -> HttpRequest:
|
|
70
70
|
try:
|
|
71
71
|
return current_request.get()
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import contextlib
|
|
2
|
-
from contextvars import ContextVar
|
|
3
2
|
from typing import (
|
|
4
3
|
Any,
|
|
5
4
|
AsyncIterator,
|
|
6
|
-
Awaitable,
|
|
7
5
|
Callable,
|
|
8
6
|
Dict,
|
|
9
7
|
Iterable,
|
|
@@ -16,26 +14,38 @@ from typing import (
|
|
|
16
14
|
)
|
|
17
15
|
|
|
18
16
|
import fastapi
|
|
19
|
-
from fastapi import FastAPI
|
|
20
|
-
from fastapi.requests import HTTPConnection
|
|
17
|
+
from fastapi import FastAPI
|
|
21
18
|
from fastapi.routing import APIRoute, APIWebSocketRoute
|
|
22
|
-
from starlette.middleware.base import BaseHTTPMiddleware
|
|
23
19
|
from starlette.routing import BaseRoute
|
|
24
20
|
from typing_extensions import Protocol
|
|
25
21
|
|
|
26
|
-
from wireup import inject_from_container
|
|
22
|
+
from wireup import inject_from_container
|
|
27
23
|
from wireup._annotations import ServiceDeclaration
|
|
28
24
|
from wireup.errors import WireupError
|
|
25
|
+
from wireup.integration.starlette import (
|
|
26
|
+
WireupAsgiMiddleware,
|
|
27
|
+
current_request,
|
|
28
|
+
get_app_container,
|
|
29
|
+
get_request_container,
|
|
30
|
+
request_factory,
|
|
31
|
+
websocket_factory,
|
|
32
|
+
)
|
|
29
33
|
from wireup.ioc.container.async_container import AsyncContainer, ScopedAsyncContainer
|
|
30
34
|
from wireup.ioc.container.sync_container import ScopedSyncContainer
|
|
31
35
|
from wireup.ioc.types import AnyCallable
|
|
32
|
-
from wireup.ioc.
|
|
33
|
-
assert_dependencies_valid,
|
|
36
|
+
from wireup.ioc.util import (
|
|
34
37
|
get_inject_annotated_parameters,
|
|
35
38
|
hide_annotated_names,
|
|
36
39
|
)
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
__all__ = [
|
|
42
|
+
"WireupRoute",
|
|
43
|
+
"get_app_container",
|
|
44
|
+
"get_request_container",
|
|
45
|
+
"request_factory",
|
|
46
|
+
"setup",
|
|
47
|
+
"websocket_factory",
|
|
48
|
+
]
|
|
39
49
|
|
|
40
50
|
|
|
41
51
|
class _ClassBasedHandlersProtocol(Protocol):
|
|
@@ -48,50 +58,6 @@ class WireupRoute(APIRoute):
|
|
|
48
58
|
super().__init__(path=path, endpoint=endpoint, **kwargs)
|
|
49
59
|
|
|
50
60
|
|
|
51
|
-
@service(lifetime="scoped")
|
|
52
|
-
def fastapi_request_factory() -> Request:
|
|
53
|
-
"""Provide the current FastAPI request as a dependency.
|
|
54
|
-
|
|
55
|
-
Note that this requires the Wireup-FastAPI integration to be set up.
|
|
56
|
-
"""
|
|
57
|
-
msg = "fastapi.Request in Wireup is only available during a request."
|
|
58
|
-
try:
|
|
59
|
-
res = current_request.get()
|
|
60
|
-
if not isinstance(res, Request):
|
|
61
|
-
raise WireupError(msg)
|
|
62
|
-
|
|
63
|
-
return res
|
|
64
|
-
except LookupError as e:
|
|
65
|
-
raise WireupError(msg) from e
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@service(lifetime="scoped")
|
|
69
|
-
def fastapi_websocket_factory() -> WebSocket:
|
|
70
|
-
"""Provide the current FastAPI WebSocket as a dependency.
|
|
71
|
-
|
|
72
|
-
Note that this requires the Wireup-FastAPI integration to be set up.
|
|
73
|
-
"""
|
|
74
|
-
msg = "fastapi.WebSocket in Wireup is only available in a websocket connection."
|
|
75
|
-
try:
|
|
76
|
-
res = current_request.get()
|
|
77
|
-
if not isinstance(res, WebSocket):
|
|
78
|
-
raise WireupError(msg)
|
|
79
|
-
|
|
80
|
-
return res
|
|
81
|
-
except LookupError as e:
|
|
82
|
-
raise WireupError(msg) from e
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
async def _wireup_request_middleware(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
|
86
|
-
token = current_request.set(request)
|
|
87
|
-
try:
|
|
88
|
-
async with request.app.state.wireup_container.enter_scope() as scoped_container:
|
|
89
|
-
request.state.wireup_container = scoped_container
|
|
90
|
-
return await call_next(request)
|
|
91
|
-
finally:
|
|
92
|
-
current_request.reset(token)
|
|
93
|
-
|
|
94
|
-
|
|
95
61
|
def _inject_fastapi_route(
|
|
96
62
|
*,
|
|
97
63
|
container: AsyncContainer,
|
|
@@ -100,14 +66,13 @@ def _inject_fastapi_route(
|
|
|
100
66
|
remove_http_connection_from_arguments: bool,
|
|
101
67
|
add_custom_middleware: bool,
|
|
102
68
|
) -> AnyCallable:
|
|
103
|
-
# Warn: Make sure the logic evolves with the _wireup_request_middleware function.
|
|
104
69
|
@contextlib.contextmanager
|
|
105
70
|
def _request_middleware(
|
|
106
71
|
scoped_container: Union[ScopedAsyncContainer, ScopedSyncContainer],
|
|
107
72
|
_args: Tuple[Any, ...],
|
|
108
73
|
kwargs: Dict[str, Any],
|
|
109
74
|
) -> Iterator[None]:
|
|
110
|
-
request
|
|
75
|
+
request = kwargs[http_connection_param_name]
|
|
111
76
|
request.state.wireup_container = scoped_container
|
|
112
77
|
token = current_request.set(request)
|
|
113
78
|
try:
|
|
@@ -135,8 +100,7 @@ def _inject_routes(container: AsyncContainer, routes: List[BaseRoute], *, is_usi
|
|
|
135
100
|
route.dependant.call = inject_from_container(container, get_request_container)(route.dependant.call)
|
|
136
101
|
continue
|
|
137
102
|
|
|
138
|
-
# This is now either a websocket route
|
|
139
|
-
# or an APIRoute but the asgi middleware is not used.
|
|
103
|
+
# This is now either a websocket route or an APIRoute but the asgi middleware is not used.
|
|
140
104
|
# In this case we need to use the custom route middleware to extract the current request/websocket.
|
|
141
105
|
add_custom_middleware = isinstance(route, APIWebSocketRoute) or not is_using_asgi_middleware
|
|
142
106
|
is_http_connection_in_signature = route.dependant.http_connection_param_name is not None
|
|
@@ -201,8 +165,9 @@ def _update_lifespan(
|
|
|
201
165
|
async def lifespan(app: FastAPI) -> AsyncIterator[Any]:
|
|
202
166
|
if class_based_routes:
|
|
203
167
|
for cbr in class_based_routes:
|
|
204
|
-
container._registry.
|
|
205
|
-
|
|
168
|
+
container._registry.extend(impls=[ServiceDeclaration(cbr)])
|
|
169
|
+
container._compiler.compile()
|
|
170
|
+
container._scoped_compiler.compile()
|
|
206
171
|
|
|
207
172
|
for cbr in class_based_routes:
|
|
208
173
|
await _instantiate_class_based_route(app, container, cbr)
|
|
@@ -242,7 +207,7 @@ def setup(
|
|
|
242
207
|
"""
|
|
243
208
|
app.state.wireup_container = container
|
|
244
209
|
if middleware_mode:
|
|
245
|
-
app.add_middleware(
|
|
210
|
+
app.add_middleware(WireupAsgiMiddleware)
|
|
246
211
|
_update_lifespan(
|
|
247
212
|
app,
|
|
248
213
|
class_based_routes=class_based_handlers,
|
|
@@ -253,13 +218,3 @@ def setup(
|
|
|
253
218
|
# If no class-based handlers are used, we inject them immediately.
|
|
254
219
|
if not class_based_handlers:
|
|
255
220
|
_inject_routes(container, app.routes, is_using_asgi_middleware=middleware_mode)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def get_app_container(app: FastAPI) -> AsyncContainer:
|
|
259
|
-
"""Return the container associated with the given FastAPI application."""
|
|
260
|
-
return app.state.wireup_container
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def get_request_container() -> ScopedAsyncContainer:
|
|
264
|
-
"""When inside a request, returns the scoped container instance handling the current request."""
|
|
265
|
-
return current_request.get().state.wireup_container
|