Skip to content

Commit cab2cbb

Browse files
authored
fix: keep the parent's child container when a sub-app also calls setup_di (#47)
1 parent 42acf1c commit cab2cbb

3 files changed

Lines changed: 97 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ app.router.add_view("/users", Users)
8989

9090
An HTTP request opens a `Scope.REQUEST` child container; a WebSocket connection opens a `Scope.SESSION` one. The connection `aiohttp.web.Request` is resolvable within DI: HTTP handlers and `REQUEST`-scoped factories inject it by type via `aiohttp_request_provider`, while WebSocket handlers read it via `FromDI(aiohttp_websocket_provider)`. For per-message work inside a WebSocket handler, open a nested `Scope.REQUEST` child of the session container fetched with `fetch_request_container`.
9191

92+
Sub-apps added with `app.add_subapp` work with or without their own `setup_di`. When both the parent and the sub-app call `setup_di`, a sub-app route resolves from a child of the sub-app's root container, and the parent's middleware still opens and closes its own child container around it. When only the parent calls `setup_di`, a sub-app route resolves from a child of the parent's root container.
93+
9294
## API
9395

9496
| Symbol | Description |

‎modern_di_aiohttp/main.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,17 @@ async def _di_middleware(
7070
async with fetch_di_container(request.app).build_child_container(
7171
scope=match.scope, context=match.context
7272
) as child_container:
73+
outer_container = request.get(_CONTAINER_REQUEST_KEY)
7374
request[_CONTAINER_REQUEST_KEY] = child_container
7475
try:
7576
return await handler(request)
7677
finally:
7778
# The container's context holds this request, so leaving the entry behind closes a
7879
# cycle and the request could only be reclaimed by the GC.
79-
del request[_CONTAINER_REQUEST_KEY]
80+
if outer_container is None:
81+
del request[_CONTAINER_REQUEST_KEY]
82+
else:
83+
request[_CONTAINER_REQUEST_KEY] = outer_container
8084

8185

8286
def setup_di(app: web.Application, container: Container) -> Container:

‎tests/test_subapps.py‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import collections.abc
2+
import dataclasses
3+
import http
4+
import typing
5+
6+
from aiohttp import web
7+
from aiohttp.test_utils import TestClient
8+
from modern_di import Container, Group, Scope, providers
9+
from modern_di.providers import CacheSettings
10+
11+
from modern_di_aiohttp import FromDI, fetch_request_container, inject, setup_di
12+
13+
14+
AiohttpClient = collections.abc.Callable[[web.Application], collections.abc.Awaitable[TestClient]]
15+
16+
17+
@dataclasses.dataclass(kw_only=True, slots=True)
18+
class Label:
19+
name: str
20+
21+
22+
def make_group(name: str, finalized: list[str]) -> type[Group]:
23+
class AppDependencies(Group):
24+
label = providers.Factory(
25+
scope=Scope.REQUEST,
26+
creator=lambda: Label(name=name),
27+
bound_type=Label,
28+
cache=CacheSettings(finalizer=lambda label: finalized.append(label.name)),
29+
)
30+
31+
return AppDependencies
32+
33+
34+
@inject
35+
async def label_view(
36+
request: web.Request, # noqa: ARG001
37+
label: typing.Annotated[Label, FromDI(Label)],
38+
) -> web.Response:
39+
return web.Response(text=label.name)
40+
41+
42+
@web.middleware
43+
async def resolve_label_middleware(
44+
request: web.Request,
45+
handler: typing.Callable[[web.Request], typing.Awaitable[web.StreamResponse]],
46+
) -> web.StreamResponse:
47+
fetch_request_container(request).resolve(Label)
48+
return await handler(request)
49+
50+
51+
def build_apps(*, sub_has_di: bool, finalized: list[str]) -> web.Application:
52+
sub = web.Application()
53+
sub.router.add_get("/x", label_view)
54+
if sub_has_di:
55+
setup_di(sub, Container(groups=[make_group("sub", finalized)]))
56+
57+
parent = web.Application()
58+
parent.router.add_get("/x", label_view)
59+
setup_di(parent, Container(groups=[make_group("parent", finalized)]))
60+
parent.middlewares.append(resolve_label_middleware)
61+
parent.add_subapp("/sub", sub)
62+
return parent
63+
64+
65+
async def test_each_app_resolves_from_its_own_container(aiohttp_client: AiohttpClient) -> None:
66+
client = await aiohttp_client(build_apps(sub_has_di=True, finalized=[]))
67+
68+
parent_response = await client.get("/x")
69+
assert parent_response.status == http.HTTPStatus.OK
70+
assert await parent_response.text() == "parent"
71+
72+
sub_response = await client.get("/sub/x")
73+
assert sub_response.status == http.HTTPStatus.OK
74+
assert await sub_response.text() == "sub"
75+
76+
77+
async def test_sub_app_without_setup_di_resolves_from_parent(aiohttp_client: AiohttpClient) -> None:
78+
client = await aiohttp_client(build_apps(sub_has_di=False, finalized=[]))
79+
80+
response = await client.get("/sub/x")
81+
assert response.status == http.HTTPStatus.OK
82+
assert await response.text() == "parent"
83+
84+
85+
async def test_both_child_containers_closed_after_sub_app_request(aiohttp_client: AiohttpClient) -> None:
86+
finalized: list[str] = []
87+
client = await aiohttp_client(build_apps(sub_has_di=True, finalized=finalized))
88+
89+
assert (await client.get("/sub/x")).status == http.HTTPStatus.OK
90+
assert sorted(finalized) == ["parent", "sub"]

0 commit comments

Comments
 (0)