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
3332import dataclasses
@@ -68,89 +67,177 @@ app = web.Application()
6867app.router.add_get(" /report" , get_report)
6968container = Container(groups = [AppGroup])
7069setup_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