From f2f6f3ccaadd6f418d4c99ff3738f487337564f6 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 26 Jun 2026 22:09:47 +0300 Subject: [PATCH] refactor: single-source the connection-kind mapping off the providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build_di_container re-stated, via an isinstance ladder, the connection types and context keys that the context providers already carry. With modern-di 2.21.0 exposing ContextProvider.context_type, the registered providers are now the single source: setup_di registers them and build_di_container walks them, taking context_type and scope from each. Adding a connection kind is adding a provider; this dispatch no longer changes. Behavior is unchanged — existing Request and WebSocket tests cover it at 100%. Promotes the change into architecture/container-lifecycle.md and sharpens the glossary's Scope mapping entry, per the planning convention. Co-Authored-By: Claude Opus 4.8 (1M context) --- architecture/container-lifecycle.md | 12 ++++++++---- architecture/glossary.md | 10 ++++++---- modern_di_fastapi/main.py | 19 ++++++++++++------- pyproject.toml | 2 +- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/architecture/container-lifecycle.md b/architecture/container-lifecycle.md index a2aa0a7..7bc8d8c 100644 --- a/architecture/container-lifecycle.md +++ b/architecture/container-lifecycle.md @@ -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 diff --git a/architecture/glossary.md b/architecture/glossary.md index 8eb1804..3838d9a 100644 --- a/architecture/glossary.md +++ b/architecture/glossary.md @@ -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**: diff --git a/modern_di_fastapi/main.py b/modern_di_fastapi/main.py index 7f93f0b..0f3d0b4 100644 --- a/modern_di_fastapi/main.py +++ b/modern_di_fastapi/main.py @@ -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) @@ -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, @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 1cd0dcc..e026124 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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]