Scopes
Scopes are one of the fundamental concepts in dependency injection. Some dependency injection frameworks provide fixes scopes, for example:
- Singleton: only one instance is created
- Request: in web frameworks, this could be the lifetime of a request
- Prototype: re-initialized every time it is needed
di
generalizes this concept by putting control of scopes into the hands of the users / implementers: a scope in di
is identified by any hashable value (a string, enum, int, etc.) and entering / exiting scopes is handled via context managers:
async with container.enter_scope("app"):
async with container.enter_scope("request"):
async with container.enter_scope("foo, bar, baz!"):
Scopes provide a framework for several other important features:
- Dependency lifespans
- Dependency value sharing
Every dependency is linked to a scope. When a scope exits, all dependencies linked to it are destroyed (if they have teardown, the teardown is run) and their value is removed from the cache. This means that dependencies scoped to an outer scope cannot depend on dependencies scoped to an inner scope:
from di import Container, Dependant, SyncExecutor
from di.typing import Annotated
class Request:
...
class DBConnection:
def __init__(self, request: Request) -> None:
...
def controller(conn: Annotated[DBConnection, Dependant(scope="app")]) -> None:
...
def framework() -> None:
container = Container(scopes=("app", "request"))
with container.enter_scope("app"):
with container.enter_scope("request"):
request = Request()
with container.register_by_type(
Dependant(lambda: request, scope="request"), Request
):
container.execute_sync(
container.solve(Dependant(controller)), executor=SyncExecutor()
)
This example will fail with di.exceptions.ScopeViolationError
because an "app"
scoped dependency (conn
, as requested by controller
via Dependant(scope="app")
) depends on a request scope dependency (in framework
, we specify Dependant(..., scope="request"
).
This is because dependencies and scopes behave much a stack and references in general purpose languages: you can't reference a function local once you exit that function.
Even if we could hold onto the value once we exit the scope, that value could be a reference to an object that already had its destructor run, for example a database connection that was closed.