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
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 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 solved.execute_async(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.solve( Dependent(DBProtocol), scopes=["request"] ).execute_async( executor=AsyncExecutor(), state=state, ) assert isinstance(db, Postgres)
In this example we register the
Postgres class to
DBProtocol, and we can see that
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.
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
This means you can implement any sort of matching you want, including:
- Matching by type (see
- Matching by any subclass (
- Custom logic, in the form of a bind hook (
For example, to match by parameter name:
import inspect import typing from dataclasses import dataclass from di import Container from di.api.dependencies import DependentBase 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 = solved.execute_sync(executor=SyncExecutor(), state=state) assert foo.bar == "baz"