Skip to content

Proposal: add a unified get_container(app) accessor to dishka.integrations #756

Open

Description

@handsome-Druid

Proposal: add a unified get_container(app) accessor to dishka.integrations

Hi maintainers, first of all — thank you for the great library. I want to discuss an idea before opening a PR, since CONTRIBUTING.md suggests new features be discussed first.

Motivation

setup_dishka stores the app-scoped container on the framework app object, but the storage location differs per integration:

  • fastapi / litestar: app.state.dishka_container
  • aiohttp: app[DISHKA_CONTAINER_KEY]
  • flask: request-scoped only (g.dishka_container), no app-level storage
  • sanic: request-scoped only (request.ctx.dishka_container), no app-level storage

These are the integrations I happen to use; the same inconsistency exists across other integrations too — the container is always bound to some host object, but there is no uniform way to retrieve it.

At the same time, the current docs require users to close the container manually on shutdown for most web integrations (only aiohttp/click/faststream have a finalize_container option), and the documented shutdown examples directly access the internal storage, e.g.:

@asynccontextmanager
async def lifespan(app: FastAPI):
    yield
    await app.state.dishka_container.close()

So the "read the container back from the app" operation already exists and is documented — but only as direct access to integration internals, which differ from framework to framework. If the internal mount point ever changes, all user code relying on it breaks, and for flask/sanic there is currently no way to retrieve the app-scoped container at all.

Proposal

Add a small, uniform accessor to each web integration:

# fastapi
def get_container(app: FastAPI) -> AsyncContainer | Container:
    return app.state.dishka_container

# flask
app.extensions["dishka_container"] = container  # in setup_dishka

def get_container(app: Flask) -> Container:
    return app.extensions["dishka_container"]

# sanic
app.ctx.dishka_container = container  # in setup_dishka

def get_container(app: Sanic[Any, Any]) -> AsyncContainer:
    return app.ctx.dishka_container

(flask/sanic would additionally store the container at app level inside setup_dishka, which is a purely additive change.)

Alternative: expose only a close function (close_container)

If returning the full container feels too permissive, a more conservative option would be to expose only the ability to close it, so users never get the full container in their hands:

# fastapi
async def close_container(app: FastAPI) -> None:
    container = app.state.dishka_container
    if isinstance(container, AsyncContainer):
        await container.close()
    else:
        container.close()

# flask
def close_container(app: Flask) -> None:
    app.extensions["dishka_container"].close()

# sanic
async def close_container(app: Sanic[Any, Any]) -> None:
    await app.ctx.dishka_container.close()

Usage on shutdown: await close_container(app). This structurally prevents service-locator usage while still letting users manage the lifecycle. The downsides: it only covers the shutdown use case, and the fastapi variant needs to branch on sync vs async (its container can be AsyncContainer | Container). The name is also up for discussion.

Scope

I want to be explicit that this is not intended as a service locator API. get_container(app) returns the app-scoped container and is meant for lifecycle/composition-root use (closing the container on shutdown, e.g. await get_container(app).close()). Request-scoped dependencies should still be resolved via injection, as dishka's design intends.

Open questions / Implementation

Which approach would better fit the library's design philosophy — get_container(app) (returns the full container) or a close-only function such as close_container(app)? Either way, I'm happy to implement it across all relevant integrations — not only the ones I use (fastapi, flask, sanic) — including tests, and open a PR.

What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions