Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions architecture/container-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ container instance.
`build_di_container` is an async FastAPI dependency that yields a *child
container* scoped to the current *connection*, then closes it:

- It applies the *scope mapping*: a `fastapi.Request` → `Scope.REQUEST` with the
request placed in `context[fastapi.Request]`; a `fastapi.WebSocket` →
`Scope.SESSION` with the socket in `context[fastapi.WebSocket]`. Any other
`HTTPConnection` yields a child with `scope=None`.
- It applies the *scope mapping* by walking the registered *context providers*
(`_CONNECTION_PROVIDERS`): the first whose `context_type` the connection is an
instance of supplies both the scope and the context key. So a `fastapi.Request`
→ `Scope.REQUEST` with the request placed in `context[fastapi.Request]`; a
`fastapi.WebSocket` → `Scope.SESSION` with the socket in
`context[fastapi.WebSocket]`. Any other `HTTPConnection` matches no provider and
yields a child with `scope=None`. The providers are the single source — adding a
connection kind is adding a provider, with no change to this dispatch.
- The child is built from the root container via
`build_child_container(context=..., scope=...)`.
- After the endpoint returns, the `finally` block calls
Expand Down
10 changes: 6 additions & 4 deletions architecture/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ off of.
_Avoid_: request (a request is only one kind of connection)

**Scope mapping**:
The fixed correspondence this integration imposes between a connection kind and
a `modern_di.Scope`: a `Request` opens a `REQUEST`-scoped child container; a
`WebSocket` opens a `SESSION`-scoped one. Any other `HTTPConnection` gets no
scope (`None`).
The correspondence between a connection kind and a `modern_di.Scope`, sourced
from the registered *context providers* (each carries its `context_type` and
`scope`): a `Request` opens a `REQUEST`-scoped child container; a `WebSocket`
opens a `SESSION`-scoped one. Any other `HTTPConnection` matches no provider and
gets no scope (`None`). The providers are the single source — there is no
separate hardcoded table.
_Avoid_: scope resolution

**Context provider**:
Expand Down
19 changes: 12 additions & 7 deletions modern_di_fastapi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
fastapi_request_provider = providers.ContextProvider(scope=Scope.REQUEST, context_type=fastapi.Request)
fastapi_websocket_provider = providers.ContextProvider(scope=Scope.SESSION, context_type=fastapi.WebSocket)

# The single source of the connection-kind mapping. Each provider pairs a connection
# type (``context_type``) with the scope its child container opens at; ``setup_di``
# registers them and ``build_di_container`` dispatches off them. Add a connection
# kind by adding its provider here — nothing else changes.
_CONNECTION_PROVIDERS = (fastapi_request_provider, fastapi_websocket_provider)


def fetch_di_container(app_: fastapi.FastAPI) -> Container:
return typing.cast(Container, app_.state.di_container)
Expand All @@ -30,7 +36,7 @@ async def _lifespan_manager(app_: fastapi.FastAPI) -> typing.AsyncIterator[None]

def setup_di(app: fastapi.FastAPI, container: Container) -> Container:
app.state.di_container = container
container.providers_registry.add_providers(fastapi_request_provider, fastapi_websocket_provider)
container.providers_registry.add_providers(*_CONNECTION_PROVIDERS)
old_lifespan_manager = app.router.lifespan_context
app.router.lifespan_context = _merge_lifespan_context(
old_lifespan_manager,
Expand All @@ -42,12 +48,11 @@ def setup_di(app: fastapi.FastAPI, container: Container) -> Container:
async def build_di_container(connection: HTTPConnection) -> typing.AsyncIterator[Container]:
context: dict[type[typing.Any], typing.Any] = {}
scope = None
if isinstance(connection, fastapi.Request):
scope = fastapi_request_provider.scope
context[fastapi.Request] = connection
elif isinstance(connection, fastapi.WebSocket):
context[fastapi.WebSocket] = connection
scope = fastapi_websocket_provider.scope
for provider in _CONNECTION_PROVIDERS:
if isinstance(connection, provider.context_type):
context[provider.context_type] = connection
scope = provider.scope
break
container = fetch_di_container(connection.app).build_child_container(context=context, scope=scope)
try:
yield container
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ classifiers = [
"Typing :: Typed",
"Topic :: Software Development :: Libraries",
]
dependencies = ["fastapi>=0.100,<1", "modern-di>=2.19.0,<3"]
dependencies = ["fastapi>=0.100,<1", "modern-di>=2.21.0,<3"]
version = "0"

[project.urls]
Expand Down
Loading