Registration and Binding
Provider registration 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.
We call a the result of registering a provider a bind.
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
if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol
from di import AsyncExecutor, 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
async def execute(self, sql: str) -> None:
print(sql)
async def framework() -> None:
container = Container(scopes=("request",))
container.register_by_type(Dependant(Postgres, scope="request"), DBProtocol)
solved = container.solve(Dependant(controller, scope="request"))
# this next line would fail without the bind
async with container.enter_scope("request"):
await container.execute_async(solved, executor=AsyncExecutor())
# and we can double check that the bind worked
# by requesting the instance directly
async with container.enter_scope("request"):
db = await container.execute_async(
container.solve(Dependant(DBProtocol)), executor=AsyncExecutor()
)
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!
Registration can be used as a direct function call, in which case they are permanent, or as a context manager.