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 ofDependant
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.