Skip to content

Binds

Provider binding serves two important functions:

  • A way to tell the container how to assemble things that can't be auto-wired, 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 DependentBase, usually just an instance of Dependent

This means that binds are themselves dependencies:

import sys
from dataclasses import dataclass

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

from di.container import Container, bind_by_type
from di.dependent import Dependent
from di.executors import AsyncExecutor


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

    async def execute(self, sql: str) -> None:
        print(sql)


async def framework() -> None:
    container = Container()
    container.bind(bind_by_type(Dependent(Postgres, scope="request"), DBProtocol))
    solved = container.solve(Dependent(controller, scope="request"), scopes=["request"])
    # this next line would fail without the bind
    async with container.enter_scope("request") as state:
        await container.execute_async(solved, executor=AsyncExecutor(), state=state)
    # and we can double check that the bind worked
    # by requesting the instance directly
    async with container.enter_scope("request") as state:
        db = await container.execute_async(
            container.solve(Dependent(DBProtocol), scopes=["request"]),
            executor=AsyncExecutor(),
            state=state,
        )
    assert isinstance(db, Postgres)

In this example we register the Postgres class to DBProtocol, and we can see that di auto-wires Postgres as well!

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

Bind hooks

Binding is implemented as hooks / callbacks: when we solve a dependency graph, every hook is called with every dependent and if the hook "matches" the dependent it returns the substitute dependency (otherwise it just returns None).

This means you can implement any sort of matching you want, including:

  • Matching by type (see di.container.bind_by_type)
  • Matching by any subclass (di.container.bind_by_type using the covariant=True parameter)
  • Custom logic, in the form of a bind hook (Container.bind)

For example, to match by parameter name:

import inspect
import typing
from dataclasses import dataclass

from di.api.dependencies import DependentBase
from di.container import Container
from di.dependent import Dependent
from di.executors import SyncExecutor


@dataclass
class Foo:
    bar: str = "bar"


def match_by_parameter_name(
    param: typing.Optional[inspect.Parameter], dependent: DependentBase[typing.Any]
) -> typing.Optional[DependentBase[typing.Any]]:
    if param is not None and param.name == "bar":
        return Dependent(lambda: "baz", scope=None)
    return None


container = Container()

container.bind(match_by_parameter_name)

solved = container.solve(Dependent(Foo, scope=None), scopes=[None])


def main():
    with container.enter_scope(None) as state:
        foo = container.execute_sync(solved, executor=SyncExecutor(), state=state)
    assert foo.bar == "baz"