diff --git a/docs/integrations/aiohttp.md b/docs/integrations/aiohttp.md index fda53f6c..d3ce1d33 100644 --- a/docs/integrations/aiohttp.md +++ b/docs/integrations/aiohttp.md @@ -1,13 +1,12 @@ # Usage with `aiohttp` -aiohttp has no dependency-injection system of its own, so `modern-di-aiohttp` -uses the `@inject` decorator with `FromDI` markers. `setup_di` opens the root -container on app startup, closes it on cleanup, and installs middleware that -opens a per-connection child container automatically. +aiohttp has no dependency injection of its own, so `modern-di-aiohttp` provides an `@inject` +decorator that fills the handler parameters marked with `FromDI`. `setup_di` installs a middleware +that gives each HTTP request a `Scope.REQUEST` child container and each websocket connection a +`Scope.SESSION` one. It also hooks the app's startup and cleanup signals, and the cleanup closes +the root container. -## How to use - -### 1. Install `modern-di-aiohttp` +## Installation === "uv" @@ -27,7 +26,7 @@ opens a per-connection child container automatically. poetry add modern-di-aiohttp ``` -### 2. Apply to your application +## Usage ```python import dataclasses @@ -68,89 +67,177 @@ app = web.Application() app.router.add_get("/report", get_report) container = Container(groups=[AppGroup]) setup_di(app, container) -container.validate() # after setup_di — its connection providers are now registered +container.validate() ``` -### 3. Scopes +Call `container.validate()` after `setup_di`. A factory that takes a `web.Request` depends on the +context provider that `setup_di` registers, so validating before it fails with +`ValidationFailedError`. -See [the scope hierarchy](../providers/scopes.md#what-each-scope-is-for). -An HTTP request opens a `Scope.REQUEST` child container; a WebSocket connection -opens a `Scope.SESSION` one. +## Injecting into handlers -Which scope gets opened is decided per-connection: the middleware checks the -request's handshake headers (via aiohttp's `can_prepare`), not the route or -handler. A request carrying WebSocket-upgrade headers opens a `Scope.SESSION` -child regardless of which handler ultimately serves it. +`FromDI` takes a type, as above, or a provider such as `FromDI(AppGroup.report)`. `@inject` +resolves it from the connection's child container, which it finds through the `web.Request` the +handler receives. A decorated handler without one among its positional arguments raises +`TypeError`, and a request that did not pass through the `setup_di` middleware raises +`RuntimeError`. -### 4. WebSockets and per-message scope +### Class-based views -A WebSocket handler runs for the whole life of the socket, so its `Scope.SESSION` -container does too. Read the connection with `FromDI(aiohttp_websocket_provider)`. +`@inject` works the same on the methods of a `web.View` subclass. Decorate the method, not the +class. aiohttp calls the method with no arguments and keeps the request on `self.request`, which +is where the decorator reads it from: -Unlike FastAPI, Litestar, and Starlette, aiohttp has no separate WebSocket -object. A WebSocket is an upgraded `web.Request`, so `aiohttp_websocket_provider` -binds `web.Request` too, and is declared `bound_type=None` (not resolvable by -type, because `aiohttp_request_provider` already owns `web.Request`). That is why -you wire it explicitly with `FromDI(aiohttp_websocket_provider)` rather than -by type annotation. +```python +class ReportView(web.View): + @inject + async def get( + self, + report: typing.Annotated[Report, FromDI(Report)], + ) -> web.Response: + return web.json_response(report.as_dict()) -For per-message work, open a nested `Scope.REQUEST` child of the session -container, fetched via `fetch_request_container`: -```python -import typing +app.router.add_view("/report-view", ReportView) +``` -from aiohttp import web -from modern_di import Scope -from modern_di_aiohttp import FromDI, aiohttp_websocket_provider, fetch_request_container, inject +## Scopes and lifecycle +An HTTP request gets a `Scope.REQUEST` child container and a websocket connection a +`Scope.SESSION` one; see [the scope hierarchy](../providers/scopes.md#what-each-scope-is-for). The +middleware builds the child before the handler runs and closes it when the handler returns. -@inject -async def ws_handler( - request: web.Request, - connection: typing.Annotated[web.Request, FromDI(aiohttp_websocket_provider)], -) -> web.WebSocketResponse: - session_container = fetch_request_container(request) - ws = web.WebSocketResponse() - await ws.prepare(request) - async for msg in ws: - if msg.type == web.WSMsgType.TEXT: - async with session_container.build_child_container(scope=Scope.REQUEST) as request_container: - ... # resolve REQUEST-scoped providers for this message - return ws -``` +The middleware picks the scope from the request's handshake headers, using aiohttp's +`can_prepare`, not from the route or handler. A request that carries websocket-upgrade headers gets +a `Scope.SESSION` child whichever handler serves it. An HTTP-only handler that injects a +REQUEST-scoped provider then fails with `ScopeNotInitializedError`, and the client gets a 500. -## Class-based views +The root container is open from the moment you construct it. `setup_di` reopens it on startup and +closes it with `close_async()` on cleanup. An app attached with `add_subapp` receives both signals +from its parent, so a `setup_di`-wired sub-app is closed too. -`@inject` works the same on the methods of a `web.View` subclass. Decorate the -handler method, not the class. aiohttp calls the method with no arguments and -keeps the request on `self.request`, which is where the decorator reads it from: +## Websockets -```python -import typing +A websocket handler runs for the whole life of the socket, and so does its `Scope.SESSION` +container. `FromDI(Container)` gives the handler that container, as does +`fetch_request_container(request)`. For per-message work, open a `Scope.REQUEST` child of it: -from aiohttp import web -from modern_di_aiohttp import FromDI, inject +```python +@inject +async def report_socket( + request: web.Request, + session_container: typing.Annotated[Container, FromDI(Container)], +) -> web.WebSocketResponse: + socket = web.WebSocketResponse() + await socket.prepare(request) + async for message in socket: + if message.type == web.WSMsgType.TEXT: + async with session_container.build_child_container(scope=Scope.REQUEST) as request_container: + await socket.send_json(request_container.resolve(Report).as_dict()) + return socket -class ReportView(web.View): - @inject - async def get( - self, - report: typing.Annotated[Report, FromDI(Report)], - ) -> web.Response: - return web.json_response(report.as_dict()) +app.router.add_get("/ws", report_socket) +``` + + +Injecting a REQUEST-scoped provider such as `FromDI(Report)` straight into a websocket handler +raises `ScopeNotInitializedError`, because no REQUEST container exists for the connection. + +## Framework context objects + +The integration makes the current `web.Request` available to your factories. See +[Framework context objects](../providers/context.md#framework-context-objects) for how implicit +and explicit resolution work. + +- `aiohttp_request_provider` provides the current `web.Request` of an HTTP request (REQUEST + scope). It is registered by type. +- `aiohttp_websocket_provider` provides the `web.Request` of a websocket connection (SESSION + scope). aiohttp has no separate websocket object, and `aiohttp_request_provider` already owns the + `web.Request` type, so this provider is declared with `bound_type=None`: only a reference to it + resolves. + +A factory that takes `request: web.Request` by type resolves only inside an HTTP request. In a +websocket handler the type still points at `aiohttp_request_provider`, which a websocket connection +never sets: resolving from the SESSION container raises `ScopeNotInitializedError`, and from a +per-message REQUEST child `ContextValueNotSetError`. A factory meant for websocket connections +names `aiohttp_websocket_provider` in `kwargs` and is SESSION-scoped: -app.router.add_view("/report", ReportView) +```python +from modern_di_aiohttp import aiohttp_request_provider, aiohttp_websocket_provider + + +def describe_request(request: web.Request) -> dict[str, str]: + return {"method": request.method, "path": request.path} + + +class RequestGroup(Group): + by_type = providers.Factory(describe_request, scope=Scope.REQUEST, bound_type=None) + by_provider = providers.Factory( + describe_request, + scope=Scope.REQUEST, + bound_type=None, + kwargs={"request": aiohttp_request_provider}, + ) + for_websocket = providers.Factory( + describe_request, + scope=Scope.SESSION, + bound_type=None, + kwargs={"request": aiohttp_websocket_provider}, + ) ``` -A view whose `get` upgrades to a WebSocket opens a `Scope.SESSION` container the -same way a function handler does; read the connection with -`FromDI(aiohttp_websocket_provider)` or `self.request`. + ## See also @@ -163,10 +250,10 @@ on the first request to a decorated method. | Symbol | Description | |---|---| -| `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. | +| `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. | | `FromDI(dependency)` | Marker (used with `@inject`) that resolves a provider or type from the per-connection child container. | -| `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. | +| `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. | | `fetch_di_container(app)` | Returns the root `Container` stored on the app. | -| `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. | +| `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. | | `aiohttp_request_provider` | `ContextProvider` for `web.Request` (REQUEST scope), auto-registered by type. | -| `aiohttp_websocket_provider` | `ContextProvider` for the WebSocket connection's `web.Request` (SESSION scope), `bound_type=None`; resolve via `FromDI(aiohttp_websocket_provider)`. | +| `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. | diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md index 5a431067..f34a176e 100644 --- a/docs/integrations/fastapi.md +++ b/docs/integrations/fastapi.md @@ -1,10 +1,14 @@ # Usage with `FastAPI` -*More advanced example of usage with FastAPI: [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template)* +`modern-di-fastapi` builds on FastAPI's own dependency injection: `FromDI` returns a +`fastapi.Depends`, so a handler declares an injected value the same way it declares any other +dependency. Each HTTP request gets a `Scope.REQUEST` child container and each websocket connection a +`Scope.SESSION` one. `setup_di` wraps the app's lifespan, which closes the root container on +shutdown. -## How to use +*More advanced example of usage with FastAPI: [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template)* -### 1. Install `modern-di-fastapi` +## Installation === "uv" @@ -24,7 +28,8 @@ poetry add modern-di-fastapi ``` -### 2. Apply to your application +## Usage + ```python import dataclasses import typing @@ -55,7 +60,7 @@ class AppGroup(Group): app = fastapi.FastAPI() container = Container(groups=[AppGroup]) modern_di_fastapi.setup_di(app, container) -container.validate() # after setup_di — its connection providers are now registered +container.validate() @app.get("/report") @@ -65,15 +70,22 @@ async def get_report( return report.as_dict() ``` -!!! warning "Deployment: mounted sub-apps and disabled lifespan" - FastAPI only opens the root container from the ASGI lifespan event. A - `setup_di`-wired app mounted as a sub-application (`app.mount("/sub", - subapp)`) never receives that event from its parent, and deployments that - disable lifespan (e.g. Mangum `lifespan="off"`) skip it too. Requests - still succeed (the container is already open from construction), but - nothing ever closes it, so its finalizers never run at shutdown. Call - `setup_di` on the top-level served app, or close the root yourself - (`await container.close_async()`) at shutdown. + + +Call `container.validate()` after `setup_di`. A factory that takes a `fastapi.Request` depends on +the context provider that `setup_di` registers, so validating before it fails with +`ValidationFailedError`. ## Coming from `Depends` @@ -85,101 +97,117 @@ closes the request container at the same point. REQUEST-scoped finalizers theref client already has its response, so a commit that fails in a finalizer can no longer change the status code. Commit in the handler or in a service method instead. +That ordering holds from FastAPI 0.118.0. FastAPI 0.106.0 through 0.117.x ran the cleanup before +sending the response, so on those versions a failing REQUEST-scoped finalizer turns the response +into a 500. + FastAPI's `Depends(scope="function" | "request")` decides whether the code after `yield` runs -before or after the response is sent. modern-di's `Scope` is unrelated: it decides how long a -cached instance lives. See [Scopes](../providers/scopes.md). +before or after the response is sent. It needs FastAPI 0.121.0 or later. modern-di's `Scope` is +unrelated: it decides how long a cached instance lives. See [Scopes](../providers/scopes.md). -## Websockets +## Injecting into handlers -Websockets add `SESSION` scope between `APP` and `REQUEST`; see [the scope -hierarchy](../providers/scopes.md#what-each-scope-is-for). `SESSION` covers -the lifetime of the websocket connection and is entered automatically; -`REQUEST` covers one message and must be entered manually: +`FromDI` takes a type, as above, or a provider such as `FromDI(AppGroup.report)`. It resolves from +the connection's child container. -```python -import typing +FastAPI caches a dependency for the rest of the request, so two parameters with the same `FromDI` +get one value. Pass `FromDI(..., use_cache=False)` to resolve again; an uncached factory then builds +a second instance. -import fastapi -import modern_di -import modern_di_fastapi +To get the child container itself, use `FromDI(modern_di.Container)` or +`fastapi.Depends(modern_di_fastapi.build_di_container)`. Both return the same container. +## Scopes and lifecycle -app = fastapi.FastAPI() +An HTTP request gets a `Scope.REQUEST` child container and a websocket connection a +`Scope.SESSION` one; see [the scope hierarchy](../providers/scopes.md#what-each-scope-is-for). +`build_di_container` creates the child as a `yield` dependency, so a handler that injects nothing +gets no child, and FastAPI's dependency cleanup closes it. + +The root container is open from the moment you construct it. `setup_di` wraps the app's lifespan: +a `lifespan=` you passed to `FastAPI()` stays the outer context and the state it yields passes +through, the container reopens on startup, and it closes with `close_async()` on shutdown. The +lifespan shutdown is the only thing that closes the root container. + +!!! warning "Deployment: mounted sub-apps and disabled lifespan" + A `setup_di`-wired app mounted as a sub-application (`app.mount("/sub", subapp)`) never + receives the lifespan events from its parent, and deployments that disable lifespan (for + example Mangum with `lifespan="off"`) skip them too. Requests still succeed, because the root + container is open from construction, but nothing closes it, so its finalizers never run. Call + `setup_di` on the top-level served app, or close the root yourself + (`await container.close_async()`) at shutdown. + +## Websockets +A websocket endpoint injects with `FromDI` too, and resolves from the connection's `Scope.SESSION` +container. That container lives as long as the connection. For per-message work, open a +`Scope.REQUEST` child of it: +```python @app.websocket("/ws") -async def websocket_endpoint( +async def report_socket( websocket: fastapi.WebSocket, - session_container: typing.Annotated[modern_di.Container, fastapi.Depends(modern_di_fastapi.build_di_container)], + session_container: typing.Annotated[Container, modern_di_fastapi.FromDI(Container)], ) -> None: await websocket.accept() - async with session_container.build_child_container(scope=modern_di.Scope.REQUEST) as request_container: - # REQUEST scope is entered here - # You can resolve dependencies here - await websocket.send_text("test") - - await websocket.close() + async for _ in websocket.iter_text(): + async with session_container.build_child_container(scope=Scope.REQUEST) as request_container: + report = request_container.resolve(Report) + await websocket.send_json(report.as_dict()) ``` + + +Injecting a REQUEST-scoped provider such as `FromDI(Report)` straight into a websocket endpoint +raises `ScopeNotInitializedError`, because no REQUEST container exists for the connection. + ## Framework context objects -Framework-specific context objects like `fastapi.Request` and `fastapi.WebSocket` -are automatically made available by the integration. See [Framework context -objects](../providers/context.md#framework-context-objects) for how implicit +The integration makes `fastapi.Request` and `fastapi.WebSocket` available to your factories. See +[Framework context objects](../providers/context.md#framework-context-objects) for how implicit and explicit resolution work. -The following context providers are available for import: - -- `fastapi_request_provider` provides the current `fastapi.Request` object -- `fastapi_websocket_provider` provides the current `fastapi.WebSocket` object +- `fastapi_request_provider` provides the current `fastapi.Request` (REQUEST scope). +- `fastapi_websocket_provider` provides the current `fastapi.WebSocket` (SESSION scope). -### Implicit (type-based) usage +A factory can receive the request by type, or name the provider in `kwargs`: ```python -import fastapi -from modern_di import Group, Scope, providers - +def describe_request(request: fastapi.Request) -> dict[str, str]: + return {"method": request.method, "url": str(request.url)} -def create_request_info(request: fastapi.Request) -> dict[str, str]: - return { - "method": request.method, - "url": str(request.url), - "timestamp": "2023-01-01T00:00:00Z" - } - -class AppGroup(Group): - # Factory automatically resolves the request dependency based on type annotation - request_info = providers.Factory( - create_request_info, +class RequestGroup(Group): + by_type = providers.Factory(describe_request, scope=Scope.REQUEST, bound_type=None) + by_provider = providers.Factory( + describe_request, scope=Scope.REQUEST, + bound_type=None, + kwargs={"request": modern_di_fastapi.fastapi_request_provider}, ) ``` -### Explicit (provider-based) usage - -```python -import fastapi -import modern_di_fastapi -from modern_di import Group, Scope, providers + ## See also @@ -192,9 +220,9 @@ class AppGroup(Group): | Symbol | Description | |---|---| -| `setup_di(app, container)` | Registers the container on the FastAPI app and appends a lifespan that closes it on shutdown (merges with any existing `lifespan=`); returns the container. | -| `FromDI(dependency, *, use_cache=True)` | A `fastapi.Depends` wrapper that resolves a provider (or type) from the per-request child container. Raises `RuntimeError` naming `setup_di` when a request reaches it without `setup_di` called. | -| `build_di_container(connection)` | A `fastapi.Depends` callable that yields the per-request child container: REQUEST scope for an HTTP request, SESSION scope for a WebSocket. | +| `setup_di(app, container)` | Stores the container on the app, registers the request and websocket context providers, and wraps the app's lifespan (any `lifespan=` you passed included) so the container reopens on startup and closes on shutdown; returns the container. | +| `FromDI(dependency, *, use_cache=True)` | A `fastapi.Depends` that resolves a provider or type from the connection's child container (REQUEST for HTTP, SESSION for a websocket). Raises `RuntimeError` naming `setup_di` when a request reaches it without `setup_di` called. | +| `build_di_container(connection)` | A `fastapi.Depends` callable that yields the connection's child container: REQUEST scope for an HTTP request, SESSION scope for a websocket. | | `fastapi_request_provider` | `ContextProvider` for `fastapi.Request` (REQUEST scope), auto-registered. | | `fastapi_websocket_provider` | `ContextProvider` for `fastapi.WebSocket` (SESSION scope), auto-registered. | | `fetch_di_container(app)` | Returns the root `Container` stored on the app. Raises `RuntimeError` naming `setup_di` when called on an app without `setup_di` called. | diff --git a/docs/integrations/flask.md b/docs/integrations/flask.md index 015059cd..acc8f2be 100644 --- a/docs/integrations/flask.md +++ b/docs/integrations/flask.md @@ -1,14 +1,13 @@ # Usage with `Flask` -Flask has no dependency-injection system of its own, so `modern-di-flask` uses -the `@inject` decorator with `FromDI` markers (there is no `Depends`). `setup_di` -installs a `before_request`/`teardown_appcontext` pair that opens a per-request -`Scope.REQUEST` child container and closes it once the request finishes. -Resolution is sync-only, and the child container is closed with `close_sync()`. +Flask has no dependency injection of its own, so `modern-di-flask` provides an `@inject` decorator +that fills the view parameters marked with `FromDI`. `setup_di` installs a +`before_request`/`teardown_appcontext` pair that gives each request a `Scope.REQUEST` child +container and closes it with `close_sync()` once the request finishes. Resolution is sync-only, +and views must be sync too: `@inject` on an `async def` view hands Flask a coroutine, and the +request fails with `TypeError`. -## How to use - -### 1. Install `modern-di-flask` +## Installation === "uv" @@ -28,7 +27,7 @@ Resolution is sync-only, and the child container is closed with `close_sync()`. poetry add modern-di-flask ``` -### 2. Apply to your application +## Usage ```python import dataclasses @@ -52,7 +51,7 @@ class Report: return {"service": self.settings.service_name} -class Dependencies(Group): +class AppGroup(Group): settings = providers.Factory(Settings, scope=Scope.APP, cache=True) report = providers.Factory(Report, scope=Scope.REQUEST) @@ -66,136 +65,139 @@ def get_report(report: typing.Annotated[Report, FromDI(Report)]) -> dict[str, st return report.as_dict() -container = Container(groups=[Dependencies]) +container = Container(groups=[AppGroup]) setup_di(app, container) -container.validate() # after setup_di — its connection providers are now registered +container.validate() ``` -`FromDI(dependency)` accepts either a provider reference or a plain type (as -above), resolved from the per-request child container the middleware built. - -### 3. `auto_inject` + -Pass `auto_inject=True` to `setup_di` to wire every registered view (app routes -and blueprint routes alike) without a per-view `@inject`. Because it walks -`app.view_functions` at call time, `setup_di` must run **after** all routes, -including blueprint routes, have been registered: +Call `container.validate()` after `setup_di`. A factory that takes a `flask.Request` depends on the +context provider that `setup_di` registers, so validating before it fails with +`ValidationFailedError`. -```python -import typing +## Injecting into handlers -from flask import Flask -from modern_di import Container, Group, Scope, providers -from modern_di_flask import FromDI, setup_di +`FromDI` takes a type, as above, or a provider such as `FromDI(AppGroup.report)`, and resolves it +from the request's child container. A decorated view reached without `setup_di` raises +`RuntimeError`. +### `auto_inject` -class Settings: - def __init__(self) -> None: - self.greeting = "hello" +Pass `auto_inject=True` to `setup_di` to wrap every registered view, app routes and blueprint +routes alike, without a per-view `@inject`. A view that already carries `@inject` is left alone. +`setup_di` wraps the views registered at the moment you call it, so call it **after** all routes, +blueprint routes included, are registered. A route added later is not wrapped, and a request to it +fails with `TypeError: get_late_report() missing 1 required positional argument: 'report'`: -class Dependencies(Group): - settings = providers.Factory(scope=Scope.APP, creator=Settings) +```python +auto_app = Flask(__name__) -app = Flask(__name__) +@auto_app.route("/report") +def get_auto_report(report: typing.Annotated[Report, FromDI(Report)]) -> dict[str, str]: + return report.as_dict() -@app.route("/hello/") -def hello(name: str, settings: typing.Annotated[Settings, FromDI(Dependencies.settings)]) -> str: - return f"{settings.greeting}, {name}" +setup_di(auto_app, Container(groups=[AppGroup]), auto_inject=True) -# no @inject needed on individual views -container = Container(groups=[Dependencies]) -setup_di(app, container, auto_inject=True) -container.validate() # after setup_di — its connection providers are now registered +# Broken: registered after setup_di, so never wrapped +@auto_app.route("/late-report") +def get_late_report(report: typing.Annotated[Report, FromDI(Report)]) -> dict[str, str]: + return report.as_dict() ``` -A view that already carries `@inject` is left alone, because `auto_inject` only -wraps views that weren't injected yet. - -### 4. Scopes and request lifecycle - -See [the scope hierarchy](../providers/scopes.md#what-each-scope-is-for). -Flask has no websocket concept, so the integration only ever opens one child -scope: `before_request` builds a `Scope.REQUEST` child of the root container -and stores it on `flask.g`; `teardown_appcontext` closes it with `close_sync()` -once the request (including error handling) is done. - -### 5. Root container teardown - -`setup_di` does not close the root container for you, because Flask has no -application-shutdown hook to run it from. You own root teardown, typically at -your own process-shutdown point: + + +## Scopes and lifecycle + +Flask has no websocket concept, so the integration only ever opens one child scope; see +[the scope hierarchy](../providers/scopes.md#what-each-scope-is-for). `before_request` builds a +`Scope.REQUEST` child of the root container and stores it on `flask.g`, and `teardown_appcontext` +closes it with `close_sync()` once the request, error handling included, is done. + +Because the child closes with `close_sync()`, REQUEST-scoped finalizers must be sync. An async +finalizer raises [`AsyncFinalizerInSyncCloseError`](../troubleshooting/async-finalizer-in-sync-close-error.md), +and it or any other failing finalizer reaches Flask as a `FinalizerError` from the teardown. The +request then fails with a 500 under the Werkzeug server, and the test client raises the error. + +`setup_di` does not close the root container, because Flask has no application-shutdown hook to +run it from. Close it yourself where your process shuts down, for example with `atexit`. That +close is a `close_sync()` too, so APP-scoped finalizers must be sync as well: ```python -from flask import Flask -from modern_di import Container, Group, Scope, providers -from modern_di_flask import fetch_di_container, setup_di - - -class Dependencies(Group): - pass +import atexit +from modern_di_flask import fetch_di_container -app = Flask(__name__) -setup_di(app, Container(groups=[Dependencies])) - -# ... register an atexit hook, a CLI teardown command, or call this -# explicitly wherever your process shuts down: -fetch_di_container(app).close_sync() +atexit.register(fetch_di_container(app).close_sync) ``` ## Framework context objects -`flask.Request` is automatically made available by the integration. See -[Framework context objects](../providers/context.md#framework-context-objects) -for how implicit and explicit resolution work. - -The following context provider is available for import: +The integration makes `flask.Request` available to your factories. See +[Framework context objects](../providers/context.md#framework-context-objects) for how implicit +and explicit resolution work. -- `flask_request_provider` is a `ContextProvider` for the current `flask.Request` (REQUEST scope), auto-registered by type. +- `flask_request_provider` provides the current `flask.Request` (REQUEST scope). It is registered + by type. -### Implicit (type-based) usage +A factory can receive the request by type, or name the provider in `kwargs`: ```python from flask import Request -from modern_di import Group, Scope, providers +from modern_di_flask import flask_request_provider -def create_request_info(request: Request) -> dict[str, str]: +def describe_request(request: Request) -> dict[str, str]: return {"method": request.method, "url": request.url} -class AppGroup(Group): - request_info = providers.Factory(create_request_info, scope=Scope.REQUEST) +class RequestGroup(Group): + by_type = providers.Factory(describe_request, scope=Scope.REQUEST, bound_type=None) + by_provider = providers.Factory( + describe_request, + scope=Scope.REQUEST, + bound_type=None, + kwargs={"request": flask_request_provider}, + ) ``` -### Explicit (provider-based) usage - -```python -from flask import Request -from modern_di import Group, Scope, providers -from modern_di_flask import flask_request_provider + ## See also - [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. -- [Multi-Group organization](../recipes/multi-group.md): structuring a larger container. +- [Multi-group organization](../recipes/multi-group.md): structuring a larger container. - [Lifecycle](../providers/lifecycle.md): finalizers and container teardown. - [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. @@ -203,8 +205,8 @@ class AppGroup(Group): | Symbol | Description | |---|---| -| `setup_di(app, container, *, auto_inject=False)` | Registers the container on `app.extensions`, installs the `before_request`/`teardown_appcontext` pair that builds and closes a per-request `Scope.REQUEST` child container, and, if `auto_inject=True`, wraps every currently-registered view with `inject`; returns the container. | +| `setup_di(app, container, *, auto_inject=False)` | Stores the container on `app.extensions`, registers the request context provider, installs the `before_request`/`teardown_appcontext` pair that builds and closes a per-request `Scope.REQUEST` child container, and, if `auto_inject=True`, wraps every currently registered view with `inject`; returns the container. It does not close the root container. | | `FromDI(dependency)` | Marker (used with `@inject`) that resolves a provider or type from the per-request child container. | -| `inject` | Decorator for a view function; resolves its `FromDI`-annotated parameters without rewriting the function's signature. Raises `RuntimeError` naming `setup_di` when a request reaches it without `setup_di` called. | +| `inject` | Decorator for a sync view function; resolves its `FromDI`-annotated parameters. Raises `RuntimeError` naming `setup_di` when a request reaches it without `setup_di` called. | | `fetch_di_container(app)` | Returns the root `Container` stored on `app.extensions`. | | `flask_request_provider` | `ContextProvider` for `flask.Request` (REQUEST scope), auto-registered by type. | diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md index 5cd5b228..7899aa9e 100644 --- a/docs/integrations/litestar.md +++ b/docs/integrations/litestar.md @@ -1,10 +1,14 @@ # Usage with `Litestar` -*More advanced example of usage with Litestar: [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template)* +`modern-di-litestar` builds on Litestar's own dependency injection. `FromDI` returns a Litestar +`Provide`, which you list in a route's `dependencies` like any other, and the handler declares the +parameter with `NamedDependency[...]`. `ModernDIPlugin` gives each HTTP request a `Scope.REQUEST` +child container and each websocket connection a `Scope.SESSION` one, and adds a lifespan hook that +closes the root container on shutdown. -## How to use +*More advanced example of usage with Litestar: [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template)* -### 1. Install `modern-di-litestar` +## Installation === "uv" @@ -24,7 +28,7 @@ poetry add modern-di-litestar ``` -### 2. Apply to your application +## Usage ```python import dataclasses @@ -63,160 +67,163 @@ app = litestar.Litestar( route_handlers=[get_report], plugins=[modern_di_litestar.ModernDIPlugin(container)], ) -container.validate() # after the plugin is installed — its connection providers are now registered +container.validate() ``` + + Mark every injected parameter with `NamedDependency[...]` from `litestar.di`. Litestar 2.23 deprecated inferring a dependency from a plain annotation, and Litestar 3.0 removes it. -### Auto-wiring with `autowired_groups` +Call `container.validate()` after the plugin is installed. A factory that takes a `litestar.Request` +depends on the context provider that the plugin registers, so validating before it fails with +`ValidationFailedError`. -Pass `autowired_groups` to `ModernDIPlugin` to automatically register every provider in those groups as a Litestar dependency, keyed by its attribute name. A route handler then receives a provider by naming a parameter after it, with no per-route `FromDI` call: +## Injecting into handlers -```python -import dataclasses -import litestar -from litestar.di import NamedDependency -from modern_di import Container, Group, Scope, providers -from modern_di_litestar import ModernDIPlugin +`FromDI` takes a type, as above, or a provider such as `FromDI(AppGroup.report)`, and resolves it +from the connection's child container. The `dependencies` mapping can sit on a route, a controller, +a router or the app, as with any Litestar dependency. +The plugin also registers a `di_container` dependency, the connection's child container itself. A +handler receives it by declaring `di_container: NamedDependency[Container]`, with no `FromDI`. -@dataclasses.dataclass(kw_only=True) -class UserRepository: - pass - +### Auto-wiring with `autowired_groups` -class AppGroup(Group): - user_repo = providers.Factory(UserRepository, scope=Scope.REQUEST) +Pass `autowired_groups` to `ModernDIPlugin` to register every provider in those groups as a Litestar +dependency, keyed by its attribute name. A handler then receives a provider by naming a parameter +after it, with no per-route `FromDI`: +```python +@litestar.get("/autowired-report") +async def get_autowired_report(report: NamedDependency[Report]) -> dict[str, str]: + return report.as_dict() -ALL_GROUPS = [AppGroup] -container = Container(groups=ALL_GROUPS) -app = litestar.Litestar( - plugins=[ModernDIPlugin(container, autowired_groups=ALL_GROUPS)], +autowired_app = litestar.Litestar( + route_handlers=[get_autowired_report], + plugins=[modern_di_litestar.ModernDIPlugin(Container(groups=[AppGroup]), autowired_groups=[AppGroup])], ) -container.validate() # after the plugin is installed — its connection providers are now registered - - -@litestar.get("/users") -async def list_users(user_repo: NamedDependency[UserRepository]) -> list[str]: - ... ``` -If an attribute name appears in more than one group, or matches a dependency the app already has (including the plugin's own `di_container`), a `UserWarning` is emitted and the autowired provider overwrites the earlier one; among groups, the last one wins. + -With `autowired_groups` set, `FromDI` on a route can still take a type. Pass a provider instance only for providers outside `autowired_groups`: Litestar rejects one provider registered under two keys and raises `ImproperlyConfiguredException`. +If an attribute name appears in more than one group, or matches a dependency the app already has, a +`UserWarning` is emitted and the autowired provider overwrites the earlier one; among groups, the +last one wins. -## Websockets +!!! warning "Don't name a provider `di_container`" + An autowired provider named `di_container` replaces the plugin's own container dependency, + which every `FromDI` depends on. Litestar then fails to build the app with `RecursionError`. -Websockets add `SESSION` scope between `APP` and `REQUEST`; see [the scope -hierarchy](../providers/scopes.md#what-each-scope-is-for). `SESSION` covers -the lifetime of the websocket connection and is entered automatically; -`REQUEST` covers one message and must be entered manually: +With `autowired_groups` set, `FromDI` on a route can still take a type. Pass a provider instance +only for providers outside `autowired_groups`: Litestar rejects one provider registered under two +keys and raises `ImproperlyConfiguredException`. -```python -import dataclasses -import litestar -from litestar.di import NamedDependency -from modern_di import Container, Group, Scope, providers -import modern_di_litestar +## Scopes and lifecycle +An HTTP request gets a `Scope.REQUEST` child container and a websocket connection a +`Scope.SESSION` one; see [the scope hierarchy](../providers/scopes.md#what-each-scope-is-for). The +child is a Litestar generator dependency, so it is built only for handlers that inject something +and closed in Litestar's dependency cleanup. -@dataclasses.dataclass -class MyService: - async def handle(self, data: str) -> None: ... +The root container is open from the moment you construct it. The plugin appends a lifespan hook +that reopens it on startup and closes it with `close_async()` on shutdown. That hook is the only +thing that closes the root container, so when the server skips the lifespan, requests still +succeed but its finalizers never run. Close it yourself (`await container.close_async()`) at +shutdown in that case. +## Websockets -class Dependencies(Group): - my_service = providers.Factory(MyService, scope=Scope.REQUEST) - - -ALL_GROUPS = [Dependencies] - -app = litestar.Litestar(plugins=[modern_di_litestar.ModernDIPlugin(Container(groups=ALL_GROUPS))]) - +In a websocket handler, `di_container` is the connection's `Scope.SESSION` container, which lives +as long as the connection. For per-message work, open a `Scope.REQUEST` child of it: +```python @litestar.websocket_listener("/ws") -async def websocket_handler( - data: str, - di_container: NamedDependency[Container], # auto-resolved — the plugin registers a "di_container" dependency -) -> None: - # For a websocket, di_container is the SESSION-scoped child; enter REQUEST scope here +async def report_socket(data: str, di_container: NamedDependency[Container]) -> dict[str, str]: async with di_container.build_child_container(scope=Scope.REQUEST) as request_container: - service = request_container.resolve(MyService) - await service.handle(data) + return request_container.resolve(Report).as_dict() -app.register(websocket_handler) +websocket_app = litestar.Litestar( + route_handlers=[report_socket], + plugins=[modern_di_litestar.ModernDIPlugin(Container(groups=[AppGroup]))], +) ``` -The plugin registers `di_container` as a Litestar dependency and injects it by name, so you don't need a `FromDI` marker for the container itself. + + +Injecting a REQUEST-scoped provider such as `FromDI(Report)` straight into a websocket handler +raises `ScopeNotInitializedError`, because no REQUEST container exists for the connection. ## Framework context objects -Framework-specific context objects like `litestar.Request` and `litestar.WebSocket` -are automatically made available by the integration. See [Framework context -objects](../providers/context.md#framework-context-objects) for how implicit +The integration makes `litestar.Request` and `litestar.WebSocket` available to your factories. See +[Framework context objects](../providers/context.md#framework-context-objects) for how implicit and explicit resolution work. -The following context providers are available for import: - -- `litestar_request_provider` provides the current `litestar.Request` object -- `litestar_websocket_provider` provides the current `litestar.WebSocket` object +- `litestar_request_provider` provides the current `litestar.Request` (REQUEST scope). +- `litestar_websocket_provider` provides the current `litestar.WebSocket` (SESSION scope). -### Implicit (type-based) usage +A factory can receive the request by type, or name the provider in `kwargs`: ```python -import litestar -from modern_di import Group, providers, Scope - - -def create_request_info(request: litestar.Request) -> dict[str, str]: - return { - "method": request.method, - "url": str(request.url), - "timestamp": "2023-01-01T00:00:00Z" - } +def describe_request(request: litestar.Request) -> dict[str, str]: + return {"method": request.method, "url": str(request.url)} -class AppGroup(Group): - # Factory automatically resolves the request dependency based on type annotation - request_info = providers.Factory( - create_request_info, +class RequestGroup(Group): + by_type = providers.Factory(describe_request, scope=Scope.REQUEST, bound_type=None) + by_provider = providers.Factory( + describe_request, scope=Scope.REQUEST, + bound_type=None, + kwargs={"request": modern_di_litestar.litestar_request_provider}, ) ``` -### Explicit (provider-based) usage - -```python -import litestar -import modern_di_litestar -from modern_di import Group, providers, Scope - + ## See also - [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. -- [Async SQLAlchemy](../recipes/sqlalchemy.md): engine + session + repository through the request container. +- [Multi-group organization](../recipes/multi-group.md): splitting providers across groups, with `autowired_groups`. - [Lifecycle](../providers/lifecycle.md): finalizers and `close_async()`. - [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. @@ -224,8 +231,9 @@ class AppGroup(Group): | Symbol | Description | |---|---| -| `ModernDIPlugin(container, autowired_groups=None)` | Litestar `InitPlugin` that registers the container, composes the lifespan, and (if `autowired_groups` is given) exposes each provider in those groups as a Litestar dependency keyed by attribute name. | -| `FromDI(dependency)` | Returns a Litestar `Provide` that resolves a provider or type from the per-request child container. | +| `ModernDIPlugin(container, autowired_groups=None)` | Litestar `InitPlugin` that stores the container on the app state, registers the request and websocket context providers and the `di_container` dependency, appends a lifespan hook that closes the container on shutdown, and (if `autowired_groups` is given) exposes each provider in those groups as a Litestar dependency keyed by attribute name. | +| `FromDI(dependency)` | Returns a Litestar `Provide` that resolves a provider or type from the connection's child container (REQUEST for HTTP, SESSION for a websocket). | +| `di_container` | The dependency name the plugin registers for the connection's child container; declare `di_container: NamedDependency[Container]` to receive it. | | `fetch_di_container(app)` | Returns the root `Container` stored on the Litestar app. | | `litestar_request_provider` | `ContextProvider` for `litestar.Request` (REQUEST scope), auto-registered. | | `litestar_websocket_provider` | `ContextProvider` for `litestar.WebSocket` (SESSION scope), auto-registered. | diff --git a/docs/integrations/starlette.md b/docs/integrations/starlette.md index 732d6d7e..9d8b0ad4 100644 --- a/docs/integrations/starlette.md +++ b/docs/integrations/starlette.md @@ -1,13 +1,11 @@ # Usage with `Starlette` -Starlette has no dependency-injection system of its own, so `modern-di-starlette` -uses the `@inject` decorator with `FromDI` markers (there is no `Depends`). -`setup_di` composes the lifespan and installs middleware that opens a -per-connection child container automatically. +Starlette has no dependency injection of its own, so `modern-di-starlette` provides an `@inject` +decorator that fills the handler parameters marked with `FromDI`. `setup_di` installs a middleware +that gives each HTTP request a `Scope.REQUEST` child container and each websocket connection a +`Scope.SESSION` one, and wraps the app's lifespan, which closes the root container on shutdown. -## How to use - -### 1. Install `modern-di-starlette` +## Installation === "uv" @@ -27,7 +25,7 @@ per-connection child container automatically. poetry add modern-di-starlette ``` -### 2. Apply to your application +## Usage ```python import dataclasses @@ -70,42 +68,43 @@ async def get_report( app = Starlette(routes=[Route("/report", get_report)]) container = Container(groups=[AppGroup]) setup_di(app, container) -container.validate() # after setup_di — its connection providers are now registered +container.validate() ``` -!!! warning "Deployment: mounted sub-apps and disabled lifespan" - Starlette only opens the root container from the ASGI lifespan event. - A `setup_di`-wired app mounted as a sub-application - (`app.mount("/sub", subapp)`) never receives that event from its parent, - and deployments that disable lifespan (e.g. Mangum `lifespan="off"`) skip - it too. Requests still succeed (the container is already open from - construction), but nothing ever closes it, so its finalizers never run at - shutdown. Call `setup_di` on the top-level served app, or close the - root yourself (`await container.close_async()`) at shutdown. + -## Class-based endpoints +Call `container.validate()` after `setup_di`. A factory that takes a Starlette `Request` depends on +the context provider that `setup_di` registers, so validating before it fails with +`ValidationFailedError`. -`@inject` works the same on the methods of an `HTTPEndpoint` or `WebSocketEndpoint` -subclass. Decorate the handler method, not the class. `self` and any arguments -Starlette passes after the connection are forwarded unchanged, so -`WebSocketEndpoint.on_receive` and `on_disconnect` inject too: +## Injecting into handlers -```python -import typing +`FromDI` takes a type, as above, or a provider such as `FromDI(AppGroup.report)`. `@inject` +resolves it from the connection's child container, which it finds through the `Request` or +`WebSocket` the handler receives. A decorated handler without one among its positional arguments +raises `TypeError`, and a request that did not pass through the `setup_di` middleware raises +`RuntimeError`. -from modern_di_starlette import FromDI, inject -from starlette.applications import Starlette +### Class-based endpoints + +`@inject` works the same on the methods of an `HTTPEndpoint` or `WebSocketEndpoint` subclass. +Decorate the method, not the class. `self` and any arguments Starlette passes after the connection +are forwarded unchanged, so `WebSocketEndpoint.on_receive` and `on_disconnect` inject too: + +```python from starlette.endpoints import HTTPEndpoint, WebSocketEndpoint -from starlette.requests import Request -from starlette.responses import JSONResponse -from starlette.routing import Route, WebSocketRoute +from starlette.routing import WebSocketRoute from starlette.websockets import WebSocket @@ -132,84 +131,119 @@ class EchoEndpoint(WebSocketEndpoint): await websocket.send_text(f"{settings.service_name}: {data}") -app = Starlette(routes=[Route("/report", ReportEndpoint), WebSocketRoute("/echo", EchoEndpoint)]) +app.routes.extend([Route("/report-endpoint", ReportEndpoint), WebSocketRoute("/echo", EchoEndpoint)]) ``` -Requires `modern-di-starlette` 3.2.0 or later. Earlier versions raise a -`TypeError` on the first request to a decorated method. + -## Websockets +## Scopes and lifecycle -For per-message work within a websocket's `Scope.SESSION` container, open a -nested `Scope.REQUEST` child: +An HTTP request gets a `Scope.REQUEST` child container and a websocket connection a +`Scope.SESSION` one; see [the scope hierarchy](../providers/scopes.md#what-each-scope-is-for). The +middleware builds the child before your handler runs and closes it once the app is done with the +connection: after the response is sent for HTTP, and when the socket closes for a websocket. -```python -import typing +The root container is open from the moment you construct it. `setup_di` wraps the app's lifespan: +a `lifespan=` you passed to `Starlette()` stays the outer context and the state it yields passes +through, the container reopens on startup, and it closes with `close_async()` on shutdown. The +lifespan shutdown is the only thing that closes the root container. -import modern_di -from modern_di import Scope, providers -from modern_di_starlette import FromDI, inject -from starlette.websockets import WebSocket +!!! warning "Deployment: mounted sub-apps and disabled lifespan" + A `setup_di`-wired app mounted as a sub-application (`app.mount("/sub", subapp)`) never + receives the lifespan events from its parent, and deployments that disable lifespan (for + example Mangum with `lifespan="off"`) skip them too. Requests still succeed, because the root + container is open from construction, but nothing closes it, so its finalizers never run. Call + `setup_di` on the top-level served app, or close the root yourself + (`await container.close_async()`) at shutdown. +## Websockets + +For per-message work inside a websocket's `Scope.SESSION` container, open a `Scope.REQUEST` child +of it. `FromDI(Container)` gives the handler the SESSION container: +```python @inject -async def ws_handler( +async def report_socket( websocket: WebSocket, - container: typing.Annotated[modern_di.Container, FromDI(providers.container_provider)], + session_container: typing.Annotated[Container, FromDI(Container)], ) -> None: await websocket.accept() - async for message in websocket.iter_text(): - async with container.build_child_container(scope=Scope.REQUEST) as request_container: - ... # resolve REQUEST-scoped providers for this message -``` - -## Framework context objects - -Framework-specific context objects like `starlette.requests.Request` and -`starlette.websockets.WebSocket` are automatically made available by the -integration. See [Framework context objects](../providers/context.md#framework-context-objects) -for how implicit and explicit resolution work. + async for _ in websocket.iter_text(): + async with session_container.build_child_container(scope=Scope.REQUEST) as request_container: + report = request_container.resolve(Report) + await websocket.send_json(report.as_dict()) -The following context providers are available for import: -- `starlette_request_provider` provides the current `starlette.requests.Request` (REQUEST scope) -- `starlette_websocket_provider` provides the current `starlette.websockets.WebSocket` (SESSION scope) +app.routes.append(WebSocketRoute("/ws", report_socket)) +``` -### Implicit (type-based) usage + -```python -from starlette.requests import Request -from modern_di import Group, Scope, providers +Injecting a REQUEST-scoped provider such as `FromDI(Report)` straight into a websocket handler +raises `ScopeNotInitializedError`, because no REQUEST container exists for the connection. +## Framework context objects -def create_request_info(request: Request) -> dict[str, str]: - return {"method": request.method, "url": str(request.url)} +The integration makes `starlette.requests.Request` and `starlette.websockets.WebSocket` available +to your factories. See [Framework context objects](../providers/context.md#framework-context-objects) +for how implicit and explicit resolution work. +- `starlette_request_provider` provides the current `starlette.requests.Request` (REQUEST scope). +- `starlette_websocket_provider` provides the current `starlette.websockets.WebSocket` (SESSION scope). -class AppGroup(Group): - request_info = providers.Factory(create_request_info, scope=Scope.REQUEST) -``` +The object these providers hold is one the middleware builds, so it is a different object from the +`Request` or `WebSocket` your handler receives. Both wrap the same ASGI scope and share +`request.state`. -### Explicit (provider-based) usage +A factory can receive the request by type, or name the provider in `kwargs`: ```python import modern_di_starlette -from starlette.requests import Request -from modern_di import Group, Scope, providers -def create_request_info(request: Request) -> dict[str, str]: +def describe_request(request: Request) -> dict[str, str]: return {"method": request.method, "url": str(request.url)} -class AppGroup(Group): - request_info = providers.Factory( - create_request_info, +class RequestGroup(Group): + by_type = providers.Factory(describe_request, scope=Scope.REQUEST, bound_type=None) + by_provider = providers.Factory( + describe_request, scope=Scope.REQUEST, + bound_type=None, kwargs={"request": modern_di_starlette.starlette_request_provider}, ) ``` + + ## See also - [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. @@ -221,9 +255,9 @@ class AppGroup(Group): | Symbol | Description | |---|---| -| `setup_di(app, container)` | Registers the container on `app.state`, composes the lifespan (opens/closes the container), and installs the middleware that builds a per-connection child container; returns the container. | +| `setup_di(app, container)` | Stores the container on `app.state`, registers the request and websocket context providers, wraps the lifespan so the container reopens on startup and closes on shutdown, and installs the middleware that builds a per-connection child container; returns the container. | | `FromDI(dependency)` | Marker (used with `@inject`) that resolves a provider or type from the per-connection child container. | -| `inject` | Decorator for an `async def handler(connection: Request | WebSocket, ...)`, a function endpoint or a method of an `HTTPEndpoint` / `WebSocketEndpoint` subclass; resolves its `FromDI`-annotated parameters. Raises `RuntimeError` naming `setup_di` when the connection did not pass through the middleware. | +| `inject` | Decorator for an `async def handler(connection: Request | WebSocket, ...)`, a function endpoint or a method of an `HTTPEndpoint` / `WebSocketEndpoint` subclass; resolves its `FromDI`-annotated parameters. Raises `TypeError` when the handler receives no `Request` or `WebSocket` positionally, and `RuntimeError` naming `setup_di` when the connection did not pass through the middleware. | | `fetch_di_container(app)` | Returns the root `Container` stored on `app.state`. | | `starlette_request_provider` | `ContextProvider` for `starlette.requests.Request` (REQUEST scope), auto-registered. | | `starlette_websocket_provider` | `ContextProvider` for `starlette.websockets.WebSocket` (SESSION scope), auto-registered. |