Skip to content

Binds

Binds provide two important functions:

  • A way to tell the container how to assemble things that can't be autowired, for example interfaces.
  • A way to override dependencies in tests.

Every bind in di consists of:

  • A target callable: this can be a function, an interface / protocol or a concrete class
  • A substitute dependency: an object implementing the DependantBase, usually just an instance of Dependant

This means that binds are themselves dependencies:

import sys
from dataclasses import dataclass
from typing import List

if sys.version_info < (3, 8):
    from typing_extensions import Protocol
else:
    from typing import Protocol

from di import Container, Dependant


class DBProtocol(Protocol):
    async def execute(self, sql: str) -> None:
        ...


async def controller(db: DBProtocol) -> None:
    await db.execute("SELECT *")


@dataclass
class DBConfig:
    host: str = "localhost"


class Postgres(DBProtocol):
    def __init__(self, config: DBConfig) -> None:
        self.host = config.host
        self.log: List[str] = []

    async def execute(self, sql: str) -> None:
        self.log.append(sql)


async def framework() -> None:
    container = Container()
    container.bind(Dependant(Postgres, scope="app"), DBProtocol)  # type: ignore
    solved = container.solve(Dependant(controller))
    async with container.enter_scope("app"):
        await container.execute_async(solved)
        db = await container.execute_async(container.solve(Dependant(DBProtocol)))
        assert isinstance(db, Postgres)
        assert db.log == ["SELECT *"]

In this example we bind a concrete Postgres instance to DBProtocol, and we can see that di autowires Postgres as well!

Binds can be used as a direct function call, in which case they are permanent, or as a context manager.