Skip to content

Providers & recipes

The central registry that binds each type to the recipe that builds it and the scope it lives in.

The registry

Providers is the one place that says what builds each type and how long it lives. It's chainable and router-style: .app(T) binds T to the app scope, .request(T) to the request scope, and .bind(T, recipe, scope=...) is the general form. Keys are types, optionally plus a qualifier.

from gazebo.di import Providers

providers = Providers().app(Settings).app(Database).request(User)

Recipes

A recipe is any callable that builds the value, and its parameters are themselves resolved by type. gazebo accepts every natural form:

  • a plain or async function;
  • a sync or async generator, wrapped automatically as a context manager so its post-yield code runs at teardown;
  • an explicit (async) context manager;
  • a class with no recipe, used as its own constructor.

So a resource that must be cleaned up is just a generator that yields it.

Colocated __provide__

The tidiest place for a recipe is on the type itself, as a __provide__ classmethod — then providers.app(Database) needs no separate recipe, and the recipe sits next to what it builds. __provide__ takes its dependencies as typed parameters like any recipe:

@dataclass
class Settings:
    dsn: str = 'postgres://localhost/app'

    @classmethod
    def __provide__(cls) -> Settings:  # colocated recipe: built on demand
        return cls()


class Database:
    def __init__(self, dsn: str) -> None:
        self.dsn = dsn

    @classmethod
    @asynccontextmanager
    async def __provide__(cls, settings: Settings) -> AsyncIterator[Database]:
        db = cls(settings.dsn)
        try:
            yield db  # built once (app scope)...
        finally:
            pass  # ...torn down at shutdown: await db.close()

Standalone recipes (external types)

For types you can't add __provide__ to — a third-party Session, a stdlib object — pass a standalone recipe as the second argument to .bind/.app/ .request. It still declares its dependencies by type. (Injecting such a type into a route additionally needs the Inject marker, since there's no __provide__ for the glue to detect.)

class Session:  # third-party type: no __provide__ to add
    def __init__(self, db: Database) -> None:
        self.db = db


@asynccontextmanager
async def provide_session(database: Database) -> AsyncIterator[Session]:
    session = Session(database)
    try:
        yield session
    finally:
        pass  # await session.close()


providers.request(Session, provide_session)  # bind the external type to a recipe

Reference

See gazebo.di.providers.