Skip to content

Usage with Flask

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.

Installation

uv add modern-di-flask
pip install modern-di-flask
poetry add modern-di-flask

Usage

import dataclasses
import typing

from flask import Flask
from modern_di import Container, Group, Scope, providers
from modern_di_flask import FromDI, inject, setup_di


@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class Settings:
    service_name: str = "catalog"


@dataclasses.dataclass(kw_only=True, slots=True)
class Report:
    settings: Settings   # APP-scoped, injected by type

    def as_dict(self) -> dict[str, str]:
        return {"service": self.settings.service_name}


class AppGroup(Group):
    settings = providers.Factory(Settings, scope=Scope.APP, cache=True)
    report = providers.Factory(Report, scope=Scope.REQUEST)


app = Flask(__name__)


@app.route("/report")
@inject
def get_report(report: typing.Annotated[Report, FromDI(Report)]) -> dict[str, str]:
    return report.as_dict()


container = Container(groups=[AppGroup])
setup_di(app, container)
container.validate()

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.

Injecting into handlers

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

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':

auto_app = Flask(__name__)


@auto_app.route("/report")
def get_auto_report(report: typing.Annotated[Report, FromDI(Report)]) -> dict[str, str]:
    return report.as_dict()


setup_di(auto_app, Container(groups=[AppGroup]), auto_inject=True)


# 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()

Scopes and lifecycle

Flask has no websocket concept, so the integration only ever opens one child scope; see the scope hierarchy. 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, 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:

import atexit

from modern_di_flask import fetch_di_container

atexit.register(fetch_di_container(app).close_sync)

Framework context objects

The integration makes flask.Request available to your factories. See Framework context objects for how implicit and explicit resolution work.

  • flask_request_provider provides the current flask.Request (REQUEST scope). It is registered by type.

A factory can receive the request by type, or name the provider in kwargs:

from flask import Request
from modern_di_flask import flask_request_provider


def describe_request(request: Request) -> dict[str, str]:
    return {"method": request.method, "url": request.url}


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},
    )

See also

API

Symbol Description
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 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.