Skip to content

Commit 3efccfa

Browse files
authored
docs: rework the web integrations pages (#665)
Fix the examples that could not serve a request (FastAPI and Starlette websockets/endpoints without setup_di, the unregistered Litestar autowired route), add test-client checks to every page, document root-container lifecycle, websocket per-message scope, and the current Litestar di_container and Flask async-view limits.
1 parent 3a903a3 commit 3efccfa

5 files changed

Lines changed: 596 additions & 437 deletions

File tree

‎docs/integrations/aiohttp.md‎

Lines changed: 159 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
# Usage with `aiohttp`
22

3-
aiohttp has no dependency-injection system of its own, so `modern-di-aiohttp`
4-
uses the `@inject` decorator with `FromDI` markers. `setup_di` opens the root
5-
container on app startup, closes it on cleanup, and installs middleware that
6-
opens a per-connection child container automatically.
3+
aiohttp has no dependency injection of its own, so `modern-di-aiohttp` provides an `@inject`
4+
decorator that fills the handler parameters marked with `FromDI`. `setup_di` installs a middleware
5+
that gives each HTTP request a `Scope.REQUEST` child container and each websocket connection a
6+
`Scope.SESSION` one. It also hooks the app's startup and cleanup signals, and the cleanup closes
7+
the root container.
78

8-
## How to use
9-
10-
### 1. Install `modern-di-aiohttp`
9+
## Installation
1110

1211
=== "uv"
1312

@@ -27,7 +26,7 @@ opens a per-connection child container automatically.
2726
poetry add modern-di-aiohttp
2827
```
2928

30-
### 2. Apply to your application
29+
## Usage
3130

3231
```python
3332
import dataclasses
@@ -68,89 +67,177 @@ app = web.Application()
6867
app.router.add_get("/report", get_report)
6968
container = Container(groups=[AppGroup])
7069
setup_di(app, container)
71-
container.validate() # after setup_di — its connection providers are now registered
70+
container.validate()
7271
```
7372

74-
### 3. Scopes
73+
Call `container.validate()` after `setup_di`. A factory that takes a `web.Request` depends on the
74+
context provider that `setup_di` registers, so validating before it fails with
75+
`ValidationFailedError`.
7576

76-
See [the scope hierarchy](../providers/scopes.md#what-each-scope-is-for).
77-
An HTTP request opens a `Scope.REQUEST` child container; a WebSocket connection
78-
opens a `Scope.SESSION` one.
77+
## Injecting into handlers
7978

80-
Which scope gets opened is decided per-connection: the middleware checks the
81-
request's handshake headers (via aiohttp's `can_prepare`), not the route or
82-
handler. A request carrying WebSocket-upgrade headers opens a `Scope.SESSION`
83-
child regardless of which handler ultimately serves it.
79+
`FromDI` takes a type, as above, or a provider such as `FromDI(AppGroup.report)`. `@inject`
80+
resolves it from the connection's child container, which it finds through the `web.Request` the
81+
handler receives. A decorated handler without one among its positional arguments raises
82+
`TypeError`, and a request that did not pass through the `setup_di` middleware raises
83+
`RuntimeError`.
8484

85-
### 4. WebSockets and per-message scope
85+
### Class-based views
8686

87-
A WebSocket handler runs for the whole life of the socket, so its `Scope.SESSION`
88-
container does too. Read the connection with `FromDI(aiohttp_websocket_provider)`.
87+
`@inject` works the same on the methods of a `web.View` subclass. Decorate the method, not the
88+
class. aiohttp calls the method with no arguments and keeps the request on `self.request`, which
89+
is where the decorator reads it from:
8990

90-
Unlike FastAPI, Litestar, and Starlette, aiohttp has no separate WebSocket
91-
object. A WebSocket is an upgraded `web.Request`, so `aiohttp_websocket_provider`
92-
binds `web.Request` too, and is declared `bound_type=None` (not resolvable by
93-
type, because `aiohttp_request_provider` already owns `web.Request`). That is why
94-
you wire it explicitly with `FromDI(aiohttp_websocket_provider)` rather than
95-
by type annotation.
91+
```python
92+
class ReportView(web.View):
93+
@inject
94+
async def get(
95+
self,
96+
report: typing.Annotated[Report, FromDI(Report)],
97+
) -> web.Response:
98+
return web.json_response(report.as_dict())
9699

97-
For per-message work, open a nested `Scope.REQUEST` child of the session
98-
container, fetched via `fetch_request_container`:
99100

100-
```python
101-
import typing
101+
app.router.add_view("/report-view", ReportView)
102+
```
102103

103-
from aiohttp import web
104-
from modern_di import Scope
105-
from modern_di_aiohttp import FromDI, aiohttp_websocket_provider, fetch_request_container, inject
104+
## Scopes and lifecycle
106105

106+
An HTTP request gets a `Scope.REQUEST` child container and a websocket connection a
107+
`Scope.SESSION` one; see [the scope hierarchy](../providers/scopes.md#what-each-scope-is-for). The
108+
middleware builds the child before the handler runs and closes it when the handler returns.
107109

108-
@inject
109-
async def ws_handler(
110-
request: web.Request,
111-
connection: typing.Annotated[web.Request, FromDI(aiohttp_websocket_provider)],
112-
) -> web.WebSocketResponse:
113-
session_container = fetch_request_container(request)
114-
ws = web.WebSocketResponse()
115-
await ws.prepare(request)
116-
async for msg in ws:
117-
if msg.type == web.WSMsgType.TEXT:
118-
async with session_container.build_child_container(scope=Scope.REQUEST) as request_container:
119-
... # resolve REQUEST-scoped providers for this message
120-
return ws
121-
```
110+
The middleware picks the scope from the request's handshake headers, using aiohttp's
111+
`can_prepare`, not from the route or handler. A request that carries websocket-upgrade headers gets
112+
a `Scope.SESSION` child whichever handler serves it. An HTTP-only handler that injects a
113+
REQUEST-scoped provider then fails with `ScopeNotInitializedError`, and the client gets a 500.
122114

123-
## Class-based views
115+
The root container is open from the moment you construct it. `setup_di` reopens it on startup and
116+
closes it with `close_async()` on cleanup. An app attached with `add_subapp` receives both signals
117+
from its parent, so a `setup_di`-wired sub-app is closed too.
124118

125-
`@inject` works the same on the methods of a `web.View` subclass. Decorate the
126-
handler method, not the class. aiohttp calls the method with no arguments and
127-
keeps the request on `self.request`, which is where the decorator reads it from:
119+
## Websockets
128120

129-
```python
130-
import typing
121+
A websocket handler runs for the whole life of the socket, and so does its `Scope.SESSION`
122+
container. `FromDI(Container)` gives the handler that container, as does
123+
`fetch_request_container(request)`. For per-message work, open a `Scope.REQUEST` child of it:
131124

132-
from aiohttp import web
133-
from modern_di_aiohttp import FromDI, inject
125+
```python
126+
@inject
127+
async def report_socket(
128+
request: web.Request,
129+
session_container: typing.Annotated[Container, FromDI(Container)],
130+
) -> web.WebSocketResponse:
131+
socket = web.WebSocketResponse()
132+
await socket.prepare(request)
133+
async for message in socket:
134+
if message.type == web.WSMsgType.TEXT:
135+
async with session_container.build_child_container(scope=Scope.REQUEST) as request_container:
136+
await socket.send_json(request_container.resolve(Report).as_dict())
137+
return socket
134138

135139

136-
class ReportView(web.View):
137-
@inject
138-
async def get(
139-
self,
140-
report: typing.Annotated[Report, FromDI(Report)],
141-
) -> web.Response:
142-
return web.json_response(report.as_dict())
140+
app.router.add_get("/ws", report_socket)
141+
```
143142

143+
<!-- invisible-code-block: python
144+
import warnings
145+
146+
from aiohttp.test_utils import TestClient, TestServer
147+
148+
with warnings.catch_warnings():
149+
warnings.simplefilter("ignore", web.NotAppKeyWarning) # modern-di-aiohttp stores the child under a str key
150+
async with TestClient(TestServer(app)) as client:
151+
for path in ("/report", "/report-view"):
152+
response = await client.get(path)
153+
assert await response.json() == {"service": "catalog"}
154+
async with client.ws_connect("/ws") as socket:
155+
await socket.send_str("report")
156+
assert await socket.receive_json() == {"service": "catalog"}
157+
assert container.closed
158+
-->
159+
160+
Injecting a REQUEST-scoped provider such as `FromDI(Report)` straight into a websocket handler
161+
raises `ScopeNotInitializedError`, because no REQUEST container exists for the connection.
162+
163+
## Framework context objects
164+
165+
The integration makes the current `web.Request` available to your factories. See
166+
[Framework context objects](../providers/context.md#framework-context-objects) for how implicit
167+
and explicit resolution work.
168+
169+
- `aiohttp_request_provider` provides the current `web.Request` of an HTTP request (REQUEST
170+
scope). It is registered by type.
171+
- `aiohttp_websocket_provider` provides the `web.Request` of a websocket connection (SESSION
172+
scope). aiohttp has no separate websocket object, and `aiohttp_request_provider` already owns the
173+
`web.Request` type, so this provider is declared with `bound_type=None`: only a reference to it
174+
resolves.
175+
176+
A factory that takes `request: web.Request` by type resolves only inside an HTTP request. In a
177+
websocket handler the type still points at `aiohttp_request_provider`, which a websocket connection
178+
never sets: resolving from the SESSION container raises `ScopeNotInitializedError`, and from a
179+
per-message REQUEST child `ContextValueNotSetError`. A factory meant for websocket connections
180+
names `aiohttp_websocket_provider` in `kwargs` and is SESSION-scoped:
144181

145-
app.router.add_view("/report", ReportView)
182+
```python
183+
from modern_di_aiohttp import aiohttp_request_provider, aiohttp_websocket_provider
184+
185+
186+
def describe_request(request: web.Request) -> dict[str, str]:
187+
return {"method": request.method, "path": request.path}
188+
189+
190+
class RequestGroup(Group):
191+
by_type = providers.Factory(describe_request, scope=Scope.REQUEST, bound_type=None)
192+
by_provider = providers.Factory(
193+
describe_request,
194+
scope=Scope.REQUEST,
195+
bound_type=None,
196+
kwargs={"request": aiohttp_request_provider},
197+
)
198+
for_websocket = providers.Factory(
199+
describe_request,
200+
scope=Scope.SESSION,
201+
bound_type=None,
202+
kwargs={"request": aiohttp_websocket_provider},
203+
)
146204
```
147205

148-
A view whose `get` upgrades to a WebSocket opens a `Scope.SESSION` container the
149-
same way a function handler does; read the connection with
150-
`FromDI(aiohttp_websocket_provider)` or `self.request`.
206+
<!-- invisible-code-block: python
207+
@inject
208+
async def describe(
209+
request: web.Request,
210+
by_type: typing.Annotated[dict[str, str], FromDI(RequestGroup.by_type)],
211+
by_provider: typing.Annotated[dict[str, str], FromDI(RequestGroup.by_provider)],
212+
) -> web.Response:
213+
return web.json_response([by_type, by_provider])
214+
151215
152-
Requires `modern-di-aiohttp` 3.2.0 or later. Earlier versions raise a `TypeError`
153-
on the first request to a decorated method.
216+
@inject
217+
async def describe_socket(
218+
request: web.Request,
219+
for_websocket: typing.Annotated[dict[str, str], FromDI(RequestGroup.for_websocket)],
220+
) -> web.WebSocketResponse:
221+
socket = web.WebSocketResponse()
222+
await socket.prepare(request)
223+
await socket.send_json(for_websocket)
224+
await socket.close()
225+
return socket
226+
227+
228+
request_app = web.Application()
229+
request_app.router.add_get("/describe", describe)
230+
request_app.router.add_get("/describe-ws", describe_socket)
231+
setup_di(request_app, Container(groups=[RequestGroup])).validate()
232+
233+
with warnings.catch_warnings():
234+
warnings.simplefilter("ignore", web.NotAppKeyWarning)
235+
async with TestClient(TestServer(request_app)) as client:
236+
response = await client.get("/describe")
237+
assert await response.json() == [{"method": "GET", "path": "/describe"}] * 2
238+
async with client.ws_connect("/describe-ws") as socket:
239+
assert await socket.receive_json() == {"method": "GET", "path": "/describe-ws"}
240+
-->
154241

155242
## See also
156243

@@ -163,10 +250,10 @@ on the first request to a decorated method.
163250

164251
| Symbol | Description |
165252
|---|---|
166-
| `setup_di(app, container)` | Opens the root container on startup, closes it on cleanup, and installs the middleware that builds a per-connection child container; returns the container. |
253+
| `setup_di(app, container)` | Stores the container on the app, registers the request and websocket context providers, reopens the container on startup and closes it on cleanup, and installs the middleware that builds a per-connection child container; returns the container. |
167254
| `FromDI(dependency)` | Marker (used with `@inject`) that resolves a provider or type from the per-connection child container. |
168-
| `inject` | Decorator for an `async def handler(request: web.Request, ...)`, a function handler or a method of a `web.View` subclass; resolves its `FromDI`-annotated parameters. Raises `RuntimeError` naming `setup_di` when the request did not pass through the middleware. |
255+
| `inject` | Decorator for an `async def handler(request: web.Request, ...)`, a function handler or a method of a `web.View` subclass; resolves its `FromDI`-annotated parameters. Raises `TypeError` when the handler receives neither a `web.Request` nor a `web.View`, and `RuntimeError` naming `setup_di` when the request did not pass through the middleware. |
169256
| `fetch_di_container(app)` | Returns the root `Container` stored on the app. |
170-
| `fetch_request_container(request)` | Returns the per-connection child container the middleware built (REQUEST for HTTP, SESSION for a WebSocket). Raises `RuntimeError` naming `setup_di` when the request did not pass through the middleware. |
257+
| `fetch_request_container(request)` | Returns the per-connection child container the middleware built (REQUEST for HTTP, SESSION for a websocket). Raises `RuntimeError` naming `setup_di` when the request did not pass through the middleware. |
171258
| `aiohttp_request_provider` | `ContextProvider` for `web.Request` (REQUEST scope), auto-registered by type. |
172-
| `aiohttp_websocket_provider` | `ContextProvider` for the WebSocket connection's `web.Request` (SESSION scope), `bound_type=None`; resolve via `FromDI(aiohttp_websocket_provider)`. |
259+
| `aiohttp_websocket_provider` | `ContextProvider` for the websocket connection's `web.Request` (SESSION scope), `bound_type=None`; resolve it via `FromDI(aiohttp_websocket_provider)` or a `kwargs` reference. |

0 commit comments

Comments
 (0)