Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ app.router.add_view("/users", Users)

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`.

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.

## API

| Symbol | Description |
Expand Down
6 changes: 5 additions & 1 deletion modern_di_aiohttp/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,17 @@ async def _di_middleware(
async with fetch_di_container(request.app).build_child_container(
scope=match.scope, context=match.context
) as child_container:
outer_container = request.get(_CONTAINER_REQUEST_KEY)
request[_CONTAINER_REQUEST_KEY] = child_container
try:
return await handler(request)
finally:
# The container's context holds this request, so leaving the entry behind closes a
# cycle and the request could only be reclaimed by the GC.
del request[_CONTAINER_REQUEST_KEY]
if outer_container is None:
del request[_CONTAINER_REQUEST_KEY]
else:
request[_CONTAINER_REQUEST_KEY] = outer_container


def setup_di(app: web.Application, container: Container) -> Container:
Expand Down
90 changes: 90 additions & 0 deletions tests/test_subapps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import collections.abc
import dataclasses
import http
import typing

from aiohttp import web
from aiohttp.test_utils import TestClient
from modern_di import Container, Group, Scope, providers
from modern_di.providers import CacheSettings

from modern_di_aiohttp import FromDI, fetch_request_container, inject, setup_di


AiohttpClient = collections.abc.Callable[[web.Application], collections.abc.Awaitable[TestClient]]


@dataclasses.dataclass(kw_only=True, slots=True)
class Label:
name: str


def make_group(name: str, finalized: list[str]) -> type[Group]:
class AppDependencies(Group):
label = providers.Factory(
scope=Scope.REQUEST,
creator=lambda: Label(name=name),
bound_type=Label,
cache=CacheSettings(finalizer=lambda label: finalized.append(label.name)),
)

return AppDependencies


@inject
async def label_view(
request: web.Request, # noqa: ARG001
label: typing.Annotated[Label, FromDI(Label)],
) -> web.Response:
return web.Response(text=label.name)


@web.middleware
async def resolve_label_middleware(
request: web.Request,
handler: typing.Callable[[web.Request], typing.Awaitable[web.StreamResponse]],
) -> web.StreamResponse:
fetch_request_container(request).resolve(Label)
return await handler(request)


def build_apps(*, sub_has_di: bool, finalized: list[str]) -> web.Application:
sub = web.Application()
sub.router.add_get("/x", label_view)
if sub_has_di:
setup_di(sub, Container(groups=[make_group("sub", finalized)]))

parent = web.Application()
parent.router.add_get("/x", label_view)
setup_di(parent, Container(groups=[make_group("parent", finalized)]))
parent.middlewares.append(resolve_label_middleware)
parent.add_subapp("/sub", sub)
return parent


async def test_each_app_resolves_from_its_own_container(aiohttp_client: AiohttpClient) -> None:
client = await aiohttp_client(build_apps(sub_has_di=True, finalized=[]))

parent_response = await client.get("/x")
assert parent_response.status == http.HTTPStatus.OK
assert await parent_response.text() == "parent"

sub_response = await client.get("/sub/x")
assert sub_response.status == http.HTTPStatus.OK
assert await sub_response.text() == "sub"


async def test_sub_app_without_setup_di_resolves_from_parent(aiohttp_client: AiohttpClient) -> None:
client = await aiohttp_client(build_apps(sub_has_di=False, finalized=[]))

response = await client.get("/sub/x")
assert response.status == http.HTTPStatus.OK
assert await response.text() == "parent"


async def test_both_child_containers_closed_after_sub_app_request(aiohttp_client: AiohttpClient) -> None:
finalized: list[str] = []
client = await aiohttp_client(build_apps(sub_has_di=True, finalized=finalized))

assert (await client.get("/sub/x")).status == http.HTTPStatus.OK
assert sorted(finalized) == ["parent", "sub"]
Loading