From 9461b019a759ea8f18181ff3c8a1ed9234252225 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 3 Oct 2026 17:29:20 +0300 Subject: [PATCH 1/2] docs: plain-prose pass over README and published docs; refresh social card --- README.md | 8 +- docs/assets/social-card.png | Bin 9649 -> 12016 bytes docs/index.md | 18 +-- docs/integrations/aiogram.md | 18 +-- docs/integrations/aiohttp.md | 12 +- docs/integrations/arq.md | 12 +- docs/integrations/celery.md | 12 +- docs/integrations/fastapi.md | 22 ++-- docs/integrations/faststream.md | 14 +-- docs/integrations/flask.md | 14 +-- docs/integrations/grpc.md | 14 +-- docs/integrations/litestar.md | 14 +-- docs/integrations/pytest.md | 6 +- docs/integrations/starlette.md | 18 +-- docs/integrations/taskiq.md | 16 +-- docs/integrations/typer.md | 6 +- docs/integrations/writing-integrations.md | 105 +++++++++--------- docs/introduction/about-di.md | 6 +- docs/introduction/comparison.md | 88 +++++++-------- docs/introduction/design-decisions.md | 24 ++-- docs/introduction/for-fastapi-users.md | 29 +++-- docs/introduction/performance.md | 63 ++++++----- docs/introduction/resolving.md | 10 +- docs/migration/from-dependency-injector.md | 70 ++++++------ docs/migration/from-that-depends.md | 40 +++---- docs/migration/to-1.x.md | 14 +-- docs/migration/to-2.x.md | 12 +- docs/migration/to-3.x.md | 56 +++++----- docs/providers/advanced-api.md | 10 +- docs/providers/container.md | 12 +- docs/providers/context.md | 16 +-- docs/providers/errors-and-exceptions.md | 54 ++++----- docs/providers/factories.md | 10 +- docs/providers/lifecycle.md | 18 +-- docs/providers/scopes.md | 19 ++-- docs/recipes/async-lifespan.md | 12 +- docs/recipes/good-and-bad-practices.md | 28 ++--- docs/recipes/multi-group.md | 6 +- docs/recipes/request-scoped-engine.md | 12 +- docs/recipes/sqlalchemy.md | 14 +-- docs/recipes/testing-overrides.md | 8 +- .../alias-source-not-registered-error.md | 4 +- .../argument-resolution-error.md | 8 +- .../child-container-registration-error.md | 3 +- docs/troubleshooting/circular-dependency.md | 11 +- .../troubleshooting/container-closed-error.md | 18 +-- docs/troubleshooting/context-not-set.md | 14 +-- docs/troubleshooting/creator-call-error.md | 6 +- docs/troubleshooting/duplicate-type-error.md | 2 +- docs/troubleshooting/finalizer-error.md | 5 +- .../group-instantiation-error.md | 2 +- .../group-scope-conflict-error.md | 4 +- .../invalid-child-scope-error.md | 2 +- .../invalid-scope-type-error.md | 2 +- .../max-scope-reached-error.md | 2 +- docs/troubleshooting/missing-provider.md | 18 +-- .../provider-scope-frozen-error.md | 14 +-- docs/troubleshooting/scope-chain.md | 12 +- .../scope-not-initialized-error.md | 2 +- docs/troubleshooting/scope-skipped-error.md | 7 +- .../validation-failed-error.md | 4 +- 61 files changed, 535 insertions(+), 545 deletions(-) diff --git a/README.md b/README.md index fcb3a04e..e13ad87c 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ - Python 3.10+ support - Fully typed and tested - Integrations with `aiogram`, `aiohttp`, `arq`, `Celery`, `FastAPI`, `FastStream`, `Flask`, `gRPC`, `Litestar`, `Starlette`, `taskiq`, and `Typer` -- Pytest integration (`modern-di-pytest`) — turns any DI dependency into a pytest fixture +- Pytest integration (`modern-di-pytest`) that turns any DI dependency into a pytest fixture ## Install @@ -45,7 +45,7 @@ uv add modern-di # or: pip install modern-di ``` -## Quick Start +## Quick start ```python import dataclasses @@ -77,8 +77,8 @@ See the [documentation](https://modern-di.modern-python.org) for scopes, lifecyc Usage examples: -- with Litestar - [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template) -- with FastAPI - [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template) +- with Litestar: [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template) +- with FastAPI: [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template) ## 📚 [Documentation](https://modern-di.modern-python.org) diff --git a/docs/assets/social-card.png b/docs/assets/social-card.png index 49e6ffe07117c67d921c4ce724118a6dd0d7b520..57719708b20962024e9bf8b6cc9c3d79e51597b6 100644 GIT binary patch literal 12016 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 OIC{kXaOuz3gnt3>_a<2Y diff --git a/docs/index.md b/docs/index.md index 0b88c487..a47f5a67 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,8 +18,8 @@ Reference templates: -- Litestar — [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template) -- FastAPI — [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template) +- Litestar: [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template) +- FastAPI: [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template) For end-to-end patterns drawn from real services, see the [Recipes](recipes/sqlalchemy.md) section. @@ -175,7 +175,7 @@ child container for you automatically. Resolution itself is always synchronous; ## Where to next -- Framework integrations — [aiogram](integrations/aiogram.md), [aiohttp](integrations/aiohttp.md), +- Framework integrations: [aiogram](integrations/aiogram.md), [aiohttp](integrations/aiohttp.md), [arq](integrations/arq.md), [Celery](integrations/celery.md), [FastAPI](integrations/fastapi.md), [FastStream](integrations/faststream.md), [Flask](integrations/flask.md), [gRPC](integrations/grpc.md), [Litestar](integrations/litestar.md), [Starlette](integrations/starlette.md), @@ -183,9 +183,9 @@ child container for you automatically. Resolution itself is always synchronous; The framework integrations build a scoped child container per request/task/call automatically, and most close the APP container at shutdown. Flask, gRPC, and Typer have no shutdown hook, so you close the root container yourself. The Pytest plugin exposes providers as fixtures. -- [Resolving](introduction/resolving.md) — how type-based auto-injection works. -- [Factories](providers/factories.md) — the provider you just used. -- [Scopes](providers/scopes.md) — the APP → REQUEST scope model in one page. -- [Lifecycle](providers/lifecycle.md) — finalizers, `close_async()`, validation. -- [Recipes](recipes/sqlalchemy.md) — async SQLAlchemy, lifespan-managed resources, testing with overrides. -- [Good and bad practices](recipes/good-and-bad-practices.md) — named footguns and the mechanism that catches each one. +- [Resolving](introduction/resolving.md): how type-based auto-injection works. +- [Factories](providers/factories.md): the provider you just used. +- [Scopes](providers/scopes.md): the APP → REQUEST scope model in one page. +- [Lifecycle](providers/lifecycle.md): finalizers, `close_async()`, validation. +- [Recipes](recipes/sqlalchemy.md): async SQLAlchemy, lifespan-managed resources, testing with overrides. +- [Good and bad practices](recipes/good-and-bad-practices.md): named footguns and the mechanism that catches each one. diff --git a/docs/integrations/aiogram.md b/docs/integrations/aiogram.md index 998738a3..9c7fa8f1 100644 --- a/docs/integrations/aiogram.md +++ b/docs/integrations/aiogram.md @@ -130,7 +130,7 @@ container.validate() # after setup_di — its connection providers are now regi ## Scopes -The integration creates one `Scope.REQUEST` child container **per update**. +The integration creates one `Scope.REQUEST` child container per update. The middleware is installed on `dispatcher.update` as an [outer middleware](https://docs.aiogram.dev/en/latest/dispatcher/middlewares.html), so it wraps every update regardless of which router or handler ultimately @@ -161,8 +161,8 @@ for how implicit and explicit resolution work. The following context providers are also available for explicit import: -- `aiogram_update_provider` — provides the current `aiogram.types.Update`. -- `aiogram_event_provider` — provides the current `aiogram.types.TelegramObject`, +- `aiogram_update_provider` provides the current `aiogram.types.Update`. +- `aiogram_event_provider` provides the current `aiogram.types.TelegramObject`, the concrete event unwrapped from the `Update` (e.g. a `Message` or `CallbackQuery` instance). @@ -211,9 +211,9 @@ async def log_message( ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Lifecycle](../providers/lifecycle.md) — finalizers and container teardown. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Lifecycle](../providers/lifecycle.md): finalizers and container teardown. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API @@ -224,14 +224,14 @@ async def log_message( | `inject` | Decorator for an aiogram handler; resolves its `FromDI`-annotated parameters. Not needed when `setup_di(..., auto_inject=True)` is used. Raises `RuntimeError` naming `setup_di` when an update reaches it without the middleware installed. | | `fetch_di_container(dispatcher)` | Returns the root `Container` stored on the dispatcher. | | `aiogram_update_provider` | `ContextProvider` for the current `aiogram.types.Update` (REQUEST scope). | -| `aiogram_event_provider` | `ContextProvider` for the current `aiogram.types.TelegramObject` (REQUEST scope) — the concrete event unwrapped from the `Update`. | +| `aiogram_event_provider` | `ContextProvider` for the current `aiogram.types.TelegramObject` (REQUEST scope), the concrete event unwrapped from the `Update`. | ## Usage with `aiogram-dialog` [aiogram-dialog](https://github.com/Tishka17/aiogram_dialog) runs inside aiogram's dispatch, so the per-update child container that `setup_di`'s middleware already builds is reachable from dialog code. `modern_di_aiogram.dialog` -adds a dialog-aware `inject` for **getters** and **callbacks** (`on_click`, +adds a dialog-aware `inject` for getters and callbacks (`on_click`, `on_start`/`on_close`, `on_process_result`). Install it with the normal `setup_di(...)` and decorate your dialog functions: @@ -277,7 +277,7 @@ and a callback via the positional `DialogManager`'s `.middleware_data`. Dialog D requires the normal `setup_di(dispatcher, container)`, whose middleware provides the per-update container. -- `modern_di_aiogram.dialog` has **no runtime dependency** on `aiogram-dialog`; +- `modern_di_aiogram.dialog` has no runtime dependency on `aiogram-dialog`; install `aiogram-dialog` yourself. - The `FromDI` marker is the same one used for handlers; it is re-exported from `modern_di_aiogram.dialog` for convenience. diff --git a/docs/integrations/aiohttp.md b/docs/integrations/aiohttp.md index 7bde90f1..a8bb7e34 100644 --- a/docs/integrations/aiohttp.md +++ b/docs/integrations/aiohttp.md @@ -91,7 +91,7 @@ Unlike FastAPI, Litestar, and Starlette, aiohttp has no separate WebSocket object. A WebSocket is an upgraded `web.Request`, so `aiohttp_websocket_provider` binds `web.Request` too, and is declared `bound_type=None` (not resolvable by type, because `aiohttp_request_provider` already owns `web.Request`). That is why -you wire it **explicitly** with `FromDI(aiohttp_websocket_provider)` rather than +you wire it explicitly with `FromDI(aiohttp_websocket_provider)` rather than by type annotation. For per-message work, open a nested `Scope.REQUEST` child of the session @@ -154,10 +154,10 @@ on the first request to a decorated method. ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Async SQLAlchemy](../recipes/sqlalchemy.md) — engine + session + repository through the request container. -- [Lifecycle](../providers/lifecycle.md) — finalizers and `close_async()`. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Async SQLAlchemy](../recipes/sqlalchemy.md): engine + session + repository through the request container. +- [Lifecycle](../providers/lifecycle.md): finalizers and `close_async()`. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API @@ -169,4 +169,4 @@ on the first request to a decorated method. | `fetch_di_container(app)` | Returns the root `Container` stored on the app. | | `fetch_request_container(request)` | Returns the per-connection child container the middleware built (REQUEST for HTTP, SESSION for a WebSocket). Raises `RuntimeError` naming `setup_di` when the request did not pass through the middleware. | | `aiohttp_request_provider` | `ContextProvider` for `web.Request` (REQUEST scope), auto-registered by type. | -| `aiohttp_websocket_provider` | `ContextProvider` for the WebSocket connection's `web.Request` (SESSION scope), `bound_type=None` — resolve via `FromDI(aiohttp_websocket_provider)`. | +| `aiohttp_websocket_provider` | `ContextProvider` for the WebSocket connection's `web.Request` (SESSION scope), `bound_type=None`; resolve via `FromDI(aiohttp_websocket_provider)`. | diff --git a/docs/integrations/arq.md b/docs/integrations/arq.md index adbfe63b..3f02533d 100644 --- a/docs/integrations/arq.md +++ b/docs/integrations/arq.md @@ -101,7 +101,7 @@ unchanged. ## Scopes -The integration builds one `Scope.REQUEST` child container **per job** in +The integration builds one `Scope.REQUEST` child container per job in `on_job_start`. For an `@inject` task, the wrapper closes it with `close_async()` when the task body exits, whether it returned or raised. Nested or concurrent `@inject` calls in the same job share the child, and the last one @@ -143,16 +143,16 @@ container. `@inject` resolves dependencies by binding the task signature by name, which is what makes injection order-insensitive. A task that mixes a `FromDI` parameter with `*args` or `**kwargs` cannot be bound unambiguously, so `@inject` raises a -`TypeError` **at decoration time** rather than silently misrouting arguments. +`TypeError` at decoration time rather than silently misrouting arguments. Give an `@inject` task explicit named parameters. (A task with no `FromDI` parameter is untouched and may use `*args`/`**kwargs` freely.) ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Async resources via lifespan](../recipes/async-lifespan.md) — constructing async resources with finalizers. -- [Lifecycle](../providers/lifecycle.md) — finalizers and `close_async()`. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Async resources via lifespan](../recipes/async-lifespan.md): constructing async resources with finalizers. +- [Lifecycle](../providers/lifecycle.md): finalizers and `close_async()`. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API diff --git a/docs/integrations/celery.md b/docs/integrations/celery.md index b25b4c8f..cc865624 100644 --- a/docs/integrations/celery.md +++ b/docs/integrations/celery.md @@ -69,7 +69,7 @@ def run_report(report: typing.Annotated[Report, FromDI(Report)]) -> str: ## Scopes -The integration creates a `Scope.REQUEST` child container **for each task invocation**, whether wired via `@inject` or [`DITask`](#the-ditask-base-class). REQUEST-scoped providers (and their finalizers) live for the duration of that one call; the child container is closed with `close_sync()` once the task returns, including when it raises. APP-scoped providers persist for the whole worker process: `setup_di` opens the APP container on `worker_process_init` (or `worker_init`) and closes it with `close_sync()` on `worker_process_shutdown` (or `worker_shutdown`). +The integration creates a `Scope.REQUEST` child container for each task invocation, whether wired via `@inject` or [`DITask`](#the-ditask-base-class). REQUEST-scoped providers (and their finalizers) live for the duration of that one call; the child container is closed with `close_sync()` once the task returns, including when it raises. APP-scoped providers persist for the whole worker process: `setup_di` opens the APP container on `worker_process_init` (or `worker_init`) and closes it with `close_sync()` on `worker_process_shutdown` (or `worker_shutdown`). There is no `Scope.SESSION` for Celery: a task queue doesn't have a session concept comparable to websockets. @@ -183,16 +183,16 @@ signals.worker_process_shutdown.send(sender=None) # a real worker fires this ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Multi-Group organization](../recipes/multi-group.md) — structuring a larger container. -- [Lifecycle](../providers/lifecycle.md) — finalizers and container teardown. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Multi-Group organization](../recipes/multi-group.md): structuring a larger container. +- [Lifecycle](../providers/lifecycle.md): finalizers and container teardown. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API | Symbol | Description | |---|---| -| `setup_di(app, container)` | Wire the APP-scope container into Celery — stores it on `app.conf` and opens/closes it on `worker_process_init`/`worker_process_shutdown` and `worker_init`/`worker_shutdown`. Returns the container. | +| `setup_di(app, container)` | Wire the APP-scope container into Celery: stores it on `app.conf` and opens/closes it on `worker_process_init`/`worker_process_shutdown` and `worker_init`/`worker_shutdown`. Returns the container. | | `FromDI(provider_or_type)` | Marker for `Annotated[T, FromDI(...)]` in task signatures; accepts a provider instance or a plain type. | | `@inject` | Decorator that builds a `Scope.REQUEST` child container per call, resolves `FromDI`-annotated parameters from it, and closes the child container with `close_sync()` afterwards. Raises `RuntimeError` naming `setup_di` when a task reaches it without `setup_di` called. A task with `FromDI` parameters that also declares `*args`/`**kwargs` raises `TypeError` at decoration. | | `DITask` | `Task` subclass that applies `@inject` to a task's `run` method automatically; pass `task_cls=DITask` to `Celery(...)` or `base=DITask` to `@app.task(...)`. | diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md index 4e01b4dd..244ab7fc 100644 --- a/docs/integrations/fastapi.md +++ b/docs/integrations/fastapi.md @@ -1,6 +1,6 @@ # Usage with `FastAPI` -*More advanced example of usage with FastAPI - [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template)* +*More advanced example of usage with FastAPI: [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template)* ## How to use @@ -66,13 +66,13 @@ async def get_report( ``` !!! warning "Deployment: mounted sub-apps and disabled lifespan" - FastAPI only opens the root container from the ASGI **lifespan** event. A - `setup_di`-wired app **mounted as a sub-application** (`app.mount("/sub", + FastAPI only opens the root container from the ASGI lifespan event. A + `setup_di`-wired app mounted as a sub-application (`app.mount("/sub", subapp)`) never receives that event from its parent, and deployments that disable lifespan (e.g. Mangum `lifespan="off"`) skip it too. Requests still succeed (the container is already open from construction), but nothing ever closes it, so its finalizers never run at shutdown. Call - `setup_di` on the **top-level served app**, or close the root yourself + `setup_di` on the top-level served app, or close the root yourself (`await container.close_async()`) at shutdown. ## Websockets @@ -116,8 +116,8 @@ and explicit resolution work. The following context providers are available for import: -- `fastapi_request_provider` - Provides the current `fastapi.Request` object -- `fastapi_websocket_provider` - Provides the current `fastapi.WebSocket` object +- `fastapi_request_provider` provides the current `fastapi.Request` object +- `fastapi_websocket_provider` provides the current `fastapi.WebSocket` object ### Implicit (type-based) usage @@ -169,10 +169,10 @@ class AppGroup(Group): ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Async SQLAlchemy](../recipes/sqlalchemy.md) — engine + session + repository through the request container. -- [Lifecycle](../providers/lifecycle.md) — finalizers and `close_async()`. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Async SQLAlchemy](../recipes/sqlalchemy.md): engine + session + repository through the request container. +- [Lifecycle](../providers/lifecycle.md): finalizers and `close_async()`. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API @@ -180,7 +180,7 @@ class AppGroup(Group): |---|---| | `setup_di(app, container)` | Registers the container on the FastAPI app and appends a lifespan that closes it on shutdown (merges with any existing `lifespan=`); returns the container. | | `FromDI(dependency, *, use_cache=True)` | A `fastapi.Depends` wrapper that resolves a provider (or type) from the per-request child container. Raises `RuntimeError` naming `setup_di` when a request reaches it without `setup_di` called. | -| `build_di_container(connection)` | A `fastapi.Depends` callable that yields the per-request child container — REQUEST scope for an HTTP request, SESSION scope for a WebSocket. | +| `build_di_container(connection)` | A `fastapi.Depends` callable that yields the per-request child container: REQUEST scope for an HTTP request, SESSION scope for a WebSocket. | | `fastapi_request_provider` | `ContextProvider` for `fastapi.Request` (REQUEST scope), auto-registered. | | `fastapi_websocket_provider` | `ContextProvider` for `fastapi.WebSocket` (SESSION scope), auto-registered. | | `fetch_di_container(app)` | Returns the root `Container` stored on the app. Raises `RuntimeError` naming `setup_di` when called on an app without `setup_di` called. | diff --git a/docs/integrations/faststream.md b/docs/integrations/faststream.md index 045e9dd1..a9e02364 100644 --- a/docs/integrations/faststream.md +++ b/docs/integrations/faststream.md @@ -101,7 +101,7 @@ Between `setup_di` and startup no broker carries the middleware yet; see ## Scopes -The integration creates a `Scope.REQUEST` child container **for each message** the subscriber receives. REQUEST-scoped providers (and their finalizers) live for the duration of that one message; APP-scoped providers persist for the whole process. At app shutdown, the integration runs `await container.close_async()` on the APP container. +The integration creates a `Scope.REQUEST` child container for each message the subscriber receives. REQUEST-scoped providers (and their finalizers) live for the duration of that one message; APP-scoped providers persist for the whole process. At app shutdown, the integration runs `await container.close_async()` on the APP container. There is no `Scope.SESSION` for FastStream: message brokers don't have a session concept comparable to websockets. @@ -111,7 +111,7 @@ There is no `Scope.SESSION` for FastStream: message brokers don't have a session The following context provider is also available for explicit import: -- `faststream_message_provider` — provides the current `faststream.StreamMessage` object. +- `faststream_message_provider` provides the current `faststream.StreamMessage` object. ### Implicit (type-based) usage @@ -174,16 +174,16 @@ class AppGroup(Group): ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Async resources via lifespan](../recipes/async-lifespan.md) — constructing async resources with finalizers. -- [Lifecycle](../providers/lifecycle.md) — finalizers and `close_async()`. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Async resources via lifespan](../recipes/async-lifespan.md): constructing async resources with finalizers. +- [Lifecycle](../providers/lifecycle.md): finalizers and `close_async()`. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API | Symbol | Description | |---|---| -| `setup_di(app, container)` | Wire the APP-scope container into FastStream — at startup, installs the middleware that creates a REQUEST child container per message on every broker of the app, and raises `RuntimeError` if there is none by then; closes the APP container at shutdown. | +| `setup_di(app, container)` | Wire the APP-scope container into FastStream: at startup, installs the middleware that creates a REQUEST child container per message on every broker of the app, and raises `RuntimeError` if there is none by then; closes the APP container at shutdown. | | `FromDI(dependency, *, use_cache=True, cast=False)` | A `faststream.Depends` wrapper for `Annotated[T, FromDI(...)]` in subscriber signatures; accepts a provider instance or a plain type. `use_cache` and `cast` are passed through to `faststream.Depends`. Raises `RuntimeError` naming `setup_di` when a message reaches it without the middleware installed. | | `fetch_di_container(app)` | Returns the APP-scope container registered with the FastStream app. | | `faststream_message_provider` | `ContextProvider` for the current `faststream.StreamMessage`. | diff --git a/docs/integrations/flask.md b/docs/integrations/flask.md index f344adac..9eca7f63 100644 --- a/docs/integrations/flask.md +++ b/docs/integrations/flask.md @@ -4,7 +4,7 @@ Flask has no dependency-injection system of its own, so `modern-di-flask` uses the `@inject` decorator with `FromDI` markers (there is no `Depends`). `setup_di` installs a `before_request`/`teardown_appcontext` pair that opens a per-request `Scope.REQUEST` child container and closes it once the request finishes. -Resolution is **sync-only**, and the child container is closed with `close_sync()`. +Resolution is sync-only, and the child container is closed with `close_sync()`. ## How to use @@ -155,7 +155,7 @@ for how implicit and explicit resolution work. The following context provider is available for import: -- `flask_request_provider` — `ContextProvider` for the current `flask.Request` (REQUEST scope), auto-registered by type. +- `flask_request_provider` is a `ContextProvider` for the current `flask.Request` (REQUEST scope), auto-registered by type. ### Implicit (type-based) usage @@ -194,16 +194,16 @@ class AppGroup(Group): ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Multi-Group organization](../recipes/multi-group.md) — structuring a larger container. -- [Lifecycle](../providers/lifecycle.md) — finalizers and container teardown. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Multi-Group organization](../recipes/multi-group.md): structuring a larger container. +- [Lifecycle](../providers/lifecycle.md): finalizers and container teardown. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API | Symbol | Description | |---|---| -| `setup_di(app, container, *, auto_inject=False)` | Registers the container on `app.extensions`, installs the `before_request`/`teardown_appcontext` pair that builds and closes a per-request `Scope.REQUEST` child container, and — if `auto_inject=True` — wraps every currently-registered view with `inject`; returns the container. | +| `setup_di(app, container, *, auto_inject=False)` | Registers the container on `app.extensions`, installs the `before_request`/`teardown_appcontext` pair that builds and closes a per-request `Scope.REQUEST` child container, and, if `auto_inject=True`, wraps every currently-registered view with `inject`; returns the container. | | `FromDI(dependency)` | Marker (used with `@inject`) that resolves a provider or type from the per-request child container. | | `inject` | Decorator for a view function; resolves its `FromDI`-annotated parameters without rewriting the function's signature. Raises `RuntimeError` naming `setup_di` when a request reaches it without `setup_di` called. | | `fetch_di_container(app)` | Returns the root `Container` stored on `app.extensions`. | diff --git a/docs/integrations/grpc.md b/docs/integrations/grpc.md index 5fb46491..72852f20 100644 --- a/docs/integrations/grpc.md +++ b/docs/integrations/grpc.md @@ -117,7 +117,7 @@ four RPC types on either server. ## Scopes -The integration opens one `Scope.REQUEST` child container **per RPC call**, for +The integration opens one `Scope.REQUEST` child container per RPC call, for all four RPC types (unary-unary, server-streaming, client-streaming, bidi). The child is created when the RPC starts and closed when it ends. For a streaming RPC it stays open for the whole stream and closes after the last message, @@ -151,14 +151,14 @@ class AppGroup(Group): `validate()` never constructs a provider, so the default isn't needed for validation. The `| None = None` default lets the provider resolve outside an RPC, where no context is set: the creator gets `None` instead of the resolve raising -`ArgumentResolutionError`. The protobuf request `Message` is **not** exposed as a provider +`ArgumentResolutionError`. The protobuf request `Message` is not exposed as a provider (that would add a `protobuf` dependency); the request is already a servicer-method argument. ## Root container lifecycle -gRPC has no server startup/shutdown hook, so the **root container's lifecycle is -yours to own** (as with Flask). Create the container open, pass it to the +gRPC has no server startup/shutdown hook, so the root container's lifecycle is +yours to own (as with Flask). Create the container open, pass it to the interceptor, and close it after the server stops to run APP-scoped finalizers: ```python @@ -185,9 +185,9 @@ imposes no restriction on the method signature beyond the injected parameters. ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Lifecycle](../providers/lifecycle.md) — finalizers and container teardown. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Lifecycle](../providers/lifecycle.md): finalizers and container teardown. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md index 25d5811d..e4919623 100644 --- a/docs/integrations/litestar.md +++ b/docs/integrations/litestar.md @@ -1,6 +1,6 @@ # Usage with `Litestar` -*More advanced example of usage with Litestar - [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template)* +*More advanced example of usage with Litestar: [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template)* ## How to use @@ -155,8 +155,8 @@ and explicit resolution work. The following context providers are available for import: -- `litestar_request_provider` - Provides the current `litestar.Request` object -- `litestar_websocket_provider` - Provides the current `litestar.WebSocket` object +- `litestar_request_provider` provides the current `litestar.Request` object +- `litestar_websocket_provider` provides the current `litestar.WebSocket` object ### Implicit (type-based) usage @@ -208,10 +208,10 @@ class AppGroup(Group): ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Async SQLAlchemy](../recipes/sqlalchemy.md) — engine + session + repository through the request container. -- [Lifecycle](../providers/lifecycle.md) — finalizers and `close_async()`. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Async SQLAlchemy](../recipes/sqlalchemy.md): engine + session + repository through the request container. +- [Lifecycle](../providers/lifecycle.md): finalizers and `close_async()`. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API diff --git a/docs/integrations/pytest.md b/docs/integrations/pytest.md index 97301f3c..45463452 100644 --- a/docs/integrations/pytest.md +++ b/docs/integrations/pytest.md @@ -128,7 +128,7 @@ one or more `Group` subclasses can be exposed against the request container. ## Overrides -`modern-di-pytest` deliberately does **not** ship override sugar. Use +`modern-di-pytest` deliberately does not ship override sugar. Use `Container.override()` directly; it is already backed by a tree-shared `OverridesRegistry`. @@ -172,5 +172,5 @@ For deeper patterns (transactional DB sessions, resetting all overrides) see the ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — override patterns beyond fixtures. -- [Scopes](../providers/scopes.md) — session vs request container fixtures. +- [Testing with overrides](../recipes/testing-overrides.md): override patterns beyond fixtures. +- [Scopes](../providers/scopes.md): session vs request container fixtures. diff --git a/docs/integrations/starlette.md b/docs/integrations/starlette.md index 84840323..e76978ee 100644 --- a/docs/integrations/starlette.md +++ b/docs/integrations/starlette.md @@ -74,13 +74,13 @@ container.validate() # after setup_di — its connection providers are now regi ``` !!! warning "Deployment: mounted sub-apps and disabled lifespan" - Starlette only opens the root container from the ASGI **lifespan** event. - A `setup_di`-wired app **mounted as a sub-application** + Starlette only opens the root container from the ASGI lifespan event. + A `setup_di`-wired app mounted as a sub-application (`app.mount("/sub", subapp)`) never receives that event from its parent, and deployments that disable lifespan (e.g. Mangum `lifespan="off"`) skip it too. Requests still succeed (the container is already open from construction), but nothing ever closes it, so its finalizers never run at - shutdown. Call `setup_di` on the **top-level served app**, or close the + shutdown. Call `setup_di` on the top-level served app, or close the root yourself (`await container.close_async()`) at shutdown. ### 3. Scopes @@ -172,8 +172,8 @@ for how implicit and explicit resolution work. The following context providers are available for import: -- `starlette_request_provider` — the current `starlette.requests.Request` (REQUEST scope) -- `starlette_websocket_provider` — the current `starlette.websockets.WebSocket` (SESSION scope) +- `starlette_request_provider` provides the current `starlette.requests.Request` (REQUEST scope) +- `starlette_websocket_provider` provides the current `starlette.websockets.WebSocket` (SESSION scope) ### Implicit (type-based) usage @@ -212,10 +212,10 @@ class AppGroup(Group): ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Async SQLAlchemy](../recipes/sqlalchemy.md) — engine + session + repository through the request container. -- [Lifecycle](../providers/lifecycle.md) — finalizers and `close_async()`. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Async SQLAlchemy](../recipes/sqlalchemy.md): engine + session + repository through the request container. +- [Lifecycle](../providers/lifecycle.md): finalizers and `close_async()`. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API diff --git a/docs/integrations/taskiq.md b/docs/integrations/taskiq.md index 549eb819..365a651f 100644 --- a/docs/integrations/taskiq.md +++ b/docs/integrations/taskiq.md @@ -64,7 +64,7 @@ async def get_report( return report.as_dict() ``` -`setup_di(broker, container)` stores the container on `broker.state` and registers `TaskiqEvents.WORKER_STARTUP`/`WORKER_SHUTDOWN` handlers that open/close it. Those fire when the broker's worker process starts and stops, so a script that just calls tasks directly (like `InMemoryBroker` in a test) must drive the container lifecycle itself, e.g. `async with broker: ...` or an explicit `container.open()` / `await container.close_async()`. +`setup_di(broker, container)` stores the container on `broker.state` and registers `TaskiqEvents.WORKER_STARTUP`/`WORKER_SHUTDOWN` handlers that open/close it. Those fire when the broker's worker process starts and stops, so a script that calls tasks directly (like `InMemoryBroker` in a test) must drive the container lifecycle itself, e.g. `async with broker: ...` or an explicit `container.open()` / `await container.close_async()`. !!! warning "Deployment: `run_receiver_task` skips startup by default" `taskiq.api.run_receiver_task(...)` defaults `run_startup=False`, which @@ -76,7 +76,7 @@ async def get_report( ## Scopes -The integration creates a `Scope.REQUEST` child container **for each task** that uses `FromDI`, built lazily through `TaskiqDepends` when the task's dependencies are resolved. A task with no `FromDI` parameter gets no child container. REQUEST-scoped providers (and their finalizers) live for the duration of that one task: the child container is closed after the task returns, including when it raises. APP-scoped providers persist for the whole worker process; `setup_di` opens the APP container on `WORKER_STARTUP` and runs `await container.close_async()` on `WORKER_SHUTDOWN`. +The integration creates a `Scope.REQUEST` child container for each task that uses `FromDI`, built lazily through `TaskiqDepends` when the task's dependencies are resolved. A task with no `FromDI` parameter gets no child container. REQUEST-scoped providers (and their finalizers) live for the duration of that one task: the child container is closed after the task returns, including when it raises. APP-scoped providers persist for the whole worker process; `setup_di` opens the APP container on `WORKER_STARTUP` and runs `await container.close_async()` on `WORKER_SHUTDOWN`. There is no `Scope.SESSION` for taskiq: a task queue doesn't have a session concept comparable to websockets. @@ -90,7 +90,7 @@ There is no `Scope.SESSION` for taskiq: a task queue doesn't have a session conc The following context provider is also available for explicit import: -- `taskiq_message_provider` — provides the current `taskiq.TaskiqMessage` object. +- `taskiq_message_provider` provides the current `taskiq.TaskiqMessage` object. ### Implicit (type-based) usage @@ -136,16 +136,16 @@ class AppGroup(Group): ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Async resources via lifespan](../recipes/async-lifespan.md) — constructing async resources with finalizers. -- [Lifecycle](../providers/lifecycle.md) — finalizers and `close_async()`. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Async resources via lifespan](../recipes/async-lifespan.md): constructing async resources with finalizers. +- [Lifecycle](../providers/lifecycle.md): finalizers and `close_async()`. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API | Symbol | Description | |---|---| -| `setup_di(broker, container)` | Wire the APP-scope container into taskiq — a REQUEST child container is then built through `TaskiqDepends` for each task that uses `FromDI`; opens/closes the APP container on worker startup/shutdown. | +| `setup_di(broker, container)` | Wire the APP-scope container into taskiq: a REQUEST child container is then built through `TaskiqDepends` for each task that uses `FromDI`; opens/closes the APP container on worker startup/shutdown. | | `FromDI(provider_or_type, *, use_cache=True)` | Marker for `Annotated[T, FromDI(...)]` in task signatures; accepts a provider instance or a plain type. `use_cache` is passed through to `TaskiqDepends`. Raises `RuntimeError` naming `setup_di` when a task reaches it without `setup_di` called. | | `fetch_di_container(broker)` | Returns the APP-scope container registered with the taskiq broker. | | `taskiq_message_provider` | `ContextProvider` for the current `taskiq.TaskiqMessage`. | diff --git a/docs/integrations/typer.md b/docs/integrations/typer.md index f40e6d91..786bf44f 100644 --- a/docs/integrations/typer.md +++ b/docs/integrations/typer.md @@ -110,9 +110,9 @@ def run_job( ## See also -- [Testing with overrides](../recipes/testing-overrides.md) — swap providers in your tests. -- [Lifecycle](../providers/lifecycle.md) — finalizers and container teardown. -- [Scopes](../providers/scopes.md) — the APP → REQUEST lifetime model. +- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests. +- [Lifecycle](../providers/lifecycle.md): finalizers and container teardown. +- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model. ## API diff --git a/docs/integrations/writing-integrations.md b/docs/integrations/writing-integrations.md index 9caf605b..a4b12475 100644 --- a/docs/integrations/writing-integrations.md +++ b/docs/integrations/writing-integrations.md @@ -1,6 +1,6 @@ # Writing an integration -This page is the specification for building a **modern-di integration** for a +This page is the specification for building a modern-di integration for a framework that does not yet have one (an ASGI app, a message broker, a CLI, a test runner...). It is written to be followed step by step: implement the contract below, mirror the scaffolding, and check every box in the final @@ -77,18 +77,18 @@ def fetch_di_container(app: myfw.App) -> Container: return typing.cast(Container, app.state.di_container) ``` -Store and read under a **named constant**, not a repeated string literal, when +Store and read under a named constant, not a repeated string literal, when the framework uses a string-keyed store (FastStream's `ContextRepo`, Typer's `context_settings["obj"]`); it keeps writer and reader in provable agreement. ### 4. Per-unit-of-work child-container builder Build a child container at the connection's scope, inject the connection object -as context, hand it to the handler, and **close it in `finally`**. The shape +as context, hand it to the handler, and close it in `finally`. The shape depends on how the framework runs handlers: -- **Dependency generator** (FastAPI, Litestar) — an `async def` that `yield`s - the container and closes after. Derive the child's scope and context with +- With a dependency generator (FastAPI, Litestar), an `async def` `yield`s + the container and closes it after. Derive the child's scope and context with `modern_di.integrations.classify_connection`, which picks the first provider the connection is an instance of and returns its scope + a `{context_type: connection}` context, or `None` if nothing matches: @@ -114,35 +114,35 @@ depends on how the framework runs handlers: own. The `with`/`async with` here buys guaranteed cleanup on the way out; it is not a required open step or a fail-fast check. - For a **single** connection kind with no dispatch to do, call + For a single connection kind with no dispatch to do, call `integrations.bind(my_provider, connection)` directly; it returns the same scope + context for one provider without the isinstance scan. An adapter - whose unit of work carries **no** connection object (a Typer command) skips + whose unit of work carries no connection object (a Typer command) skips the kit entirely and calls `build_child_container(scope=...)` directly. See [How the existing integrations realize the contract](#how-the-existing-integrations-realize-the-contract) for which shape fits which adapter. -- **Middleware** (FastStream) — a `BaseMiddleware` whose `consume_scope` builds +- With middleware (FastStream), the `consume_scope` of a `BaseMiddleware` builds the child, stashes it in the framework context for the duration of the call, and closes it in `finally`. -- **Decorator** (Typer) — an `inject` decorator that wraps the command, opens a +- With a decorator (Typer), an `inject` decorator wraps the command, opens a child container for the command's duration, resolves the marked parameters, and closes the container (synchronously) on exit. ### 5. `FromDI` marker + `Dependency` resolver -`FromDI(dependency)` accepts a provider **or** a type and, at a handler's call +`FromDI(dependency)` accepts a provider or a type and, at a handler's call site, stands in for the resolved value: `x: Annotated[Foo, FromDI(foo_provider)]`. How it delivers that value splits into two modes depending on the framework: -- **Native-DI frameworks** (FastAPI, FastStream, Litestar) have a per-handler +- Native-DI frameworks (FastAPI, FastStream, Litestar) have a per-handler injection seam: `Depends`, `Provide`. `FromDI` returns that native marker and the framework calls your resolver with the request container. This is the path documented below. -- **Frameworks with no request-scoped DI** (Typer/Click CLIs, argparse, task - runners) have no seam. `FromDI` returns an inert marker and a **decorator** +- Frameworks with no request-scoped DI (Typer/Click CLIs, argparse, task + runners) have no seam. `FromDI` returns an inert marker and a decorator does the resolution. See [Frameworks without native DI](#frameworks-without-native-di-the-decorator-path). @@ -185,7 +185,7 @@ from whichever it dispatches to. `FromDI` is spelled in PascalCase (with existing lifespan rather than replacing it. - With callback hooks: `app.on_startup(container.open)` and `app.after_shutdown(container.close_async)`. Calling `open()` on an - already-open container **is** a no-op: it unconditionally clears + already-open container is a no-op: it unconditionally clears `closed`, runs no validation, and costs nothing either way. - Always close the child container in `finally`. Never leak a unit-of-work container on the error path. @@ -198,9 +198,8 @@ from whichever it dispatches to. `FromDI` is spelled in PascalCase (with integration's own connection providers (typically via `add_providers`), so a `validate()` call made *before* `setup_di` sees an incomplete graph and raises for any service that depends on the connection object *by type*, which - genuinely isn't registered yet. Calling `validate()` after `setup_di` sees the - complete graph. Validating at all is optional, and nothing requires a caller - to do it, but document the ordering for whoever does. + isn't registered yet. Calling `validate()` after `setup_di` sees the + complete graph. Validating at all is optional, but document the ordering for whoever does. - Open the root in *every* execution context the framework runs work in. A worker may dispatch units of work from more than one place: Celery fires `worker_process_init` only for the prefork/solo pools, never for the @@ -247,8 +246,8 @@ Pattern-match your framework to the closest precedent. | `FromDI` bridge | `fastapi.Depends(Dependency(Marker(...)))` | `faststream.Depends(Dependency(Marker(...)))` | `Provide(_Dependency(Marker(...)))` | inert `Marker` (`integrations.from_di`) + `inject` | | Child close | `close_async` | `close_async` | `close_async` | `close_sync` | -The **Starlette** integration ([`modern-di-starlette`](starlette.md)) is the -reference for a **middleware + decorator hybrid**: Starlette has no native DI, +The Starlette integration ([`modern-di-starlette`](starlette.md)) is the +reference for a middleware + decorator hybrid: Starlette has no native DI, so a pure-ASGI middleware owns the child-container lifecycle (like FastStream) while an `@inject` decorator with an inert `FromDI` marker does resolution (like Typer). It splits the two responsibilities of the decorator path: the middleware @@ -256,7 +255,7 @@ builds and closes the per-connection child, and the decorator only reads it back from the ASGI scope and resolves. See [Frameworks without native DI](#frameworks-without-native-di-the-decorator-path). -The **aiohttp** integration ([`modern-di-aiohttp`](aiohttp.md)) is another +The aiohttp integration ([`modern-di-aiohttp`](aiohttp.md)) is another middleware + decorator hybrid, for a non-ASGI server where the only connection object at middleware entry is `web.Request`: a WebSocket is an upgraded HTTP request, not a distinct type. It detects a WebSocket via @@ -267,20 +266,20 @@ both connection providers bind `web.Request`, it registers reference-only (`bound_type=None`). Its root lifecycle rides aiohttp's `on_startup`/`on_cleanup` signals rather than a composed lifespan. -The **pytest** integration +The pytest integration ([`modern-di-pytest`](pytest.md)) is a different shape: it has no app to wire, so instead of `setup_di`/`FromDI` it exposes `modern_di_fixture` (turn one dependency into a fixture) and `expose` (turn a `Group`'s providers into fixtures). It resolves from a user-supplied `di_container` fixture. Follow it -when integrating a **test runner** rather than an application framework. +when integrating a test runner. ## Frameworks without native DI (the decorator path) -Contract points 4 and 5 assume a **per-handler injection seam** (FastAPI / +Contract points 4 and 5 assume a per-handler injection seam (FastAPI / FastStream `Depends`, Litestar `Provide`) that you hand a native marker and that calls your resolver with the request container. Some frameworks have none: a Typer/Click command, an argparse handler, or a plain task callable receives -only what the framework's argument parser binds. There is nowhere to inject. +only what the framework's argument parser binds. The rule: an integration is decorator-free only where the framework evaluates a parameter default as a provider (FastAPI and FastStream `Depends`, Litestar `Provide`, taskiq @@ -289,7 +288,7 @@ callable, and aiogram matches its `data` dict by parameter name, so those need ` of them apply it for you: Flask and aiogram take `setup_di(..., auto_inject=True)`, and Celery has the `DITask` base class. -For these, `FromDI` becomes an inert annotation marker and a **decorator** does +For these, `FromDI` becomes an inert annotation marker and a decorator does the work native DI would have. [`modern-di-typer`](typer.md)'s `@inject` is the reference implementation. Reach for this shape whenever the framework runs handlers as plain callables it parses arguments for. The decorator can build the @@ -308,9 +307,9 @@ either way. service: typing.Annotated[MyService, FromDI(Dependencies.service)] ``` -- **Decoration time** — the decorator introspects +- At decoration time, the decorator introspects `typing.get_type_hints(func, include_extras=True)`, finds parameters whose - `Annotated` metadata holds a `Marker`, then **rewrites the signature**: + `Annotated` metadata holds a `Marker`, then rewrites the signature: *remove* those parameters (so the arg parser never treats them as CLI options) and *insert* the framework's context parameter (`typer.Context`) at position 0 if the handler didn't declare one. Assign the cleaned signature to @@ -326,14 +325,14 @@ either way. `@inject` per handler), guard against double-wrapping with `integrations.is_injected(func)` / `integrations.mark_injected(wrapper)`. -- **Call time** — bind incoming args against the rewritten signature, pull out +- At call time, bind incoming args against the rewritten signature, pull out the context object (deleting it again if the decorator added it implicitly), build the per-call child container, resolve each marked parameter by kind (contract point 5), fill them into the call by name, invoke the original function, and `close_sync` the container in `finally`. DI parameters coexist with ordinary framework parameters because the decorator -strips **only** the marked ones; everything else still reaches the parser. +strips only the marked ones; everything else still reaches the parser. ### What changes vs. the native path @@ -353,7 +352,7 @@ strips **only** the marked ones; everything else still reaches the parser. - Strip only DI params. Leave real arguments/options in the signature or the framework stops parsing them. - Get the decorator order right. The framework's own registration decorator goes - **outside**: `@app.command()` above `@inject`, so it registers the rewritten + outside: `@app.command()` above `@inject`, so it registers the rewritten signature. - Isolate per-call state. Stash the per-call container on a per-invocation store (`ctx.meta`), not shared app state (`ctx.obj`), so nested scopes can @@ -379,55 +378,55 @@ strips **only** the marked ones; everything else still reaches the parser. Each official integration is its own repository and PyPI package, mirroring the `modern-di` repo's tooling. -- **Names.** Repo and PyPI package `modern-di-`; import package +- Name the repo and PyPI package `modern-di-` and the import package `modern_di_`. -- **Layout.** - - `modern_di_/main.py` — the implementation. A larger integration may add +- Lay out the package as follows: + - `modern_di_/main.py` holds the implementation. A larger integration may add modules beside it, as aiogram does with `dialog.py`; pytest keeps its code in `factory.py`. - - `modern_di_/__init__.py` — re-export the public API from - `main` and list it in an explicit `__all__` (this is the integration's + - `modern_di_/__init__.py` re-exports the public API from + `main` and lists it in an explicit `__all__` (this is the integration's surface; keep private helpers out of it). -- **`pyproject.toml`.** `name = "modern-di-"`, +- In `pyproject.toml`, set `name = "modern-di-"`, `description = "modern-di integration for "`, dependencies `[">=...,<...", "modern-di>=,<4"]`, the standard `classifiers` (Typed, supported Python versions) and `[project.urls]` pointing - at the shared docs site and the integration's own repo. `version = "0"`, since + at the shared docs site and the integration's own repo. Use `version = "0"`, since the release tag sets it. -- **Tests** (`tests/`): - - `conftest.py` — fixtures that build an app, call `setup_di` (or install the +- Put tests in `tests/`: + - `conftest.py` holds fixtures that build an app, call `setup_di` (or install the plugin) with a `Container(groups=[Dependencies])`, and yield a test client. - - `dependencies.py` — a sample `Group` with `Factory` providers at several + - `dependencies.py` defines a sample `Group` with `Factory` providers at several scopes, plus providers that read the connection object (e.g. a request header) to prove context injection works. - `test_lifespan.py` (startup/shutdown + restart), `test_routes.py` / `test_commands.py` (resolution through `FromDI`), and `test_websockets.py` where the framework has websockets. Aim for the same 100%-coverage gate `modern-di` holds. -- **Canonical example** (`examples/`). Ship a runnable `examples/app.py` (plus an - empty `examples/__init__.py`) demonstrating the recommended wiring: an +- Ship a runnable canonical example, `examples/app.py` (plus an + empty `examples/__init__.py`), demonstrating the recommended wiring: an APP-scoped `Settings` plus one work-scoped service that depends on it by type, resolved into a single handler/task/command via the framework's real idiom. Use the `typing.Annotated[T, FromDI(...)]` marker form, not a `= FromDI(...)` default (the default-call form trips ruff `B008`). Name the example's types to match the integration's `docs/integrations/.md` snippet; diverge only where testability requires it (e.g. return a value the test can assert). - A `tests/test_example.py` **smoke test** drives it through the repo's own + A `tests/test_example.py` smoke test drives it through the repo's own in-memory test double (test client / eager mode / in-memory broker, whatever - the existing tests use) and asserts the **real injected output**, never a mock. - The smoke test must cover `examples/app.py` to **100%** under the coverage - gate. Do **not** add a coverage `omit`; mark `# pragma: no cover` only on a + the existing tests use) and asserts the real injected output, never a mock. + The smoke test must cover `examples/app.py` to 100% under the coverage + gate. Do not add a coverage `omit`; mark `# pragma: no cover` only on a genuinely unreachable boot line (`if __name__ == "__main__"` / server-run). Link it from the README with a `Usage example: [examples/](./examples)` line directly under `Full guide:`. -- **Tooling.** Mirror `modern-di`'s `AGENTS.md` and `justfile`. Keep behavioural +- For tooling, mirror `modern-di`'s `AGENTS.md` and `justfile`. Keep behavioural invariants in named tests rather than in a prose truth home, and record rejected alternatives on the [design decisions](../introduction/design-decisions.md#non-goals) page. Keep resolution sync-only and add no runtime dependency beyond the framework and `modern-di`. `ruff` is unpinned and CI floats it forward, so keep `CPY001` (no per-file copyright header) in the lint `ignore` and reflow any pre-existing Markdown-embedded code fences the current `ruff` reformats. -- **Docs.** Add a `docs/integrations/.md` usage page **in the - `modern-di` repo** and a nav entry for it in `mkdocs.yml` (under the matching +- For docs, add a `docs/integrations/.md` usage page in the + `modern-di` repo and a nav entry for it in `mkdocs.yml` (under the matching family group: Web / Tasks & events / Bots / RPC / CLI / Testing). Follow the canonical page shape the existing pages use: a single realistic-but-compact example, an APP-scoped `Settings` plus one work-scoped service (two providers, @@ -435,7 +434,7 @@ Each official integration is its own repository and PyPI package, mirroring the (`Container(groups=[AppGroup])`, no `validate=` argument, which is deprecated and does nothing) and `container.validate()` called explicitly *after* `setup_di`, demonstrating the [ordering rule](#lifecycle-rules) above. Keep the - connection/message object **out** of that validated example: its + connection/message object out of that validated example: its `ContextProvider` is registered by `setup_di`, so a service that requires it by type would fail `validate()` if that call were placed *before* `setup_di`. Demonstrate context injection in a dedicated "Framework context objects" @@ -451,7 +450,7 @@ Each official integration is its own repository and PyPI package, mirroring the [Lifecycle](../providers/lifecycle.md), [Scopes](../providers/scopes.md), and the most relevant recipe, and the `## API` table last. Integrations do not ship their own docs site. -- **Release.** Tag-driven, mirroring `modern-di`: push a bare semver tag off +- Releases are tag-driven, mirroring `modern-di`: push a bare semver tag off green `main` and let the workflow publish. ## Checklist @@ -465,14 +464,14 @@ Each official integration is its own repository and PyPI package, mirroring the - [ ] `fetch_di_container` reads the root container back out of framework state. - [ ] A per-unit-of-work builder opens a child container at the right scope, injects the connection as context, and closes it in `finally`. -- [ ] Root container **reopens on startup** so a restart doesn't rely on the +- [ ] Root container reopens on startup so a restart doesn't rely on the implicit-reuse warning (`ContainerClosedWarning`) and gets finalizers wired to shutdown. - [ ] `close_async` / `close_sync` matches the framework's async-ness. - [ ] `FromDI` accepts `AbstractProvider[T] | type[T]` and resolves it via `resolve_dependency`. Use `modern_di.integrations.from_di` (or a factory wrapping `integrations.Marker`) rather than hand-rolling it. -- [ ] **No native DI?** `FromDI` is an inert marker and a decorator rewrites the +- [ ] Without native DI, `FromDI` is an inert marker and a decorator rewrites the handler signature (strips DI params, threads the context object, sets `wrapper.__signature__`), resolves at call time, and closes the per-call container in `finally`. See the [decorator diff --git a/docs/introduction/about-di.md b/docs/introduction/about-di.md index 8dea4940..72e1172f 100644 --- a/docs/introduction/about-di.md +++ b/docs/introduction/about-di.md @@ -70,6 +70,6 @@ per request, or a fresh one on every call. `modern-di` expresses this with ## See also -- [modern-di vs other libraries](comparison.md) — including whether you need a container at all. -- [Quickstart](../index.md) — modern-di's own syntax, end to end. -- [Design decisions](design-decisions.md) — the reasoning behind the API's choices. +- [modern-di vs other libraries](comparison.md), including whether you need a container at all. +- [Quickstart](../index.md): modern-di's own syntax, end to end. +- [Design decisions](design-decisions.md): the reasoning behind the API's choices. diff --git a/docs/introduction/comparison.md b/docs/introduction/comparison.md index 430c991b..bf4a3ea4 100644 --- a/docs/introduction/comparison.md +++ b/docs/introduction/comparison.md @@ -1,8 +1,7 @@ # modern-di vs other libraries -modern-di isn't the only way to do dependency injection in Python. This page -covers where it fits among the alternatives, including when you don't need a DI -container at all. +This page covers where modern-di fits among the other ways to do dependency +injection in Python, including when you don't need a DI container at all. ## Do you even need a DI container? @@ -13,15 +12,15 @@ standalone container is overkill. Reach for a container when one of these is true: -- **More than one entrypoint.** An API *and* a worker (FastStream/Celery) *and* - a CLI (Typer), all sharing one wiring instead of three parallel copies. -- **Typed, app-scoped singletons with real teardown** — instead of an untyped - `app.state` bag plus `lru_cache` with no cleanup. -- **Resolution off the request path** — in startup, background tasks, workers, or - CLI commands, where `Depends`/`Provide` simply don't run. -- **Whole-app test overrides** — swap a dependency once and have every entrypoint - (HTTP, worker, CLI, direct unit tests) see it, not just code reached through - the HTTP layer. +- You have more than one entrypoint: an API *and* a worker (FastStream/Celery) + *and* a CLI (Typer) can share one wiring instead of three parallel copies. +- You want typed, app-scoped singletons with real teardown, where the usual + alternative is an untyped `app.state` bag plus `lru_cache` with no cleanup. +- You resolve dependencies off the request path: in startup, background tasks, + workers, or CLI commands, where `Depends`/`Provide` don't run. +- You want whole-app test overrides: swap a dependency once and every + entrypoint (HTTP, worker, CLI, direct unit tests) sees it, including code the + HTTP layer never reaches. modern-di covers those cases with one typed wiring shared across twelve frameworks: aiohttp, FastAPI, Litestar, FastStream, Starlette, Typer, Flask, @@ -37,10 +36,10 @@ gRPC, Celery, arq, taskiq, and aiogram. | First-party pytest plugin | ✅ | ❌ | ❌ | ❌ | n/a | | Integrations | 12 official frameworks (aiogram, aiohttp, arq, Celery, FastAPI, FastStream, Flask, gRPC, Litestar, Starlette, taskiq, Typer) + a pytest plugin | 13 official frameworks + ~10 community-maintained | aiohttp, Flask, Starlette; FastAPI via wiring | Flask (1st-party), FastAPI (3rd-party) | n/a | | Typed resolution | ✅ | ✅ | partial | ✅ | callable-keyed | -| License | MIT | Apache-2.0 | BSD-3 | BSD-3 | — | +| License | MIT | Apache-2.0 | BSD-3 | BSD-3 | n/a | | Adoption | newest, very active | established, large community | most popular, mature | mature | built into FastAPI | -On the **typed-resolution** row: modern-di keeps the concrete static type end to +On the typed-resolution row: modern-di keeps the concrete static type end to end. `resolve(SomeType)` is typed `SomeType` (not `Any`), and the injection marker for integrations, `Annotated[T, from_di(dep)]`, type-checks as `T` (the same clean shape as Dishka's `FromDishka[T]` and FastAPI's @@ -62,23 +61,23 @@ and Starlette support are two of those community packages ([`dishka-faststream`](https://github.com/faststream-community/dishka-faststream) and [`starlette-dishka`](https://github.com/reagento/starlette-dishka)); the bundled `dishka.integrations` modules for both are deprecated in favor of them. -If you need **arbitrary *named* scopes** or **async resolution**, Dishka is an +If you need arbitrary *named* scopes or async resolution, Dishka is an excellent choice, as it is if you need an integration modern-di doesn't have yet: aiogram-dialog, Click, Sanic and telebot officially, or Pyramid, Quart, RQ, Strawberry and APScheduler from the community. modern-di's deliberate differences: -- **A first-party pytest plugin** (`modern-di-pytest`) that turns any dependency - into a fixture. Dishka ships no pytest *plugin*, documenting a hand-written - fixtures recipe instead. -- **Sync-only *resolution* (async finalizers still supported) and a small, - built-in scope chain you can still extend with any `IntEnum`** — a simpler - model. Dishka's own docs note that custom scopes are "hardly ever needed," +- A first-party pytest plugin (`modern-di-pytest`) turns any dependency into a + fixture. Dishka ships no pytest *plugin* and documents a hand-written fixtures + recipe instead. +- *Resolution* is sync-only (async finalizers are still supported), and the + built-in scope chain is small, though you can extend it with any `IntEnum`. + Dishka's own docs note that custom scopes are "hardly ever needed," which is the case for modern-di's simpler design. See [Custom scopes](../providers/scopes.md#custom-scopes). -- **All-official, uniformly-maintained integrations** under a single MIT-licensed - project, as part of the broader [modern-python](https://github.com/modern-python) +- Every integration is official and uniformly maintained under a single + MIT-licensed project, part of the broader [modern-python](https://github.com/modern-python) stack. ### vs dependency-injector @@ -86,17 +85,17 @@ modern-di's deliberate differences: `dependency-injector` is the most popular Python DI library, with a mature, Cython-accelerated core and a declarative style using `Provide[...]` markers and `@inject`. It is actively maintained again after an earlier hiatus. modern-di -differs in style (**type-based autowiring instead of explicit markers**) and -adds **nested request scopes** and a **first-party pytest plugin**. If you prefer +differs in style (type-based autowiring instead of explicit markers) and adds +nested request scopes and a first-party pytest plugin. If you prefer explicit declarative wiring and the largest ecosystem, dependency-injector is a -solid, proven choice. Migrating an existing codebase? See the +solid, proven choice. To migrate an existing codebase, see the [migration guide](../migration/from-dependency-injector.md) for the full provider-by-provider mapping. ### vs injector `injector` is a Guice-inspired, mature library with `@inject` and `Module`-based -configuration. Its core has **no async support** and **no nested request scope** +configuration. Its core has no async support and no nested request scope (request scoping comes from third-party FastAPI adapters). modern-di has built-in scopes, official framework integrations, and resource finalization. @@ -105,22 +104,21 @@ built-in scopes, official framework integrations, and resource finalization. For a single web service, native DI is simpler and a container is overkill; see [Do you even need a DI container?](#do-you-even-need-a-di-container) above. Reach for modern-di once you have a second entrypoint, or need typed, scoped, -app-wide singletons with overrides that work everywhere, not just on the HTTP -path. +app-wide singletons with overrides that also apply off the HTTP path. ## that-depends or modern-di? [`that-depends`](https://github.com/modern-python/that-depends) is a sibling project from the same author, in the same [modern-python](https://github.com/modern-python) family. It isn't in the table -above because the choice between the two isn't about features so much as which -generation of the same design you want. +above because choosing between the two is mostly a matter of which generation +of the same design you want. -- **Starting a new project?** Use **modern-di**. It has explicit scopes, no +- For a new project, use modern-di. It has explicit scopes, no global state, a small strictly-typed core, and separate framework adapters; see [Design decisions](design-decisions.md). -- **Already using that-depends?** It remains **actively maintained and - production-proven**, so you don't need to migrate. Move when you want explicit +- If you already use that-depends, it remains actively maintained and + production-proven, so you don't need to migrate. Move when you want explicit scopes or a no-global-state architecture; the [migration guide](../migration/from-that-depends.md) maps every concept across. @@ -129,10 +127,10 @@ generation of the same design you want. | Resolution | async + sync (`AsyncFactory`, `await resolve`) | sync resolution (async finalizers supported) | | Container model | the container class is both schema and runtime | `Group` (schema) and `Container` (runtime) are separate | | Scopes | context-based lifetimes | explicit, enforced scope chain (APP→…→STEP) | -| Global state | resolves directly from the container class | none — you create and pass containers explicitly | +| Global state | resolves directly from the container class | none; you create and pass containers explicitly | | Integrations | bundled | separate adapter packages (install only what you need) | -Choose **that-depends** if you specifically want async resolution +Choose that-depends if you specifically want async resolution (`await container.resolve(...)`; modern-di is sync-only by design and won't add it), want the simplest setup for a single service without an explicit scope chain, or already run it in production with no reason to change. @@ -145,22 +143,22 @@ explicit scopes. ## Where is Singleton? Cross-framework vocabulary modern-di deliberately has no `Singleton` class: "create once and reuse" is spelled via a scope -plus `cache=True` on an ordinary `Factory`. Every arriving user speaks a different framework's -lifetime dialect, so here is how the same six concepts translate: +plus `cache=True` on an ordinary `Factory`. The table translates six lifetime concepts from other +frameworks: | Concept | dependency-injector | dishka | wireup | svcs | FastAPI `Depends` | modern-di | |---|---|---|---|---|---|---| -| Singleton (create once, share) | `providers.Singleton(...)` | `provide(Impl, scope=Scope.APP)` — cached by default within its scope | `@injectable` — default `lifetime="singleton"` | `registry.register_value(Type, value)` at startup | a dependency wrapped in `@lru_cache` | [`Factory(..., scope=Scope.APP, cache=True)`](../providers/factories.md#cached-factories) | -| Transient (fresh instance every time) | `providers.Factory(...)` | `provide(Impl, cache=False)` | `@injectable(lifetime="transient")` | no dedicated provider — call the plain factory directly | `Depends(fn, use_cache=False)` | a plain [`Factory(...)`](../providers/factories.md) with no `cache` | -| Request-scoped | `providers.Resource` + the `Closing` wiring marker | `provide(Impl, scope=Scope.REQUEST)` | `@injectable(lifetime="scoped")` | one instance per `svcs.Container` (built per request) | bare `Depends(fn)` — computed once per request by default | [`Factory(..., scope=Scope.REQUEST, cache=True)`](../providers/scopes.md) | +| Singleton (create once, share) | `providers.Singleton(...)` | `provide(Impl, scope=Scope.APP)`, cached by default within its scope | `@injectable` (default `lifetime="singleton"`) | `registry.register_value(Type, value)` at startup | a dependency wrapped in `@lru_cache` | [`Factory(..., scope=Scope.APP, cache=True)`](../providers/factories.md#cached-factories) | +| Transient (fresh instance every time) | `providers.Factory(...)` | `provide(Impl, cache=False)` | `@injectable(lifetime="transient")` | no dedicated provider; call the plain factory directly | `Depends(fn, use_cache=False)` | a plain [`Factory(...)`](../providers/factories.md) with no `cache` | +| Request-scoped | `providers.Resource` + the `Closing` wiring marker | `provide(Impl, scope=Scope.REQUEST)` | `@injectable(lifetime="scoped")` | one instance per `svcs.Container` (built per request) | bare `Depends(fn)`, computed once per request by default | [`Factory(..., scope=Scope.REQUEST, cache=True)`](../providers/scopes.md) | | Runtime value (request object, etc.) | `providers.Configuration` / `.from_value()` | `from_context(provides=Type, scope=...)` declared, then `context={Type: value}` at scope entry | a typed constructor parameter resolved from the active scope's context | `registry.register_value(Type, value)`, or a per-container local factory | the framework injects `Request`/`WebSocket` directly by type | [`ContextProvider(...)`](../providers/context.md) + `context={...}` | -| Interface binding (concrete → abstract type) | `providers.AbstractFactory` — must be overridden with a concrete `Factory` before use | `alias(source=Impl, provides=Interface)` | `@injectable(as_type=Interface)` | `register_factory(Interface, factory)` — svcs keys by whatever type you register under | n/a — `Depends` is callable-keyed, not type-keyed | [`Alias(Impl, bound_type=Interface)`](../providers/alias.md) | -| Test override | `provider.override(...)`, or `with provider.override(...):` | no dedicated API — build a separate container from mock providers | `with container.override.injectable(Target, new=fake):` | re-call `register_value()`/`register_factory()`; `container.close()` first if already cached | `app.dependency_overrides[dep] = fake` | [`container.override(provider, mock)`](../recipes/testing-overrides.md) | +| Interface binding (concrete → abstract type) | `providers.AbstractFactory`, which must be overridden with a concrete `Factory` before use | `alias(source=Impl, provides=Interface)` | `@injectable(as_type=Interface)` | `register_factory(Interface, factory)`; svcs keys by whatever type you register under | n/a (`Depends` is callable-keyed, not type-keyed) | [`Alias(Impl, bound_type=Interface)`](../providers/alias.md) | +| Test override | `provider.override(...)`, or `with provider.override(...):` | no dedicated API; build a separate container from mock providers | `with container.override.injectable(Target, new=fake):` | re-call `register_value()`/`register_factory()`; `container.close()` first if already cached | `app.dependency_overrides[dep] = fake` | [`container.override(provider, mock)`](../recipes/testing-overrides.md) | ## See also -- [Design decisions](design-decisions.md) — the reasoning behind sync-only +- [Design decisions](design-decisions.md): the reasoning behind sync-only resolution, no global state, a conservative core, and the deliberate [non-goals](design-decisions.md#non-goals) that keep it that way. -- [Performance](performance.md) — comparative benchmarks: how fast resolution is +- [Performance](performance.md): comparative benchmarks of how fast resolution is versus other DI frameworks, and the method behind the numbers. diff --git a/docs/introduction/design-decisions.md b/docs/introduction/design-decisions.md index 49061467..ba57cef0 100644 --- a/docs/introduction/design-decisions.md +++ b/docs/introduction/design-decisions.md @@ -4,7 +4,7 @@ ## 1. Resolution is sync-only; finalizers may be sync or async -Since 2.x, `Container.resolve(...)` and `resolve_provider(...)` are synchronous. There is no `await container.resolve(...)`, no `AsyncFactory`, no `AsyncSingleton`. Async work belongs in the framework's lifespan and per-request hooks; the container holds the already-constructed objects (see [Async resources via lifespan](../recipes/async-lifespan.md)). Resolution being sync does not mean teardown is: finalizers may be sync or async (`close_sync` / `close_async`), so async cleanup is fully supported. +Since 2.x, `Container.resolve(...)` and `resolve_provider(...)` are synchronous. There is no `await container.resolve(...)`, no `AsyncFactory`, no `AsyncSingleton`. Async work belongs in the framework's lifespan and per-request hooks; the container holds the already-constructed objects (see [Async resources via lifespan](../recipes/async-lifespan.md)). Teardown is separate from resolution: finalizers may be sync or async (`close_sync` / `close_async`). Async resolution will not be added. @@ -14,21 +14,21 @@ Cached `Factory` providers use one reentrant lock (`threading.RLock`) per contai ### The thread-safety boundary -- **Cached / singleton creation is locked.** The tree-wide reentrant lock guards the create-and-store step, so two threads racing to resolve the same cached provider get the same single instance. -- **Provider registration is safe.** `ProvidersRegistry` mutations (`register`, `add_providers`) are guarded by the registry's own lock, and iteration snapshots the provider dict (`iter(list(...))`), so registering providers concurrently, or while another thread iterates, will not corrupt the registry or raise "dict changed size during iteration". -- **Registration is a setup phase, not a coordination tool.** The registry is - lock-guarded against corruption, but the supported model is register every - provider *before* serving. Registering a provider while other threads are +- Cached / singleton creation is locked. The tree-wide reentrant lock guards the create-and-store step, so two threads racing to resolve the same cached provider get the same single instance. +- Provider registration is safe. `ProvidersRegistry` mutations (`register`, `add_providers`) are guarded by the registry's own lock, and iteration snapshots the provider dict (`iter(list(...))`), so registering providers concurrently, or while another thread iterates, will not corrupt the registry or raise "dict changed size during iteration". +- Registration belongs to the setup phase. The registry is lock-guarded + against corruption, but the supported model is to register every provider + *before* serving. Registering a provider while other threads are already resolving is timing-dependent by nature: nothing breaks, but whether a given resolve sees the new provider is undefined. -- **`set_context` and overrides are last-write-wins.** Both write into a dict +- `set_context` and overrides are last-write-wins. Both write into a dict with no ordering, queueing, or merge; concurrent writes to the same key keep whichever landed last. Context is per container, so per-request context belongs on a request-local child container. Overrides live in one registry shared by the whole container tree, so an override set on any container is seen by every container in it. Set overrides during setup, never from competing threads. -- **Free-threaded CPython (PEP 703) is supported at `2 - Beta`.** It is tested +- Free-threaded CPython (PEP 703) is supported at `2 - Beta`. It is tested under real multithreading on the `3.14t` build. It is Beta rather than Stable for one specific reason: modern-di relies on object-publication ordering (that a reader observing a stored reference sees fully-initialized fields), and @@ -47,7 +47,7 @@ The codebase is type-checked with `ty` and linted with ruff's full rule set (`se ## 5. Conservative feature set -New features get added only when existing primitives genuinely cannot solve the task. The core has three concrete provider types (`Factory`, `Alias`, `ContextProvider`), plus the `AbstractProvider` base and the pre-built `container_provider` singleton. Most other DI frameworks have two to three times that. This is deliberate: a small, composable core is easier to learn, easier to test, and easier to keep correct. +New features get added only when existing primitives genuinely cannot solve the task. The core has three concrete provider types (`Factory`, `Alias`, `ContextProvider`), plus the `AbstractProvider` base and the pre-built `container_provider` singleton. Most other DI frameworks have two to three times that. The small core is deliberate, because a small, composable core is easier to learn, test, and keep correct. The provider set is closed. `AbstractProvider` is the shared base that appears in signatures, not a hook: resolution compiles a resolver per known provider type, so a subclass of `AbstractProvider` or `Factory` raises `TypeError` at its first resolve. Compose behaviour in a creator function or an `Alias` instead. @@ -63,7 +63,7 @@ Beyond the choices above, these are deliberately out of scope. Naming them here ### Auto-binding / auto-registration -modern-di never registers a provider for a type you did not declare and never infers wiring by scanning your code. Without it, a missing provider is an `ArgumentResolutionError`, reported by `validate()` (run it once at startup or in a test) or raised at resolve. Auto-binding would hide that error until whichever request first exercises the untested path. Register the provider in a `Group`; if the boilerplate is real, a small helper that builds several `Factory` instances from a list of classes is application code, not a framework feature. +modern-di never registers a provider for a type you did not declare and never infers wiring by scanning your code. Without it, a missing provider is an `ArgumentResolutionError`, reported by `validate()` (run it once at startup or in a test) or raised at resolve. Auto-binding would hide that error until whichever request first exercises the untested path. Register the provider in a `Group`; if the boilerplate is real, write a small helper in your application that builds several `Factory` instances from a list of classes. ### In-package framework integrations @@ -95,5 +95,5 @@ No static dependency-graph checker and no type-checker plugin. True compile-time ## See also -- [About DI](about-di.md) — the framework-agnostic introduction. -- [Migration from `that-depends`](../migration/from-that-depends.md) — what these decisions changed compared to the older framework. +- [About DI](about-di.md): the framework-agnostic introduction. +- [Migration from `that-depends`](../migration/from-that-depends.md): what these decisions changed compared to the older framework. diff --git a/docs/introduction/for-fastapi-users.md b/docs/introduction/for-fastapi-users.md index acc09ca0..2a6b6d05 100644 --- a/docs/introduction/for-fastapi-users.md +++ b/docs/introduction/for-fastapi-users.md @@ -11,25 +11,24 @@ translates the `Depends` idioms you already know into their modern-di equivalent | FastAPI `Depends` | modern-di | Notes | |---|---|---| | `Depends(fn)` | `Factory(fn)` | Both auto-wire the callable's parameters; modern-di matches by type annotation instead of by the callable's own parameter defaults. | -| bare `Depends(fn)` (`use_cache=True`, the default) | `Factory(fn, scope=Scope.REQUEST, cache=True)` | FastAPI memoizes a dependency for the rest of the *same request* once it's been called; the REQUEST-scoped cached `Factory` is the equivalent — one shared instance per request container. | -| `Depends(fn, use_cache=False)` | a bare `Factory(fn)` — no `cache` | Without `cache`, a `Factory` builds a fresh instance on every resolve, matching `use_cache=False`. | -| `yield`-based teardown (`def fn(): ...; yield x; ...cleanup...`) | `cache=CacheSettings(finalizer=cleanup_fn)` | modern-di has no generator-creator form (see [Design decisions](design-decisions.md)); teardown is a second, explicit object instead of code after `yield`. `finalizer` may be sync or async — see [Lifecycle](../providers/lifecycle.md). | +| bare `Depends(fn)` (`use_cache=True`, the default) | `Factory(fn, scope=Scope.REQUEST, cache=True)` | FastAPI memoizes a dependency for the rest of the *same request* once it's been called; the REQUEST-scoped cached `Factory` is the equivalent, with one shared instance per request container. | +| `Depends(fn, use_cache=False)` | a bare `Factory(fn)` with no `cache` | Without `cache`, a `Factory` builds a fresh instance on every resolve, matching `use_cache=False`. | +| `yield`-based teardown (`def fn(): ...; yield x; ...cleanup...`) | `cache=CacheSettings(finalizer=cleanup_fn)` | modern-di has no generator-creator form (see [Design decisions](design-decisions.md)); teardown is a second, explicit object instead of code after `yield`. `finalizer` may be sync or async; see [Lifecycle](../providers/lifecycle.md). | | `@lru_cache`-wrapped dependency (process-wide singleton) | `Factory(fn, scope=Scope.APP, cache=True)`, optionally with a `finalizer` | `lru_cache` has no cleanup hook; the APP-scoped cached `Factory` adds one via `CacheSettings(finalizer=...)` if the singleton needs to release anything on shutdown. | -| `app.dependency_overrides[fn] = fake` | `container.override(provider, fake)` | modern-di overrides are keyed by **provider reference**, not by callable, and apply across the whole container tree — see [Testing with overrides](../recipes/testing-overrides.md). Reset with `container.reset_override(provider)`. | +| `app.dependency_overrides[fn] = fake` | `container.override(provider, fake)` | modern-di overrides are keyed by provider reference, not by callable, and apply across the whole container tree; see [Testing with overrides](../recipes/testing-overrides.md). Reset with `container.reset_override(provider)`. | | the manual `try`/`finally` reset FastAPI's docs recommend around `dependency_overrides` | `with container.override(provider, fake) as mock: ...` | Auto-resets on exit instead of a hand-written `finally`. See [Testing with overrides](../recipes/testing-overrides.md) for the full semantics. | ## Two meanings of "scope" -Since FastAPI 0.121.0, `Depends(scope="function" | "request")` controls **when the code after -`yield` runs** relative to the response: `scope="function"` tears down right after your path +Since FastAPI 0.121.0, `Depends(scope="function" | "request")` controls when the code after +`yield` runs relative to the response: `scope="function"` tears down right after your path operation function returns (before the response is sent), and `scope="request"`, the default for a `yield` dependency, tears down after the response has been sent back to the client. It says nothing about how many times the dependency is *constructed*; that's `use_cache`'s job. -modern-di's `Scope` (`APP → SESSION → REQUEST → ACTION → STEP`) answers a different question -entirely: **how long a provider's cached instance lives**, not when its finalizer fires relative to -a response. The two `scope`s share a word but not an axis: FastAPI's is teardown timing, and -modern-di's is how long a cached instance lives. See [Scopes](../providers/scopes.md) for the +modern-di's `Scope` (`APP → SESSION → REQUEST → ACTION → STEP`) answers a different question: +how long a provider's cached instance lives. The two `scope`s share a word, but FastAPI's controls +teardown timing relative to the response and modern-di's controls instance lifetime. See [Scopes](../providers/scopes.md) for the full model. ## Example: request-scoped session with teardown @@ -62,11 +61,11 @@ class Dependencies(Group): ``` This is the modern-di equivalent of a FastAPI `yield`-dependency that hands out one session per -request and closes it afterward. The cleanup is the container's finalizer rather than code after -`yield`, and `Scope.REQUEST` names how long the session lives rather than when it is torn down. +request and closes it afterward. The cleanup runs as the container's finalizer instead of code +after `yield`. ## See also -- [modern-di vs other libraries](comparison.md) — including the cross-framework vocabulary table. -- [FastAPI integration](../integrations/fastapi.md) — `setup_di`, `FromDI`, and websocket scopes. -- [Design decisions](design-decisions.md) — why modern-di has no generator-based teardown. +- [modern-di vs other libraries](comparison.md), including the cross-framework vocabulary table. +- [FastAPI integration](../integrations/fastapi.md): `setup_di`, `FromDI`, and websocket scopes. +- [Design decisions](design-decisions.md): why modern-di has no generator-based teardown. diff --git a/docs/introduction/performance.md b/docs/introduction/performance.md index af91e619..927e8774 100644 --- a/docs/introduction/performance.md +++ b/docs/introduction/performance.md @@ -36,13 +36,13 @@ lookup; that-depends and dependency-injector only by-reference. Each C1-C3 table modern-di variant against the rivals whose API matches it, because a single column would flatter modern-di against half the set. By-type resolution used to add a fixed lookup cost on top of `resolve_provider`: 54-65 ns through 3.2.0, then 21/17/23 ns on C1/C2/C3 once 3.3.0 inlined -`resolve_provider`'s body into `resolve`. As of the template resolver `Container.resolve` memoizes +`resolve_provider`'s body into `resolve`. With the template resolver, `Container.resolve` memoizes type → resolver directly, and the two columns are within run-to-run noise of each other (253 vs 247 ns on C1, 151 vs 147 on C2, 789 vs 798 on C3 in the cells below). -C4 does not split this way: modern-di's C4 body resolves **by reference** throughout, while -dishka and wireup can only be measured by type. That asymmetry cuts **against** modern-di's -C4 ratios, not for them, though with the by-type surcharge now inside noise, levelling it would not +C4 does not split this way: modern-di's C4 body resolves by reference throughout, while +dishka and wireup can only be measured by type. That asymmetry cuts against modern-di's +C4 ratios, though with the by-type surcharge now inside noise, levelling it would not move the dishka ratio below (1.08). The C1-C3 leveling does not apply to C4. C6 does not split either, for the same reason: modern-di's C6 body resolves by reference and there @@ -65,18 +65,18 @@ unreleased when measured) on an Apple M2 (macOS 26.6.2), CPython 3.14.7, median of each side's own median. Rival versions: dishka 1.10.1, dependency-injector 4.49.1, that-depends 4.1.0, wireup 2.12.0. Generated by `just bench-report`. -This publication is on the **same machine and CPython build** as the 3.5.0 one, so this time the +This publication is on the same machine and CPython build as the 3.5.0 one, so this time the absolute cells are comparable with the previous tables as well as the ratios; the [what moved](#what-moved-in-this-publication) section below reads both. > **These numbers are a snapshot of the version named above; a newer release does not update -> them.** The tables are regenerated by hand, so they lag a release rather than ship with one. If +> them.** The tables are regenerated by hand, so they lag behind releases. If > you are running a later modern-di, nothing below has been re-measured against it: run > `just bench-report` yourself (see [Reproduce it yourself](#reproduce-it-yourself)) to measure > the version you have. Each cell is modern-di ÷ rival: below 1.0 (bold) means modern-di is faster, -above 1.0 means slower. Every ratio is **paired within each run**: one run measures both sides +above 1.0 means slower. Every ratio is paired within each run: one run measures both sides under the same machine state, so the published statistic is the median of the per-run ratios, not a ratio of two independently-reduced medians. Pairing gives each ratio a well-defined across-run IQR, published as the `±X.X%` on the cell; read it before treating a near-1.00 cell @@ -134,35 +134,34 @@ _Across-run IQR of each side's own median (5 runs): modern-di ≤1.1%, rivals (**0.53**). The C1 series across publications is 1.08, 1.12, 0.98, 0.98, 0.97, 0.89, 0.91, 0.65, 0.58, now 0.58. that-depends remains faster on C2 warm-singleton (1.78); the suite does not decompose its `resolve_sync` cache-hit path, so no mechanism is asserted for the remaining gap. -- Against the two `exec`-codegen frameworks, the by-type table is now mostly modern-di's. +- Against the two `exec`-codegen frameworks, modern-di leads most of the by-type table. modern-di is faster than `dishka` on C1 (**0.72**) and C2 (**0.62**), and faster than `wireup` on C1 (**0.82**) and C3 (**0.87**). dishka keeps its lead on C3 (1.25), the deepest graph, and wireup keeps C2 (1.47). Since #470 modern-di also generates its resolvers from a source - template, so the frame-count story this page used to tell about dishka's C3 no longer applies: - both sides run one generated frame per node, and the suite does not decompose what dishka does + template, so both sides run one generated frame per node, and the suite does not decompose what dishka does differently on a six-node chain. No mechanism is asserted for that cell. -- The by-type surcharge is gone. `Container.resolve` memoizes type → resolver directly, so +- By-type resolution carries no surcharge. `Container.resolve` memoizes type → resolver directly, so the by-type and by-reference cells differ by 4-6 ns on C1 and C2 and swap sign on C3, all - inside the run-to-run spread. The two tables now measure the same resolve; only the rival set + inside the run-to-run spread. The two tables measure the same resolve; only the rival set differs. - On C6 (per-request context) modern-di is faster than `dependency-injector` (**0.38**) and `that-depends` (**0.48**), slower than `dishka` (1.15), and level with `wireup` (1.02 ±2.4%, a cell whose spread straddles 1.00). The direction - matches the by-type table, but the cells are **not** on one basis: each framework supplies the + matches the by-type table, but the cells are not on one basis: each framework supplies the request value through its own idiom, and two of those are structural analogs rather than equivalents (see the caveat below). No mechanism is asserted for the gaps; the suite does not decompose any framework's context lookup. - On C4 (request lifecycle), the corrected batching does not *remove* the ~35 µs asyncio floor, it amortizes it. The guard tier's `test_g7c_event_loop_floor_control` times the same batch - shape with an empty body and puts the residual at **~0.3 µs per request** still inside every + shape with an empty body and puts the residual at ~0.3 µs per request still inside every C4 cell (~13% of modern-di's C4 figure), shared identically by all five frameworks. With the - floor amortized, dishka is measurably **faster** than modern-di here (1.08); modern-di remains + floor amortized, dishka is measurably faster than modern-di here (1.08); modern-di remains far faster than that-depends, dependency-injector, and wireup on this scenario. ### What moved in this publication -**The per-request cells, not the resolve cells.** The three changes since 3.5.0 sit on paths -C1-C3 never take: #540 inlined the cross-scope hop into the generated resolver, #541 shares one +The changes in this publication moved the per-request cells and left the resolve cells alone. +The three changes since 3.5.0 sit on paths C1-C3 never take: #540 inlined the cross-scope hop into the generated resolver, #541 shares one lock per container tree instead of allocating an `RLock` per child, and #542 stops allocating a coroutine per finalizer-less cached item in `close_async`. A C1-C3 body resolves inside one warm container, so every one of those cells is within its own run-to-run spread of the 3.5.0 @@ -189,7 +188,7 @@ nothing and closes synchronously. C4's own cell carries a ±4.8% spread this time, so read its 5% as the sign and the G7 figure as the size. The wireup C6 cell has crossed from 1.16 to 1.02 ±2.4%, which is level, not a lead. -**The C4 gain recorded at 3.1.1 was a library fix**, and it stands: every `Container` used to +The C4 gain recorded at 3.1.1 was a library fix, and it stands: every `Container` used to store itself in its own `_scope_map`, making it a reference cycle that reference counting could never free, so a request-scoped application handed the garbage collector work at its request rate. Seeding the map from the parent instead removed the cycle. Measured on the C4 benchmark at the @@ -197,33 +196,33 @@ time, that cut the median from 232.7 µs to 194.8 µs per 100-request batch and deviation from 123.0 µs to 7.9 µs. The tail this scenario used to carry was the collector reclaiming containers, and it is gone. -**C4 is a batched request lifecycle.** modern-di resolves the connection synchronously while +C4 is a batched request lifecycle. modern-di resolves the connection synchronously while finalizing it asynchronously; the other four force an awaited resolve once the finalizer is async. C4 therefore measures the whole request lifecycle (enter scope → resolve → -async-finalize), not an isolated resolve. It is timed as a **batch of 100 cycles per event-loop -entry**, because a single `run_until_complete` entry costs ~35 µs on any body: timing one +async-finalize), not an isolated resolve. It is timed as a batch of 100 cycles per event-loop +entry, because a single `run_until_complete` entry costs ~35 µs on any body: timing one request per entry made every framework's cell ~93% asyncio floor. The published figure is the -batch divided by 100. C1–C3 are synchronous resolves for every framework. +batch divided by 100. C1-C3 are synchronous resolves for every framework. -**C6 is sync for all five, but not one idiom.** Each framework supplies the per-request value its +C6 is sync for all five frameworks, though they use different idioms. Each framework supplies the per-request value its own way: modern-di seeds a child container's context and resolves by reference; dishka uses `from_context`; wireup requires the runtime type registered as a scoped injectable behind a raising placeholder factory; that-depends supplies it through `container_context(global_context=)`; and -dependency-injector injects **by reference** via `providers.Dependency` + `.override()`, a +dependency-injector injects by reference via `providers.Dependency` + `.override()`, a structural analog rather than an equivalent. modern-di's timed body builds the child, resolves, and closes -it. It calls no `open()`: a freshly built child is already open as of 3.1, so timing one would +it. It calls no `open()`: a freshly built child is already open, so timing one would charge modern-di a redundant lock acquire (81 ns, ~6% of the cell) with no counterpart in any rival's body. It does close, because all four rivals exit their scope inside the timed body; that teardown is ~110 ns, and omitting it would have flattered modern-di by more than the `open()` would have cost it. -**Thread-safety configuration differs, at each framework's default.** dishka's `make_container` -defaults to `lock_factory=`, so every `get()` behind its C1–C3 cells +Each framework runs at its default thread-safety configuration, and the defaults differ. dishka's `make_container` +defaults to `lock_factory=`, so every `get()` behind its C1-C3 cells acquires a lock; modern-di's cached read is lock-free by design (see [Design decisions](design-decisions.md#the-thread-safety-boundary)). Both run at their defaults, which is the comparison a user gets out of the box. A dishka user targeting single-threaded work can pass `lock_factory=None`, -and that would move dishka's C1–C3 cells. The axis is disclosed rather than normalized away. +and that would move dishka's C1-C3 cells. This page discloses the difference and leaves it in the numbers. ## Why the results look this way @@ -237,14 +236,14 @@ history below is in order. 3.1.0 removed one more frame from the top of every resolve: `resolve_provider` now opens with an inline `closed` check instead of an unconditional method call, and `build_child_container` carries no such check at all. Measured on the guard -suite against 3.0.0 on one machine, that is worth roughly 5–11% on C1- and +suite against 3.0.0 on one machine, that is worth roughly 5-11% on C1- and C2-shaped resolves and on child construction; the deeper scenarios, which run through compiled resolvers where the check was already inline, did not move. 3.1.1 removed a reference cycle rather than a frame: every `Container` stored itself in its own `_scope_map`, so no container could be freed by reference counting and each one waited for the garbage collector. Seeding the map from the parent removed it. Resolution is untouched (the -C1–C3 cells did not move), but the request lifecycle did: C4's median fell from 232.7 µs to +C1-C3 cells did not move), but the request lifecycle did: C4's median fell from 232.7 µs to 194.8 µs per 100-request batch and its standard deviation from 123.0 µs to 7.9 µs, because the collector no longer has to reclaim containers that refcounting now frees. @@ -334,5 +333,5 @@ across machines than the absolute times. ## See also -- [Comparison](comparison.md) — how modern-di compares on features. -- [Design decisions](design-decisions.md) — why resolution is sync-only. +- [Comparison](comparison.md): how modern-di compares on features. +- [Design decisions](design-decisions.md): why resolution is sync-only. diff --git a/docs/introduction/resolving.md b/docs/introduction/resolving.md index 03929010..005f905d 100644 --- a/docs/introduction/resolving.md +++ b/docs/introduction/resolving.md @@ -2,8 +2,8 @@ `modern-di` exposes two ways to resolve a dependency: -- **By type**: `container.resolve(SomeType)`. The resolver finds the provider whose `bound_type` matches `SomeType`. This is what handlers and creator signatures normally use. -- **By provider reference**: `container.resolve_provider(Dependencies.some_provider)`. Resolves a specific provider directly, skipping the type lookup. Useful in tests and when two providers produce the same type. +- By type, with `container.resolve(SomeType)`. The resolver finds the provider whose `bound_type` matches `SomeType`. This is what handlers and creator signatures normally use. +- By provider reference, with `container.resolve_provider(Dependencies.some_provider)`. This resolves a specific provider directly and skips the type lookup, which helps in tests and when two providers produce the same type. In practice, prefer resolution by type: it lets the same code work whether you swap implementations via subclassing, `Alias`, or `override`. Reach for `resolve_provider` only when type-based resolution would be ambiguous. @@ -48,6 +48,6 @@ For union-typed parameters (`dep: A | B`), the resolver picks the *first* type i ## See also -- [Scopes](../providers/scopes.md) — the scope chain governs which container resolves which provider. -- [Lifecycle](../providers/lifecycle.md) — `container.validate()` catches resolution problems at startup. -- [Factories: `bound_type`](../providers/factories.md) — how the type lookup key is set, and how to opt out. +- [Scopes](../providers/scopes.md): the scope chain governs which container resolves which provider. +- [Lifecycle](../providers/lifecycle.md): `container.validate()` catches resolution problems at startup. +- [Factories: `bound_type`](../providers/factories.md): how the type lookup key is set, and how to opt out. diff --git a/docs/migration/from-dependency-injector.md b/docs/migration/from-dependency-injector.md index 2c86056f..00dc35cc 100644 --- a/docs/migration/from-dependency-injector.md +++ b/docs/migration/from-dependency-injector.md @@ -61,30 +61,30 @@ Use this table as the index for the rest of the guide. Every provider class docu | `dependency-injector` | `modern-di` replacement | Where to look | |---|---|---| | `Factory` | `providers.Factory(...)` | [§4](#4-migrate-the-dependency-graph) | -| `Callable` | `providers.Factory(the_callable)` — `Factory`'s creator can be any callable, not just a class | [§4](#4-migrate-the-dependency-graph) | +| `Callable` | `providers.Factory(the_callable)`; `Factory`'s creator can be any callable, not just a class | [§4](#4-migrate-the-dependency-graph) | | `Singleton` | `providers.Factory(..., cache=True)` | [§4](#4-migrate-the-dependency-graph) | -| `ThreadSafeSingleton` | `providers.Factory(..., cache=True)` — `modern-di`'s cache is lock-guarded by default (`use_lock=True` on the container) | [§4](#4-migrate-the-dependency-graph) | -| `ThreadLocalSingleton` | No direct equivalent — see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | -| `Resource` (plain-function initializer — their docs' most common form; no shutdown step) | `providers.Factory(..., cache=True)` — same as `Singleton`; add a finalizer only when there is teardown | [§4](#4-migrate-the-dependency-graph) | +| `ThreadSafeSingleton` | `providers.Factory(..., cache=True)`; `modern-di`'s cache is lock-guarded by default (`use_lock=True` on the container) | [§4](#4-migrate-the-dependency-graph) | +| `ThreadLocalSingleton` | No direct equivalent; see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | +| `Resource` (plain-function initializer, their docs' most common form; no shutdown step) | `providers.Factory(..., cache=True)`, same as `Singleton`; add a finalizer only when there is teardown | [§4](#4-migrate-the-dependency-graph) | | `Resource` (generator / context-manager initializer) | `providers.Factory(..., cache=CacheSettings(finalizer=...))` | [§4](#4-migrate-the-dependency-graph) | | `Resource` (async initializer) | Lifespan + `ContextProvider` (or sync creator + async finalizer) | [§4](#4-migrate-the-dependency-graph) | | `ContextLocalResource` | `providers.Factory(..., scope=Scope.REQUEST, cache=CacheSettings(finalizer=...))` resolved from a per-request child container | [§4](#4-migrate-the-dependency-graph) | -| `Coroutine` | No direct equivalent — resolution is sync-only; do the `await` in the lifespan and inject the result, same as an async `Resource` | [§4](#4-migrate-the-dependency-graph) | +| `Coroutine` | No direct equivalent. Resolution is sync-only, so do the `await` in the lifespan and inject the result, same as an async `Resource` | [§4](#4-migrate-the-dependency-graph) | | `Object` | `providers.Factory` with a creator that returns the value | [§4](#4-migrate-the-dependency-graph) | | `List` | `providers.Factory` with a creator that returns a list | [§4](#4-migrate-the-dependency-graph) | | `Dict` | `providers.Factory` with a creator that returns a dict | [§4](#4-migrate-the-dependency-graph) | | `Dependency` | `providers.ContextProvider(...)` | [§4](#4-migrate-the-dependency-graph) | -| `AbstractFactory` | `providers.Alias(..., bound_type=...)` — pick the concrete implementation at declaration time instead of via `.override()` before first use | [§4](#4-migrate-the-dependency-graph) | -| `Configuration` | A plain settings object registered as a provider — no config subsystem (`from_yaml`/`from_env`/etc.) | [§5](#5-configuration) | -| `Selector` | No direct equivalent — see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | -| `Aggregate` / `FactoryAggregate` | No direct equivalent — see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | -| `.provided` (attribute / item / method-call access on a provider) | No direct equivalent — see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | +| `AbstractFactory` | `providers.Alias(..., bound_type=...)`; pick the concrete implementation at declaration time instead of via `.override()` before first use | [§4](#4-migrate-the-dependency-graph) | +| `Configuration` | A plain settings object registered as a provider; there is no config subsystem (`from_yaml`/`from_env`/etc.) | [§5](#5-configuration) | +| `Selector` | No direct equivalent; see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | +| `Aggregate` / `FactoryAggregate` | No direct equivalent; see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | +| `.provided` (attribute / item / method-call access on a provider) | No direct equivalent; see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | | `@inject` + `Provide[...]` + `container.wire(modules=[...])` (web) | `FromDI(T)` from the framework integration | [§6](#6-wiring-replacement), [§8](#8-framework-integration-and-routes) | | `@inject` + `Provide[...]` + `container.wire(modules=[...])` (non-web) | Explicit `container.resolve(T)` | [§6](#6-wiring-replacement) | | `DeclarativeContainer` | `Group` (schema) + `Container(groups=[...])` (runtime), checked with `.validate()` | [§2](#2-key-conceptual-shifts) | -| `container.init_resources()` | Lazy initialization — no equivalent needed | [§9](#9-testing-and-overrides) | +| `container.init_resources()` | Lazy initialization; no equivalent needed | [§9](#9-testing-and-overrides) | | `container.shutdown_resources()` / `provider.shutdown()` | `container.close_sync()` / `await container.close_async()` | [§9](#9-testing-and-overrides) | -| `provider.override(...)` / `with provider.override(...):` | `container.override(provider, mock)` / `with container.override(provider, mock):` — see [§9](#9-testing-and-overrides) | [§9](#9-testing-and-overrides) | +| `provider.override(...)` / `with provider.override(...):` | `container.override(provider, mock)` / `with container.override(provider, mock):`; see [§9](#9-testing-and-overrides) | [§9](#9-testing-and-overrides) | | `provider.reset_override()` / `provider.reset_last_overriding()` | `container.reset_override(provider)` | [§9](#9-testing-and-overrides) | ## 4. Migrate the dependency graph @@ -93,9 +93,9 @@ Use this table as the index for the rest of the guide. Every provider class docu 2. Add an explicit `scope=` to each provider (defaults to `Scope.APP`). 3. Create the runtime container with `Container(groups=[MyGroup])`, then call `container.validate()` for whole-graph checks. In `modern-di`, `Group` is a schema only; you cannot resolve from it directly, unlike a `DeclarativeContainer` instance. -**`Singleton` / `ThreadSafeSingleton`** → `providers.Factory(SomeClass, cache=True)`. There is no separate thread-safe class, since `modern-di`'s cache is lock-guarded by default. See [Cached factories](../providers/factories.md#cached-factories). +Replace `Singleton` and `ThreadSafeSingleton` with `providers.Factory(SomeClass, cache=True)`. There is no separate thread-safe class, since `modern-di`'s cache is lock-guarded by default. See [Cached factories](../providers/factories.md#cached-factories). -**`Resource`** → cached `Factory`, with or without a `finalizer` depending on the initializer form. Their docs call the plain-function initializer "the most common way to specify resource initialization", and a plain-function `Resource` has no shutdown step, so it maps to exactly what `Singleton` maps to: +Replace `Resource` with a cached `Factory`, with or without a `finalizer` depending on the initializer form. Their docs call the plain-function initializer "the most common way to specify resource initialization", and a plain-function `Resource` has no shutdown step, so it maps to exactly what `Singleton` maps to: ```python # dependency-injector — plain-function initializer, no shutdown @@ -129,7 +129,7 @@ thread_pool = providers.Factory( ) ``` -**`ContextLocalResource`** → `REQUEST`-scoped cached `Factory` with a `finalizer`. `dependency-injector`'s `ContextLocalResource` uses `contextvars` to give each execution context (in practice: each async request) its own instance of a `Resource`, cleaned up when the context ends. `modern-di` expresses the same lifetime explicitly: declare the provider at `Scope.REQUEST` and resolve it from a per-request child container. The framework integrations build that child container for you ([§8](#8-framework-integration-and-routes)), and closing it runs the finalizer: +Replace `ContextLocalResource` with a `REQUEST`-scoped cached `Factory` that has a `finalizer`. `dependency-injector`'s `ContextLocalResource` uses `contextvars` to give each execution context (in practice: each async request) its own instance of a `Resource`, cleaned up when the context ends. `modern-di` expresses the same lifetime explicitly: declare the provider at `Scope.REQUEST` and resolve it from a per-request child container. The framework integrations build that child container for you ([§8](#8-framework-integration-and-routes)), and closing it runs the finalizer: ```python # dependency-injector @@ -143,7 +143,7 @@ db_session = providers.Factory( ) ``` -**`Callable`** → a plain `Factory` whose creator is the callable. `modern-di` has no separate "wraps a function vs. wraps a class" distinction; `Factory.creator` accepts any `Callable[..., T]`. Note the call-time argument this example passes (`container.password_hasher("super secret")`) has no `modern-di` equivalent; see the note below: +Replace `Callable` with a plain `Factory` whose creator is the callable. `modern-di` has no separate "wraps a function vs. wraps a class" distinction; `Factory.creator` accepts any `Callable[..., T]`. Note the call-time argument this example passes (`container.password_hasher("super secret")`) has no `modern-di` equivalent; see the note below: ```python # dependency-injector @@ -157,9 +157,9 @@ password_hasher = providers.Factory( ) ``` -> **Providers are not partially-applied callables in `modern-di`.** In `dependency-injector`, every provider instance is itself callable, and calling it with extra positional/keyword arguments merges them with the declared ones for that one call (`container.some_factory(extra_arg)`). `modern-di`'s `Factory` has no equivalent: `resolve()`/`resolve_provider()` take no arguments, and every constructor argument must be resolvable (by type, by `kwargs`, or by default) at declaration time. If a value genuinely varies per call site, resolve a plain function or make it a `ContextProvider`/`Scope.REQUEST` dependency instead of trying to pass it at the call site. +> Providers are not partially-applied callables in `modern-di`. In `dependency-injector`, every provider instance is itself callable, and calling it with extra positional/keyword arguments merges them with the declared ones for that one call (`container.some_factory(extra_arg)`). `modern-di`'s `Factory` has no equivalent: `resolve()`/`resolve_provider()` take no arguments, and every constructor argument must be resolvable (by type, by `kwargs`, or by default) at declaration time. If a value genuinely varies per call site, resolve a plain function or make it a `ContextProvider`/`Scope.REQUEST` dependency instead of trying to pass it at the call site. -**`Object`** → `Factory` whose creator returns the value. Define a small typed function (lambdas have no return annotation, which prevents resolution by type): +Replace `Object` with a `Factory` whose creator returns the value. Define a small typed function (lambdas have no return annotation, which prevents resolution by type): ```python # dependency-injector @@ -176,7 +176,7 @@ api_key = providers.Factory(_api_key, cache=True) If you only need the value passed into one downstream provider, skip the wrapper and put it directly in that provider's `kwargs`. -**`List` / `Dict`** → `Factory` with a creator that builds the collection: +Replace `List` and `Dict` with a `Factory` whose creator builds the collection: ```python # dependency-injector @@ -192,7 +192,7 @@ def build_modules() -> list[Module]: modules = providers.Factory(build_modules) ``` -**`Dependency`** → `ContextProvider`. Both are a typed placeholder filled in at runtime rather than constructed by a factory: +Replace `Dependency` with `ContextProvider`. Both are a typed placeholder filled in at runtime rather than constructed by a factory: ```python # dependency-injector @@ -204,7 +204,7 @@ database = providers.ContextProvider(DbAdapter, scope=Scope.APP) # container = Container(groups=[AppGroup], context={DbAdapter: SqliteDbAdapter()}) ``` -**`AbstractFactory`** → `Alias`. `dependency-injector`'s `AbstractFactory` starts unbound and must be `.override()`-ed with a concrete `Factory` before first use; `modern-di` instead registers the concrete provider directly and re-exports it under the abstract type at declaration time. There is no override step, and `validate()` catches a missing binding before the first resolve: +Replace `AbstractFactory` with `Alias`. `dependency-injector`'s `AbstractFactory` starts unbound and must be `.override()`-ed with a concrete `Factory` before first use; `modern-di` instead registers the concrete provider directly and re-exports it under the abstract type at declaration time. There is no override step, and `validate()` catches a missing binding before the first resolve: ```python # dependency-injector @@ -218,7 +218,7 @@ cache_client = providers.Alias(RedisCacheClient, bound_type=AbstractCacheClient) ## 5. Configuration -`dependency-injector`'s `Configuration` provider is a subsystem: `providers.Configuration()` plus `.from_yaml()` / `.from_json()` / `.from_ini()` / `.from_env()` / `.from_pydantic()` / `.from_dict()` / `.from_value()` loaders, environment-variable interpolation (`${VAR:default}`), and a "use first, define later" declaration order. `modern-di` deliberately has no equivalent subsystem. This is a design decision, not a gap: load your settings with whatever library you already use (`pydantic-settings`, `environ-config`, plain `os.environ`, ...) into a regular object, then register that object as an ordinary provider: +`dependency-injector`'s `Configuration` provider is a subsystem: `providers.Configuration()` plus `.from_yaml()` / `.from_json()` / `.from_ini()` / `.from_env()` / `.from_pydantic()` / `.from_dict()` / `.from_value()` loaders, environment-variable interpolation (`${VAR:default}`), and a "use first, define later" declaration order. `modern-di` deliberately has no equivalent subsystem. Load your settings with whatever library you already use (`pydantic-settings`, `environ-config`, plain `os.environ`, ...) into a regular object, then register that object as an ordinary provider: ```python class Settings: @@ -292,7 +292,7 @@ Replace `container.wire(modules=[...])` (plus any per-framework glue such as `co ### Overrides -Overrides are keyed by **provider reference**, not attribute name, same idea as `dependency-injector` but through the container rather than the provider object: +Overrides are keyed by provider reference, not attribute name, same idea as `dependency-injector` but through the container rather than the provider object: ```python # dependency-injector @@ -319,7 +319,7 @@ See [Testing with overrides](../recipes/testing-overrides.md) for tree-wide shar ### Lifecycle - There is no `init_resources()` equivalent: providers initialize lazily on first resolve; see [Lazy initialization](../providers/lifecycle.md#lazy-initialization) for eager-warmup at startup. -- `shutdown_resources()` / `provider.shutdown()` → `container.close_sync()` / `await container.close_async()`, also usable as (async) context managers, with finalizers running in reverse order on exit. +- `shutdown_resources()` / `provider.shutdown()` become `container.close_sync()` / `await container.close_async()`, also usable as (async) context managers, with finalizers running in reverse order on exit. ### Pytest @@ -331,8 +331,8 @@ See [Testing with overrides](../recipes/testing-overrides.md) for tree-wide shar |---|---|---| | Circular dependency | No cycle detection; a circular provider graph raises a bare `RecursionError` from Cython-level `deepcopy`, with no cycle path ([issue #811](https://github.com/ets-labs/python-dependency-injector/issues/811)) | `validate()` reports every cycle up front as `CircularDependencyError` with an arrow-chain `cycle_path`; even without `validate()`, a runtime cycle hit is caught and re-raised as `CircularDependencyError` (not a bare `RecursionError`) | | Unwired injection point | Silent: an un-wired function keeps the raw `Provide` marker as its default, surfacing as `AttributeError: 'Provide' object has no attribute ...` far from the actual mistake ([#658](https://github.com/ets-labs/python-dependency-injector/issues/658), [#521](https://github.com/ets-labs/python-dependency-injector/issues/521)) | No marker subsystem to leave unwired: a missing dependency fails at declaration time (`UnsupportedCreatorParameterError`) or resolve time (`ProviderNotRegisteredError`, `ArgumentResolutionError`) | -| Whole-graph validation | None — errors surface one at a time, on first resolve, wherever the graph happens to break | `container.validate()` walks the entire graph and raises one `ValidationFailedError` aggregating *every* wiring bug (cycles, inverted scopes, missing dependencies) at once | -| Resolve by type | [No type-based resolution API](https://python-dependency-injector.ets-labs.org/wiring.html) — every call site needs an explicit `Provide[Container.x]` marker | `container.resolve(SomeType)` resolves directly from a type annotation; unregistered types get closest-match ("did you mean") suggestions | +| Whole-graph validation | None: errors surface one at a time, on first resolve, wherever the graph happens to break | `container.validate()` walks the entire graph and raises one `ValidationFailedError` aggregating *every* wiring bug (cycles, inverted scopes, missing dependencies) at once | +| Resolve by type | [No type-based resolution API](https://python-dependency-injector.ets-labs.org/wiring.html); every call site needs an explicit `Provide[Container.x]` marker | `container.resolve(SomeType)` resolves directly from a type annotation; unregistered types get closest-match ("did you mean") suggestions | Call `container.validate()` explicitly during migration. The cycle row above is considerably noisier without it, since the error surfaces deep inside an already near-exhausted call stack instead of a clean, aggregated report. @@ -340,15 +340,15 @@ Call `container.validate()` explicitly during migration. The cycle row above is A handful of `dependency-injector` features have no direct port. Workarounds: -- **`ThreadLocalSingleton`**: register an uncached `Factory` whose creator reads the object from a module-level `threading.local()` and creates and stores it there on a thread's first call. A cached `Factory` can't do this, because it caches one object for the whole container. -- **`Selector`**: write a creator function that takes whatever the selector depended on and returns the chosen object. If the choice is static (e.g. one implementation per environment), `Alias` may be cleaner. -- **`Aggregate` / `FactoryAggregate`**: resolve each candidate provider individually (by type or by reference) and dispatch on the key yourself in a small creator function, rather than injecting the whole aggregate object. -- **`.provided` (attribute / item / method-call access on a provider, e.g. `service.provided.value`)**: resolve the parent inside the consuming creator and access the attribute, item, or method result there, or expose a dedicated `Factory` whose creator returns just that piece. -- **`@inject` + `Provide[T]()` for non-framework functions**: `modern-di` has no general-purpose injection decorator. Call `container.resolve(T)` explicitly at the call site, or expose the function through a framework integration and use `FromDI(T)`. -- **Call-time provider arguments** (`container.some_factory(extra_arg)` merging extra args into that one call): `modern-di` providers resolve with no arguments; move the varying value into `kwargs=` if it is static, or into a `ContextProvider`/deeper-scoped dependency if it genuinely varies per call site. +- For `ThreadLocalSingleton`, register an uncached `Factory` whose creator reads the object from a module-level `threading.local()` and creates and stores it there on a thread's first call. A cached `Factory` can't do this, because it caches one object for the whole container. +- For `Selector`, write a creator function that takes whatever the selector depended on and returns the chosen object. If the choice is static (e.g. one implementation per environment), `Alias` may be cleaner. +- For `Aggregate` / `FactoryAggregate`, resolve each candidate provider individually (by type or by reference) and dispatch on the key yourself in a small creator function, rather than injecting the whole aggregate object. +- For `.provided` (attribute / item / method-call access on a provider, e.g. `service.provided.value`), resolve the parent inside the consuming creator and access the attribute, item, or method result there, or expose a dedicated `Factory` whose creator returns just that piece. +- `modern-di` has no general-purpose injection decorator to replace `@inject` + `Provide[T]()` on non-framework functions. Call `container.resolve(T)` explicitly at the call site, or expose the function through a framework integration and use `FromDI(T)`. +- Call-time provider arguments (`container.some_factory(extra_arg)` merging extra args into that one call) have no equivalent, because `modern-di` providers resolve with no arguments. Move the varying value into `kwargs=` if it is static, or into a `ContextProvider`/deeper-scoped dependency if it genuinely varies per call site. ## More -- [modern-di vs dependency-injector](../introduction/comparison.md#vs-dependency-injector) — the short, non-migration-focused comparison. -- Litestar usage example — [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template) -- FastAPI usage example — [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template) +- [modern-di vs dependency-injector](../introduction/comparison.md#vs-dependency-injector): the short, non-migration-focused comparison. +- Litestar usage example: [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template) +- FastAPI usage example: [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template) diff --git a/docs/migration/from-that-depends.md b/docs/migration/from-that-depends.md index 24d3f45c..f094a692 100644 --- a/docs/migration/from-that-depends.md +++ b/docs/migration/from-that-depends.md @@ -70,9 +70,9 @@ Use this table as the index for the rest of the guide. | `Object` | `providers.Factory` with a creator that returns the value | [§4](#4-migrate-the-dependency-graph) | | `List` | `providers.Factory` with a creator that returns a list | [§4](#4-migrate-the-dependency-graph) | | `Dict` | `providers.Factory` with a creator that returns a dict | [§4](#4-migrate-the-dependency-graph) | -| `Selector` | No direct equivalent — see [§9](#9-no-direct-equivalent) | -| `AttrGetter` (`provider.attr`) | No direct equivalent — see [§9](#9-no-direct-equivalent) | -| `ThreadLocalSingleton` | No direct equivalent — see [§9](#9-no-direct-equivalent) | +| `Selector` | No direct equivalent; see [§9](#9-no-direct-equivalent) | +| `AttrGetter` (`provider.attr`) | No direct equivalent; see [§9](#9-no-direct-equivalent) | +| `ThreadLocalSingleton` | No direct equivalent; see [§9](#9-no-direct-equivalent) | | `State` | `ContextProvider` + `set_context` | [§5](#5-context-resources-and-request-scope) | | `Provider.bind(Type)` | `providers.Alias(..., bound_type=...)` | [§4](#4-migrate-the-dependency-graph) | | `@inject` + `Provide[T]()` (web) | `FromDI(T)` from the framework integration | [§8](#8-framework-integration-and-routes) | @@ -80,7 +80,7 @@ Use this table as the index for the rest of the guide. | `container_context()` | `container.build_child_container(scope=..., context=...)` | [§5](#5-context-resources-and-request-scope) | | `DIContextMiddleware` | `setup_di(app, container)` / `ModernDIPlugin(container)` | [§8](#8-framework-integration-and-routes) | | `fetch_context_item` / `_by_type` | `ContextProvider(T)` | [§5](#5-context-resources-and-request-scope) | -| `init_resources()` | Lazy initialization — no equivalent needed | [§7](#7-lifecycle-and-testing) | +| `init_resources()` | Lazy initialization; no equivalent needed | [§7](#7-lifecycle-and-testing) | | `tear_down()` / `tear_down_sync()` | `await container.close_async()` / `container.close_sync()` | [§7](#7-lifecycle-and-testing) | | `container.override_providers_sync({...})` | `container.override(provider, mock)` | [§7](#7-lifecycle-and-testing) | | `provider.override_sync(mock)` | `container.override(provider, mock)` | [§7](#7-lifecycle-and-testing) | @@ -149,7 +149,7 @@ When a provider is passed inside `kwargs={...}`, `modern-di` detects it and reso ### Per-provider replacements -**`Singleton`** → cached `Factory` of `APP` scope: +Replace `Singleton` with a cached `Factory` of `APP` scope: ```python # that-depends @@ -162,9 +162,9 @@ some_singleton = providers.Factory( ) ``` -**`Resource`** (sync generator or context manager) → cached `Factory` with a `finalizer`, splitting the generator into a creator and a finalizer function. See `database_engine` in the worked example above. +Replace a sync `Resource` (sync generator or context manager) with a cached `Factory` that has a `finalizer`, splitting the generator into a creator and a finalizer function. See `database_engine` in the worked example above. -**`Object`** → `Factory` whose creator returns the value. Define a small typed function (lambdas have no return annotation, which prevents resolution by type): +Replace `Object` with a `Factory` whose creator returns the value. Define a small typed function (lambdas have no return annotation, which prevents resolution by type): ```python # that-depends @@ -181,7 +181,7 @@ api_key = providers.Factory(_api_key, cache=True) If you only need the value passed into one downstream provider, skip the wrapper and put it directly in that provider's `kwargs`. -**`List` / `Dict`** → `Factory` with a creator that builds the collection: +Replace `List` and `Dict` with a `Factory` whose creator builds the collection: ```python # that-depends @@ -194,7 +194,7 @@ def build_list(a: SomeType1, b: SomeType2) -> list[object]: some_list = providers.Factory(build_list) ``` -**`Provider.bind(Type)`** → `Alias`. Useful when you want an abstract type (`Protocol`, ABC) to resolve to a concrete registered provider: +Replace `Provider.bind(Type)` with `Alias`. This is useful when you want an abstract type (`Protocol`, ABC) to resolve to a concrete registered provider: ```python # that-depends @@ -238,11 +238,11 @@ with container.build_child_container( repo = request_container.resolve(TenantScopedRepository) ``` -`ContextProvider` returns the value registered for that type on the container **at the provider's own scope**. There is no global lookup like `fetch_context_item`, and [context never propagates between containers](../providers/context.md#context-propagation). For a REQUEST-scoped `ContextProvider`, pass the value to the request container via `build_child_container(context={TenantId: tenant})` or `request_container.set_context(TenantId, tenant)`. +`ContextProvider` returns the value registered for that type on the container at the provider's own scope. There is no global lookup like `fetch_context_item`, and [context never propagates between containers](../providers/context.md#context-propagation). For a REQUEST-scoped `ContextProvider`, pass the value to the request container via `build_child_container(context={TenantId: tenant})` or `request_container.set_context(TenantId, tenant)`. ## 6. Async resources -`modern-di` resolves synchronously. There is no `AsyncFactory`, no `AsyncSingleton`, and no `await container.resolve(...)`. The pattern is **async lives in the lifespan, not in the resolve path.** Three cases cover almost everything. +`modern-di` resolves synchronously. There is no `AsyncFactory`, no `AsyncSingleton`, and no `await container.resolve(...)`. Async work lives in the lifespan, not in the resolve path. Three cases cover almost everything. ### Sync creator, async finalizer @@ -273,7 +273,7 @@ engine = providers.Factory( `ContextProvider` via `set_context` so downstream factories can depend on its type. See [Async resources via lifespan](../recipes/async-lifespan.md) for the full pattern, the pitfalls (setting context before yielding, combining a hand-written lifespan with an integration's -`setup_di`), and which resources construct synchronously enough to skip this and just use a +`setup_di`), and which resources construct synchronously enough to skip this and use a sync creator with an async finalizer instead. ### Per-request async construction @@ -285,11 +285,11 @@ If a per-request resource genuinely needs `await` at construction time, the simp ### Lifecycle - There is no `init_resources()` equivalent: providers initialize lazily on first resolve; see [Lazy initialization](../providers/lifecycle.md#lazy-initialization) for eager-warmup at startup. -- `tear_down()` / `tear_down_sync()` → `await container.close_async()` / `container.close_sync()`, also usable as (async) context managers. The framework integrations call `close_async()` automatically at app shutdown. +- `tear_down()` / `tear_down_sync()` become `await container.close_async()` / `container.close_sync()`, also usable as (async) context managers. The framework integrations call `close_async()` automatically at app shutdown. ### Overrides -Overrides are keyed by **provider reference**, not by name: +Overrides are keyed by provider reference, not by name: ```python # that-depends @@ -315,12 +315,12 @@ Replace `DIContextMiddleware` with the integration package's setup call ([FastAP A handful of `that-depends` features have no direct port. Workarounds: -- **`Selector`**: write a creator function that takes whatever the selector depended on and returns the chosen object. If the choice is static (e.g. one implementation per environment), `Alias` may be cleaner. -- **`AttrGetter` (`provider.attr` syntax)**: resolve the parent inside the consuming creator and access the attribute there, or expose a dedicated `Factory` whose creator returns the attribute. -- **`ThreadLocalSingleton`**: register an uncached `Factory` whose creator reads the object from a module-level `threading.local()` and creates and stores it there on a thread's first call. A cached `Factory` can't do this, because it caches one object for the whole container. -- **`@inject` + `Provide[T]()` for non-framework functions**: `modern-di` has no general-purpose injection decorator. Call `container.resolve(T)` explicitly at the call site, or expose the function through a framework integration and use `FromDI(T)`. +- For `Selector`, write a creator function that takes whatever the selector depended on and returns the chosen object. If the choice is static (e.g. one implementation per environment), `Alias` may be cleaner. +- For `AttrGetter` (`provider.attr` syntax), resolve the parent inside the consuming creator and access the attribute there, or expose a dedicated `Factory` whose creator returns the attribute. +- For `ThreadLocalSingleton`, register an uncached `Factory` whose creator reads the object from a module-level `threading.local()` and creates and stores it there on a thread's first call. A cached `Factory` can't do this, because it caches one object for the whole container. +- `modern-di` has no general-purpose injection decorator to replace `@inject` + `Provide[T]()` on non-framework functions. Call `container.resolve(T)` explicitly at the call site, or expose the function through a framework integration and use `FromDI(T)`. ## More -- Litestar usage example — [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template) -- FastAPI usage example — [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template) +- Litestar usage example: [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template) +- FastAPI usage example: [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template) diff --git a/docs/migration/to-1.x.md b/docs/migration/to-1.x.md index 32aaf0a9..87e3d79b 100644 --- a/docs/migration/to-1.x.md +++ b/docs/migration/to-1.x.md @@ -1,12 +1,12 @@ # Migration guide: upgrading to modern-di 1.x !!! warning "Historical guide" - This guide covers migrating from 0.x to 1.x. The APIs shown here (`AsyncContainer`, `SyncContainer`, `providers.Singleton`, `.cast`) were **removed in 2.x**. + This guide covers migrating from 0.x to 1.x. The APIs shown here (`AsyncContainer`, `SyncContainer`, `providers.Singleton`, `.cast`) were removed in 2.x. If you are on 1.x today, also follow the [2.x migration guide](to-2.x.md) to reach the current API. modern-di 1.x inverts where resolution methods live and replaces a handful of provider types. Breaking changes, once: -1. **`BaseGraph` → `Group`**; single `Container` → `AsyncContainer` or `SyncContainer` (async supports both sync and async resolution; sync is sync-only). +1. `BaseGraph` is replaced by `Group`, and the single `Container` by `AsyncContainer` or `SyncContainer` (async supports both sync and async resolution; sync is sync-only). ```python # Before (0.x) @@ -19,7 +19,7 @@ modern-di 1.x inverts where resolution methods live and replaces a handful of pr sync_container.enter() # replaces Container().sync_enter() ``` -2. **Resolution moved from provider to container**, and can now target a type directly (requires passing `groups=` at construction): +2. Resolution moved from the provider to the container, and can now target a type directly (requires passing `groups=` at construction): ```python # Before (0.x) @@ -35,7 +35,7 @@ modern-di 1.x inverts where resolution methods live and replaces a handful of pr Manual provider overrides and the way dependencies are declared in web-framework applications changed accordingly. Both now go through the container and integration APIs. -3. **`Selector` and `ContextAdapter` removed.** Replace both with `Factory` + `ContextProvider`: +3. `Selector` and `ContextAdapter` are removed. Replace both with `Factory` + `ContextProvider`: ```python # Before (0.x) @@ -46,9 +46,9 @@ modern-di 1.x inverts where resolution methods live and replaces a handful of pr dynamic_engine = providers.Factory(Scope.REQUEST, choose_engine, context=mode.cast, write=w.cast, read=r.cast) ``` -4. **`AttrGetter` removed.** Reference the provider directly, or write a small factory function that extracts the attribute. -5. **Factory attribute access removed** (`.async_provider`/`.sync_provider`). Inject the container itself and resolve dependencies manually instead of injecting a factory function. -6. **`async_enter()` → `enter()`.** +4. `AttrGetter` is removed. Reference the provider directly, or write a small factory function that extracts the attribute. +5. Factory attribute access (`.async_provider`/`.sync_provider`) is removed. Inject the container itself and resolve dependencies manually instead of injecting a factory function. +6. `async_enter()` is renamed to `enter()`. ## More diff --git a/docs/migration/to-2.x.md b/docs/migration/to-2.x.md index 9fef1ff4..337e23a2 100644 --- a/docs/migration/to-2.x.md +++ b/docs/migration/to-2.x.md @@ -2,7 +2,7 @@ modern-di 2.x merges the container classes, moves providers to keyword-only arguments, removes four provider types in favor of `Factory`, and drops async resolution. Breaking changes, once: -1. **`AsyncContainer`/`SyncContainer` → `Container`** (single class, both sync and async operations): +1. `AsyncContainer` and `SyncContainer` are merged into a single `Container` class that supports both sync and async operations: ```python # Before (1.x) @@ -18,7 +18,7 @@ modern-di 2.x merges the container classes, moves providers to keyword-only argu `with`/`async with container.build_child_container(...)` still works for automatic cleanup; `close_sync()`/`close_async()` are also available for manual lifecycle control. The framework integration packages were updated with matching new APIs. -2. **Provider constructor arguments became keyword-only.** +2. Provider constructor arguments became keyword-only. ```python # Before (1.x) @@ -30,7 +30,7 @@ modern-di 2.x merges the container classes, moves providers to keyword-only argu Since 2.27, the subject argument (`creator` / `context_type` / `source_type`) is accepted positionally again; all other parameters remain keyword-only. -3. **`Singleton`, `Resource`, `Dict`, `List` removed.** All four map onto `Factory`: +3. `Singleton`, `Resource`, `Dict`, and `List` are removed. All four map onto `Factory`: ```python # Before (1.x) @@ -51,7 +51,7 @@ modern-di 2.x merges the container classes, moves providers to keyword-only argu semantics: finalizer runs on close, instance rebuilt on next resolve); set `clear_cache=False` only when the same object must survive a close→reopen cycle. -4. **Resolution is sync-only.** No more `sync_` prefix, no `await` on resolution (async *finalizers* are still supported via `CacheSettings(finalizer=async_fn)` and `await container.close_async()`): +4. Resolution is sync-only. There is no more `sync_` prefix and no `await` on resolution (async *finalizers* are still supported via `CacheSettings(finalizer=async_fn)` and `await container.close_async()`): ```python # Before (1.x) @@ -61,11 +61,11 @@ modern-di 2.x merges the container classes, moves providers to keyword-only argu instance = container.resolve_provider(provider) ``` -5. **`.cast` removed.** Wiring is by type instead: +5. `.cast` is removed, and wiring is by type: | 1.x | 2.x | |---|---| - | `dep=other_provider.cast` (a provider dependency) | Drop the argument — annotate the creator parameter with the dependency's type. | + | `dep=other_provider.cast` (a provider dependency) | Drop the argument and annotate the creator parameter with the dependency's type. | | `value=settings.host` (a static value) | Pass it in `kwargs={"value": ...}`. | | a request/context value | Register a `ContextProvider` for that type (see [Context](../providers/context.md)). | diff --git a/docs/migration/to-3.x.md b/docs/migration/to-3.x.md index fcd372db..a69e4391 100644 --- a/docs/migration/to-3.x.md +++ b/docs/migration/to-3.x.md @@ -8,9 +8,9 @@ modern-di 3.0 flips five switches from warn-then-continue to raise/validate-by-d one more that has no 2.x precedent to warn from. Each of the five already has a 2.x signal, a warning that fires today wherever the 3.0 behavior would differ. If your 2.x test suite is green with the [readiness recipe](#readiness-recipe-escalating-warnings-to-errors-with-filterwarnings) -below escalating those five warnings to errors, **those five switches** are a no-op for you. +below escalating those five warnings to errors, those five switches are a no-op for you. -3.0 **additionally** requires a container to be opened (`with`/`async with`/`open()`) before it can +3.0 additionally requires a container to be opened (`with`/`async with`/`open()`) before it can `resolve` or `build_child_container` (switch 6 below), and changes `validate`'s constructor signature from `bool | None` to a plain `bool`. Neither has a 2.x warning to escalate: 2.x has no "unopened" state to signal on, and an explicit `validate=True` in 2.x validates eagerly at @@ -26,9 +26,9 @@ and a green suite under the recipe does not, by itself, get you past them. See | Reusing a closed container raises `ContainerClosedError` | `ContainerClosedWarning` | | `Alias(scope=)` parameter removed | `DeprecationWarning` | | `Factory(cache_settings=)` removed | `DeprecationWarning` | -| `validate` defaults to `True` and runs at container entry (`open()`/`with`) | `UnvalidatedContainerWarning` — covers the *unset* case only; see below | +| `validate` defaults to `True` and runs at container entry (`open()`/`with`) | `UnvalidatedContainerWarning` (covers the *unset* case only; see below) | | Direct resolve of an unset `ContextProvider` raises `ContextValueNotSetError` | `ContextValueNoneWarning` | -| A container must be opened before `resolve`/`build_child_container` | **none** — inherent hard break, no 2.x state to warn from | +| A container must be opened before `resolve`/`build_child_container` | None: an inherent hard break, with no 2.x state to warn from | ## Key changes @@ -38,7 +38,7 @@ In 2.x, resolving from (or building a child of) a closed container emits `Contai and transparently reopens the container so the call still succeeds. In 3.0 the same call raises `ContainerClosedError` instead. -**Before (2.x):** +Before (2.x): ```python container = Container(scope=Scope.APP, groups=[MyGroup], validate=True) container.close_sync() @@ -50,7 +50,7 @@ container.close_sync() service = container.resolve(MyService) # succeeds — container self-reopens ``` -**After (3.0):** +After (3.0): ```python container = Container(scope=Scope.APP, groups=[MyGroup], validate=True) with container: @@ -62,7 +62,7 @@ service = container.resolve(MyService) # raises ContainerClosedError — reused Re-enter the container with `with`/`async with`, or call `container.open()`, before reusing it. -This is one half of a single rule: **a container must be open to be used.** This switch is the +This is one half of a single rule: a container must be open to be used. This switch is the *closed-after-use* half (a container that was open, then closed); [switch 6](#6-a-container-must-be-opened-before-use) below is the *never-opened* half (a fresh container that was never entered at all). Both raise the same `ContainerClosedError`, and both are fixed the same way: enter the container with @@ -74,7 +74,7 @@ same `ContainerClosedError`, and both are fixed the same way: enter the containe never affected resolution. In 2.x, passing it emits a `DeprecationWarning`; in 3.0 the parameter is gone. -**Before (2.x):** +Before (2.x): ```python from modern_di import Scope, providers @@ -84,7 +84,7 @@ from modern_di import Scope, providers alias = providers.Alias(DatabaseProtocol, scope=Scope.APP) ``` -**After (3.0):** +After (3.0): ```python from modern_di import providers @@ -96,7 +96,7 @@ alias = providers.Alias(DatabaseProtocol) `cache_settings=` was the pre-`cache=` spelling for tuning a `Factory`'s cache. In 2.x it still works but warns; in 3.0 only `cache=` is accepted. -**Before (2.x):** +Before (2.x): ```python # DeprecationWarning: `cache_settings=` is deprecated; use `cache=` (pass # cache=True for defaults, or cache=CacheSettings(...) to tune). It will be @@ -108,7 +108,7 @@ factory = providers.Factory( ) ``` -**After (3.0):** +After (3.0): ```python factory = providers.Factory( create_resource, @@ -121,18 +121,18 @@ factory = providers.Factory( The final 3.0 form differs from what 2.x signals in two ways, so read this one carefully. -**The signature.** In 2.x, `Container`'s `validate` argument is `bool | None = None`: unset (`None`) +The first is the signature. In 2.x, `Container`'s `validate` argument is `bool | None = None`: unset (`None`) skips validation but emits `UnvalidatedContainerWarning`; `False` skips it silently; `True` enables it. In 3.0, the parameter is a plain `validate: bool = True`, and the `None` sentinel is gone. Passing `validate=False` still means "off"; there is no other spelling to adopt for the unset case, because unset now *is* the default-on case. -**The timing.** In 2.x, `validate=True` validates **eagerly at construction**: `Container(...)` +The second is the timing. In 2.x, `validate=True` validates eagerly at construction: `Container(...)` itself raises `ValidationFailedError` if the graph is broken. In 3.0, validation never runs in -`__init__`. It runs once, at container **entry** (`open()`, or `with`/`async with`, which call +`__init__`. It runs once, at container entry (`open()`, or `with`/`async with`, which call `open()`), so an invalid graph raises there instead. This lets a framework integration register its own providers (e.g. via `add_providers`) after construction and still have the complete graph -validated before first use. `validate=True` is **not eager**: if you need a construction-time +validated before first use. `validate=True` is not eager: if you need a construction-time check, call `container.validate()` explicitly right after building it. This timing change has no 2.x warning: an explicit `validate=True` caller in 2.x sees no @@ -143,7 +143,7 @@ about when validation happens once enabled. Escalating it to an error still gets signal for switching the *default* to on. It does not, and cannot, warn you about the *timing* move for callers who already pass `validate=True`. -**Before (2.x):** +Before (2.x): ```python # UnvalidatedContainerWarning: This root container was created without an # explicit `validate` argument. modern-di 3.0 runs validate() at container @@ -156,7 +156,7 @@ container.resolve(MyService) container = Container(scope=Scope.APP, groups=[MyGroup], validate=True) # raises here if broken ``` -**After (3.0):** +After (3.0): ```python # validate is on by default; it runs once at open(), not at construction. with Container(scope=Scope.APP, groups=[MyGroup]) as container: @@ -175,7 +175,7 @@ container.validate() # raises ValidationFailedError here if the graph is broken Child containers (built via `build_child_container`) never validate, in either version; this switch only affects root containers. -**Changed again in 3.1.** Validation is explicit-only; `open()` no longer runs it either. See +3.1 changed this again. Validation is explicit-only; `open()` no longer runs it either. See the [3.1 note under switch 6](#6-a-container-must-be-opened-before-use) below for the full correction. @@ -187,7 +187,7 @@ In 2.x, resolving a type backed by a `ContextProvider` with no value set emits parameter backed by the same `ContextProvider` continues to follow its own default/nullable/required disposition, unchanged. -**Before (2.x):** +Before (2.x): ```python # ContextValueNoneWarning: No context value is set for (scope # APP); returning None. modern-di 3.0 raises ContextValueNotSetError here. @@ -195,7 +195,7 @@ default/nullable/required disposition, unchanged. value = container.resolve(SomeContextType) # None ``` -**After (3.0):** +After (3.0): ```python value = container.resolve(SomeContextType) # raises ContextValueNotSetError ``` @@ -206,7 +206,7 @@ resolving. ### 6. A container must be opened before use -New in 3.0, added mid-development, with **no 2.x deprecation signal at all**: 2.x has no +New in 3.0, added mid-development, with no 2.x deprecation signal at all: 2.x has no "unopened" state, so there was never anything for it to warn about. A freshly constructed container now starts unopened; using it before entering it (`resolve`, `resolve_provider`, `build_child_container`) raises `ContainerClosedError`. Enter it with `with`/`async with`, or call @@ -215,11 +215,11 @@ use. Child containers (from `build_child_container`) also start unopened and mus themselves before they can be used. This is the *never-opened* half of the same rule as [switch 1](#1-closed-containers-raise-instead-of-self-healing) -above (the *closed-after-use* half): **a container must be open to be used**, whether it was never +above (the *closed-after-use* half): a container must be open to be used, whether it was never opened or was opened and then closed. Both cases raise the identical `ContainerClosedError`, with a message that names which state applies, and both are fixed the same way. -**Before (2.x):** +Before (2.x): ```python container = Container(scope=Scope.APP, groups=[MyGroup]) service = container.resolve(MyService) # works — no open() call needed @@ -228,7 +228,7 @@ child = container.build_child_container(scope=Scope.REQUEST) value = child.resolve(SomeContextType) # works — no open() call needed either ``` -**After (3.0):** +After (3.0): ```python container = Container(scope=Scope.APP, groups=[MyGroup]) service = container.resolve(MyService) # raises ContainerClosedError: not open @@ -251,9 +251,9 @@ Because there is no 2.x signal for this one, the [readiness recipe](#readiness-r below cannot surface it in advance. A green 2.x suite under that recipe still needs every construct-then-use call site audited for a matching `with`/`open()` before it can run against 3.0. -**Changed again in 3.1.** This requirement is relaxed, not reversed: see the +3.1 changed this again. The requirement is relaxed, not reversed: see the [3.1 release notes](https://github.com/modern-python/modern-di/releases) for the full -change. A container is **open from construction** again (`closed = False` the moment +change. A container is open from construction again (`closed = False` the moment `Container(...)` returns, no `open()` step required), and reusing a container after an explicit close warns (`ContainerClosedWarning`) and reopens instead of raising `ContainerClosedError`. @@ -296,7 +296,7 @@ This is the one place in the docs that lists the full `filterwarnings` escalatio other page that mentions escalating a specific warning links back here. This recipe covers switches 1, 2, 3, and 5 fully, and switch 4 only for the *unset-`validate`* -case, the case `UnvalidatedContainerWarning` actually warns about. It has **nothing** to say about +case, the case `UnvalidatedContainerWarning` actually warns about. It has nothing to say about switch 6 (mandatory-open) or about switch 4's timing move for callers who already pass `validate=True` explicitly: both are hard breaks with no 2.x warning to escalate. A green suite under this recipe rules out five-and-a-half of the six switches; you still need to audit @@ -346,7 +346,7 @@ filterwarnings = [ happens to still be inside `modern_di`, so they *would* match. That inconsistency is exactly why `module=` isn't part of the recipe above. - **Changed again in 3.1.** `ContainerClosedWarning` now computes its `stacklevel` (via + 3.1 changed this again. `ContainerClosedWarning` now computes its `stacklevel` (via `_caller_stacklevel`) so it attributes *outside* `modern_di`, and `ContextValueNoneWarning` has no raise sites left at all, so on 3.1 a `module=r"modern_di(\..*)?"` filter escalates none of the five. The paragraph above describes 2.x, which is what this page's recipe runs diff --git a/docs/providers/advanced-api.md b/docs/providers/advanced-api.md index 5dc9c9cd..a4fcf9b3 100644 --- a/docs/providers/advanced-api.md +++ b/docs/providers/advanced-api.md @@ -14,7 +14,7 @@ inspect or iterate all providers declared on a group hierarchy. !!! note "The provider set is closed: `AbstractProvider` is not an extension point" `Factory`, `Alias`, `ContextProvider`, and the pre-built `container_provider` are the only provider types. `AbstractProvider` is their shared base and the type that appears - in public signatures (`resolve_dependency`, `kwargs=`), but it is **not** a hook for + in public signatures (`resolve_dependency`, `kwargs=`), but it is not a hook for adding your own: resolution compiles a resolver per known provider type, so a subclass of `AbstractProvider` (or of `Factory`) raises `TypeError` at its first resolve, and `validate()` does not catch it. Compose behavior in a creator function, or use `Alias`, @@ -33,21 +33,21 @@ inspect or iterate all providers declared on a group hierarchy. for debugging and deep integration work only, and may change without a deprecation cycle. Do not build on them. -- **`parent_container`** is a constructor kwarg and slot: the direct parent of a child container, +- `parent_container` is a constructor kwarg and slot: the direct parent of a child container, or `None` for a root. Passing a `scope ≤ parent.scope` raises `InvalidChildScopeError`. -- **`_scope_map`** is a `dict[IntEnum, Container]` mapping each **ancestor's** scope to its +- `_scope_map` is a `dict[IntEnum, Container]` mapping each **ancestor's** scope to its container, built at construction time, with a child inheriting its parent's map plus the parent itself. A root's map is empty. The container is never in its own map, since that self-reference would make every container a reference cycle, and `find_container` never needs it: it short-circuits on its own scope first. The compiled resolvers read it directly on every cross-scope hop. -- **`find_container(scope)`** returns `self` when `scope` is the container's own scope, otherwise +- `find_container(scope)` returns `self` when `scope` is the container's own scope, otherwise the ancestor in `_scope_map`, and raises `ScopeNotInitializedError` or `ScopeSkippedError` when the scope is absent. The compiled resolvers call it only on that miss, so overriding it in a `Container` subclass does not redirect navigation. `resolve` and `resolve_provider` are entry points, not hooks either: a compiled resolver calls its dependencies' resolvers directly, so an override of either sees only the top-level call. -- **`_lock`** is the tree's `threading.RLock`, created by the root and shared by every child, or +- `_lock` is the tree's `threading.RLock`, created by the root and shared by every child, or `None` when the root was created with `use_lock=False`. A cached `Factory`'s compiled resolver hands it to `CacheItem.get_or_create`, which gates the cold-miss build so one instance is created per cache key. diff --git a/docs/providers/container.md b/docs/providers/container.md index 975f4fde..d569f95b 100644 --- a/docs/providers/container.md +++ b/docs/providers/container.md @@ -28,7 +28,7 @@ result = container.resolve(str) ### Explicit injection -You can also explicitly inject the container using `providers.container_provider`. Reach for this when the parameter is **not** annotated as `Container` (so type-based injection can't find it), or when you want an explicit binding instead of relying on the type: +You can also explicitly inject the container using `providers.container_provider`. Reach for this when the parameter is not annotated as `Container` (so type-based injection can't find it), or when you want an explicit binding instead of relying on the type: ```python from modern_di import Container, Group, Scope, providers @@ -52,7 +52,7 @@ result = container.resolve(str) ## Which container you get Resolving `Container` returns the **calling container**: the deepest, most-specific container in -the active chain, not the `APP` root. The `container_provider` simply hands back whichever container +the active chain, not the `APP` root. The `container_provider` hands back whichever container ran the resolve, so a `REQUEST` child resolves `Container` to *itself*: ```python @@ -83,7 +83,7 @@ resolve a `FromDI`-style marker. See [Writing an integration](../integrations/wr ## See also -- **Context propagation** — how context values reach (and don't reach) a `ContextProvider` is - covered on the [Context providers](context.md#context-propagation) page. -- **Low-level API** — `find_container`, `scope_map`, and `Group.get_providers()` are documented - under [Advanced / low-level API](advanced-api.md). +- [Context providers](context.md#context-propagation) covers how context values reach (and don't + reach) a `ContextProvider`. +- [Advanced / low-level API](advanced-api.md) documents `find_container`, `scope_map`, and + `Group.get_providers()`. diff --git a/docs/providers/context.md b/docs/providers/context.md index 25c42bf0..09fb6ab9 100644 --- a/docs/providers/context.md +++ b/docs/providers/context.md @@ -108,9 +108,9 @@ objects, so you never declare a `ContextProvider` for these yourself. Each integ per-request (or per-message, or per-connection) child container and sets the framework object as context on it before your code resolves anything from it. There are two ways to consume that value: -**Implicit usage (type-based resolution).** Annotate a factory parameter with the framework's +For implicit, type-based resolution, annotate a factory parameter with the framework's type; because the integration already registered a matching `ContextProvider`, modern-di resolves -it automatically. It is the same mechanism as [Basic usage](#basic-usage) above, just with the +it automatically. It is the same mechanism as [Basic usage](#basic-usage) above, with the `ContextProvider` declared by the integration instead of by you. With [FastAPI](../integrations/fastapi.md), the `fastapi.Request` is injected into each per-request child container automatically: @@ -147,7 +147,7 @@ container.validate() Nothing validates automatically, so the ordering above is what matters: `fastapi.Request`'s `ContextProvider` only exists once `setup_di()` has registered it, so calling -`container.validate()` **before** that line would raise +`container.validate()` before that line would raise [`ValidationFailedError`](../troubleshooting/validation-failed-error.md), and its `.errors` would carry an [`ArgumentResolutionError`](../troubleshooting/argument-resolution-error.md) for the required `request` parameter, since the provider isn't there yet. Call `validate()` after @@ -164,7 +164,7 @@ parameter keeps its own disposition here too: `ContextValueNotSetError` (see [Wh set](#when-no-value-is-set) above) affects only a *direct* resolve of an unset context type, not a defaulted parameter, which still falls back to its default when no context is set. -**Explicit usage (provider-based resolution).** Every integration also exports the underlying +For explicit, provider-based resolution, every integration also exports the underlying `ContextProvider` object itself (e.g. `fastapi_request_provider`, `litestar_request_provider`, `aiohttp_request_provider`, `faststream_message_provider`) so you can wire it through `kwargs` instead of relying on type-based resolution. This is useful with `skip_creator_parsing=True`, or @@ -183,7 +183,7 @@ Each integration's own page has its exact provider names, scopes, and API table: ## See also -- [Factories](factories.md) — how factories receive injected context values. -- [Scopes](scopes.md) — choosing the scope a `ContextProvider` is bound to. -- [Container](container.md) — `build_child_container` and `set_context`. -- [FastAPI integration](../integrations/fastapi.md) — framework-provided context objects. +- [Factories](factories.md): how factories receive injected context values. +- [Scopes](scopes.md): choosing the scope a `ContextProvider` is bound to. +- [Container](container.md): `build_child_container` and `set_context`. +- [FastAPI integration](../integrations/fastapi.md): framework-provided context objects. diff --git a/docs/providers/errors-and-exceptions.md b/docs/providers/errors-and-exceptions.md index 7c140e9c..d1a0b7f3 100644 --- a/docs/providers/errors-and-exceptions.md +++ b/docs/providers/errors-and-exceptions.md @@ -47,7 +47,7 @@ ModernDIError (RuntimeError) ## Root -- **`ModernDIError`** is the base class for every error the library raises. It subclasses +- `ModernDIError` is the base class for every error the library raises. It subclasses `RuntimeError` for backwards compatibility, so `except RuntimeError` keeps working. Catch `ModernDIError` to handle any framework error in one place. @@ -55,30 +55,30 @@ ModernDIError (RuntimeError) Catch `ContainerError` for any container/scope failure. -- **`InvalidChildScopeError`** is raised when `build_child_container(scope=...)` is given a scope +- `InvalidChildScopeError` is raised when `build_child_container(scope=...)` is given a scope that is not deeper than the parent's (or the constructor receives a parent at an equal/shallower scope). The error lists the scopes that *are* allowed. See [Troubleshooting: InvalidChildScopeError](../troubleshooting/invalid-child-scope-error.md). -- **`MaxScopeReachedError`** is raised by `build_child_container()` with no explicit `scope` when the +- `MaxScopeReachedError` is raised by `build_child_container()` with no explicit `scope` when the parent is already at the deepest scope (`STEP`), so there is no next level to advance to. See [Troubleshooting: MaxScopeReachedError](../troubleshooting/max-scope-reached-error.md). -- **`ScopeNotInitializedError`** is raised during resolution when a provider needs a scope *deeper* +- `ScopeNotInitializedError` is raised during resolution when a provider needs a scope *deeper* than the current container's, and no container at that scope exists in the chain (e.g. resolving a `REQUEST`-scoped provider from the `APP` container). Like `ResolutionError`, it carries a breadcrumb `dependency_path`: a runtime *captive dependency* (a shallower-scoped provider depending, directly or transitively, on this deeper-scoped one) names both the capturing provider and the one that actually failed, not just the two scope names. See [Troubleshooting: ScopeNotInitializedError](../troubleshooting/scope-not-initialized-error.md). -- **`ScopeSkippedError`** is raised during resolution when the target scope is *shallower* than the +- `ScopeSkippedError` is raised during resolution when the target scope is *shallower* than the current container but is missing from the scope chain (a level was skipped when building children). Carries the same breadcrumb `dependency_path` as `ScopeNotInitializedError`. See [Troubleshooting: ScopeSkippedError](../troubleshooting/scope-skipped-error.md). -- **`InvalidScopeTypeError`** is raised by the `Container` constructor, and by a `Group` subclass +- `InvalidScopeTypeError` is raised by the `Container` constructor, and by a `Group` subclass declared as `class G(Group, scope=...)`, when `scope` is not an `enum.IntEnum`. See [Troubleshooting: InvalidScopeTypeError](../troubleshooting/invalid-scope-type-error.md). -- **`ContainerClosedError`** is no longer raised as of modern-di 3.1; it stays importable for +- `ContainerClosedError` has not been raised since modern-di 3.1; it stays importable for back-compat and is removed in 4.0. A container is open from construction, so there is nothing to - raise: resolving from a container that was **explicitly closed**, directly or through a child + raise: resolving from a container that was explicitly closed, directly or through a child whose resolve reaches back into its scope, reopens it and emits `ContainerClosedWarning` (a `RuntimeWarning`, not a `ModernDIError`) instead. `build_child_container()` itself never checks or touches any container's open/closed state, so building a child of a closed parent triggers neither @@ -86,13 +86,13 @@ Catch `ContainerError` for any container/scope failure. `container.open()`, to reopen it deliberately and silently. See [Lifecycle: closing and reopening](lifecycle.md#closing-and-reopening) and [Troubleshooting: ContainerClosedError](../troubleshooting/container-closed-error.md). -- **`ValidationFailedError`** is raised only by `Container.validate()`. Catch this for validation +- `ValidationFailedError` is raised only by `Container.validate()`. Catch this for validation results; its `.errors` attribute holds the list of individual issues (each itself a `ResolutionError` or `RegistrationError`), and `str()` renders them all, grouped by error kind. Nothing validates automatically (not construction, not `open()`, not `add_providers`, not `resolve()`), so call `validate()` explicitly whenever you want the whole graph checked; an integration that registers its own providers after construction (via `add_providers`) should call - it **after** that registration. `Container(validate=...)` is a deprecated no-op: passing `True` or + it after that registration. `Container(validate=...)` is a deprecated no-op: passing `True` or `False` emits `ValidateArgumentWarning` and gates nothing. See [Lifecycle: validation](lifecycle.md#validation), [Migration: To 3.x](../migration/to-3.x.md) and @@ -105,27 +105,27 @@ accumulated as the error propagates, so the message shows the full chain from th down to the failing dependency. `dependency_path` is a `list[ResolutionStep]`, where each `ResolutionStep` (importable from `modern_di.exceptions`) has a `.scope` and a `.name`; inspect it to render the chain programmatically. `ScopeNotInitializedError` and `ScopeSkippedError` (below) carry -the same `dependency_path`, since the breadcrumb machinery is shared rather than duplicated. +the same `dependency_path`, since they share the breadcrumb machinery. -- **`ProviderNotRegisteredError`** is raised by `resolve(SomeType)` when no provider is registered for +- `ProviderNotRegisteredError` is raised by `resolve(SomeType)` when no provider is registered for the type. The message includes "did you mean…" suggestions when a close match exists. See [Troubleshooting: Missing provider](../troubleshooting/missing-provider.md). -- **`AliasSourceNotRegisteredError`** is raised when an `Alias` points at a `source_type` that has no +- `AliasSourceNotRegisteredError` is raised when an `Alias` points at a `source_type` that has no registered provider (eagerly during `validate()`, or at resolution time). See [Troubleshooting: AliasSourceNotRegisteredError](../troubleshooting/alias-source-not-registered-error.md). -- **`ArgumentResolutionError`** is raised when a creator parameter cannot be resolved: no provider +- `ArgumentResolutionError` is raised when a creator parameter cannot be resolved: no provider matches its annotated type, or the parameter is unannotated. See [Troubleshooting: ArgumentResolutionError](../troubleshooting/argument-resolution-error.md). -- **`CircularDependencyError`** is raised when the provider graph contains a cycle (A → B → A); the +- `CircularDependencyError` is raised when the provider graph contains a cycle (A → B → A); the message shows the cycle path. Raised eagerly by `validate()`, and also by a bare `resolve()` on an unvalidated cyclic graph via a runtime guard; see [Troubleshooting: Circular dependency](../troubleshooting/circular-dependency.md#the-runtime-cycle-guard-without-validate). -- **`CreatorCallError`** is raised when a creator's dependencies all resolved but argument binding +- `CreatorCallError` is raised when a creator's dependencies all resolved but argument binding failed while calling it (the assembled arguments don't match the signature, typically a `kwargs` / `skip_creator_parsing` mismatch). Exceptions raised *inside* the creator body propagate unchanged, never wrapped. The binding `TypeError` is preserved on `.original_error` (and as the `__cause__`). See [Troubleshooting: CreatorCallError](../troubleshooting/creator-call-error.md). -- **`ContextValueNotSetError`** is raised when an unset `ContextProvider` is resolved *directly* +- `ContextValueNotSetError` is raised when an unset `ContextProvider` is resolved *directly* (`container.resolve(SomeContextType)` with no value set); there is no fallback. See [Migration: To 3.x](../migration/to-3.x.md#5-direct-resolve-of-an-unset-contextprovider-raises). Only the direct-resolve path is affected; a `Factory` parameter backed by the same @@ -136,33 +136,33 @@ the same `dependency_path`, since the breadcrumb machinery is shared rather than Catch `RegistrationError` for declaration- and registration-time problems. -- **`DuplicateProviderTypeError`** is raised when two providers are registered for the same bound type +- `DuplicateProviderTypeError` is raised when two providers are registered for the same bound type (within one group, across groups passed together, or against an already-registered type). See [Troubleshooting: Duplicate type](../troubleshooting/duplicate-type-error.md). -- **`ChildContainerRegistrationError`** is raised by `Container.add_providers()` when called on a child +- `ChildContainerRegistrationError` is raised by `Container.add_providers()` when called on a child container; registration is root-only because the providers registry is shared tree-wide, so registering from a child would mutate every container in the tree. Call `add_providers` on the root container instead. Inspect `.scope` for the offending child container's scope. See [Container: registering after construction](container.md#registering-providers-after-construction) and [Troubleshooting: ChildContainerRegistrationError](../troubleshooting/child-container-registration-error.md). -- **`GroupScopeConflictError`** is raised when a scope-defaulted provider (no explicit `scope=`) is +- `GroupScopeConflictError` is raised when a scope-defaulted provider (no explicit `scope=`) is shared by two `Group` subclasses declared with different `scope=` kwargs; the provider's scope cannot follow both defaults at once, and import order must never be what decides it. Inspect `.provider_name`, `.first_group`/`.first_scope`, and `.second_group`/`.second_scope`. See [Troubleshooting: GroupScopeConflictError](../troubleshooting/group-scope-conflict-error.md). -- **`ProviderScopeFrozenError`** is raised when a `Group` would change the scope of a provider that +- `ProviderScopeFrozenError` is raised when a `Group` would change the scope of a provider that is already registered with a container. Resolvers compiled before the change captured the old scope, so applying it would make the same provider resolve differently through an existing container than through a fresh one. Inspect `.provider_name`, `.group_name`, `.current_scope`, `.new_scope`. See [Troubleshooting: ProviderScopeFrozenError](../troubleshooting/provider-scope-frozen-error.md). -- **`UnknownFactoryKwargError`** is raised when `Factory(kwargs={...})` contains a key that is not a +- `UnknownFactoryKwargError` is raised when `Factory(kwargs={...})` contains a key that is not a parameter of the creator's signature; lists the known parameters and "did you mean" hints. See [Troubleshooting: UnknownFactoryKwargError](../troubleshooting/unknown-factory-kwarg-error.md). -- **`UnsupportedCreatorParameterError`** is raised when a creator's signature has a parameter +- `UnsupportedCreatorParameterError` is raised when a creator's signature has a parameter `modern-di` cannot wire (e.g. an unsupported kind); names the parameter and the reason. See [Troubleshooting: UnsupportedCreatorParameterError](../troubleshooting/unsupported-creator-parameter-error.md). -- **`InvalidScopeDependencyError`** is raised when a provider depends on another provider bound to a +- `InvalidScopeDependencyError` is raised when a provider depends on another provider bound to a *deeper* scope than its own (a longer-lived provider depending on a shorter-lived one). Surfaced by `validate()`. Renders the chain from the depender to the provider that supplies the dependency; `.dep_chain` carries that chain, with `.dep_provider` and `.dep_terminal` as its ends. See @@ -172,17 +172,17 @@ Catch `RegistrationError` for declaration- and registration-time problems. These don't fit the register/resolve/validate grouping: -- **`FinalizerError`** is raised by `close_sync()` / `close_async()` when one or more finalizers raised +- `FinalizerError` is raised by `close_sync()` / `close_async()` when one or more finalizers raised during cleanup. The remaining finalizers still run; all errors are aggregated into this single exception. `.finalizer_errors` holds the list and `.is_async` records which close path ran. See [Lifecycle](lifecycle.md#close-failure-semantics) and [Troubleshooting: FinalizerError](../troubleshooting/finalizer-error.md). -- **`AsyncFinalizerInSyncCloseError`** is raised when `close_sync()` reaches a cached resource whose +- `AsyncFinalizerInSyncCloseError` is raised when `close_sync()` reaches a cached resource whose finalizer is async. Because `close_sync()` aggregates, this arrives *wrapped inside a* `FinalizerError` (as an entry in `.finalizer_errors`), not on its own. The cache is retained so a later `await close_async()` can finalize it. See [Lifecycle](lifecycle.md#close-failure-semantics) and [Troubleshooting: AsyncFinalizerInSyncCloseError](../troubleshooting/async-finalizer-in-sync-close-error.md). -- **`GroupInstantiationError`** is raised when a `Group` subclass is instantiated. Groups are +- `GroupInstantiationError` is raised when a `Group` subclass is instantiated. Groups are namespaces and must never be created as objects. See [Troubleshooting: GroupInstantiationError](../troubleshooting/group-instantiation-error.md). diff --git a/docs/providers/factories.md b/docs/providers/factories.md index 46626fae..ecd905dd 100644 --- a/docs/providers/factories.md +++ b/docs/providers/factories.md @@ -44,7 +44,7 @@ assert isinstance(instance2, IndependentFactory) Cached factories resolve the dependency only once and cache the resolved instance for future injections. -**This is modern-di's Singleton.** There is no separate `Singleton` provider class: `Factory(cache=True)` +This is modern-di's Singleton. There is no separate `Singleton` provider class: `Factory(cache=True)` *is* the singleton idiom, at whatever scope you declare it (`Scope.APP` for one-per-process, `Scope.REQUEST` for one-per-request, etc.). Other DI frameworks name this concept `Singleton`, `provide(..., scope=...)`, `@injectable(lifetime="singleton")`, or `@lru_cache`; see @@ -208,14 +208,14 @@ The table below summarises how Modern-DI handles each parameter shape during **d | Parameter shape | Behaviour | When it fails | |---|---|---| -| `param: SomeClass` — plain type annotation with a registered provider | Resolved and injected automatically. | `ArgumentResolutionError` at resolve if no provider is registered and there is no default. | -| `param: X | None` / `Optional[X]` | Provider injected if one is registered; otherwise `None`. | Never fails — see [Optional parameters](#optional-parameters). | -| `param: A | B` — union without `None` | First registered type from the union is injected. A member that is itself a parameterized generic (e.g. `int | list[X]`) degrades to its bare origin (`list`) for matching purposes — see the note below. | `ArgumentResolutionError` at resolve if neither `A` nor `B` has a registered provider. | +| `param: SomeClass` (plain type annotation with a registered provider) | Resolved and injected automatically. | `ArgumentResolutionError` at resolve if no provider is registered and there is no default. | +| `param: X | None` / `Optional[X]` | Provider injected if one is registered; otherwise `None`. | Never fails; see [Optional parameters](#optional-parameters). | +| `param: A | B` (union without `None`) | First registered type from the union is injected. A member that is itself a parameterized generic (e.g. `int | list[X]`) degrades to its bare origin (`list`) for matching purposes; see the note below. | `ArgumentResolutionError` at resolve if neither `A` nor `B` has a registered provider. | | `param: list[X]` / any parameterized generic, **outside a union** | **`UnsupportedCreatorParameterError` at declaration** unless the parameter has a default value or is covered by `kwargs`. | Raised at `Factory(...)` call time. | | Positional-only param (`def f(x: T, /)`) | **`UnsupportedCreatorParameterError` at declaration** unless the parameter has a default (in which case it is silently skipped). | Raised at `Factory(...)` call time. | | Unannotated param (`def f(x)`) | Parsed but unresolvable by type. | `ArgumentResolutionError` at resolve unless covered by `kwargs`. | | Signature whose hints `get_type_hints` cannot resolve (e.g. a forward reference to an undefined name, or `functools.partial` on Python < 3.14) | `UserWarning` is emitted and type-based wiring is skipped; parameters are still parsed (as unannotated). Silence by passing `skip_creator_parsing=True` and an explicit `bound_type`. | A required unannotated param with no provider/default raises `ArgumentResolutionError` at resolve unless covered by `kwargs` (a parameterized-generic or positional-only param still raises `UnsupportedCreatorParameterError` at declaration). | -| `skip_creator_parsing=True` | No wiring at all — every required argument must be supplied via `kwargs`. | `CreatorCallError` at resolve for any missing required argument. | +| `skip_creator_parsing=True` | No wiring at all; every required argument must be supplied via `kwargs`. | `CreatorCallError` at resolve for any missing required argument. | A parameterized generic used *inside* a union (`param: int | list[X]`) is the one exception to the "parameterized generic raises at declaration" row above: the member degrades to its bare diff --git a/docs/providers/lifecycle.md b/docs/providers/lifecycle.md index 5eafe280..2a97acad 100644 --- a/docs/providers/lifecycle.md +++ b/docs/providers/lifecycle.md @@ -34,8 +34,8 @@ session = providers.Factory( ) ``` -- **Caching.** With `cache=True`, the provider returns the same instance for every resolve inside that scope's container. That is the singleton idiom; see [Cached factories](factories.md#cached-factories). Without `cache`, the provider creates a fresh instance every call. -- **Finalizer.** A callable that runs on the cached instance when the container is closed. It can be sync or async; `CacheSettings` auto-detects via `inspect.iscoroutinefunction()`. The finalizer takes one argument: the cached instance. +- With `cache=True`, the provider returns the same instance for every resolve inside that scope's container. That is the singleton idiom; see [Cached factories](factories.md#cached-factories). Without `cache`, the provider creates a fresh instance every call. +- A finalizer is a callable that runs on the cached instance when the container is closed. It can be sync or async; `CacheSettings` auto-detects via `inspect.iscoroutinefunction()`. The finalizer takes one argument: the cached instance. ```python def close_engine_sync(engine: Engine) -> None: @@ -177,7 +177,7 @@ Framework integrations handle this automatically: they build the REQUEST child c `container.validate()` is the only thing that walks the graph. Nothing validates automatically: not construction, not `open()`, not `add_providers`, not `resolve()`. A container is fully usable, -and stays usable, without ever calling `validate()`; a broken graph nobody validates simply surfaces +and stays usable, without ever calling `validate()`; a broken graph nobody validates surfaces at whichever resolve first hits the problem, as an ordinary resolution error. Call it explicitly, whenever you want the whole graph checked at once: cycles, inverted scope @@ -192,7 +192,7 @@ It aggregates every issue it finds into one `exceptions.ValidationFailedError` r at the first; see [Troubleshooting: ValidationFailedError](../troubleshooting/validation-failed-error.md). Call it right after building the container for a construction-time check, or later. A framework integration that registers its own providers after construction (via `add_providers`) should call it -**after** that registration, so the complete graph is what gets checked; see [Writing an +after that registration, so the complete graph is what gets checked; see [Writing an integration](../integrations/writing-integrations.md#lifecycle-rules). A repeat `validate()` after a clean walk is free: it memoizes against the registry's contents and @@ -204,14 +204,14 @@ you don't want to discover under load. `Container(validate=...)` still exists for backward compatibility. Passing `True` or `False` is ignored and emits `exceptions.ValidateArgumentWarning` (a `DeprecationWarning`); omitting it (the -default) is silent either way. It changes nothing about the container built: there is no longer a -spelling of the constructor that validates for you. The argument is removed in 4.0; call +default) is silent either way. It changes nothing about the container built: no spelling of the +constructor validates for you. The argument is removed in 4.0; call `container.validate()` instead. See [Migration: To 3.x](../migration/to-3.x.md#4-validate-runs-at-container-entry-on-by-default) for how this used to work. ## See also -- [Scopes](scopes.md) — child containers and per-scope finalization. -- [Factories](factories.md) — `CacheSettings` is configured on the factory itself. -- [Async resources via lifespan](../recipes/async-lifespan.md) — sync creator + async finalizer is the most common shape. +- [Scopes](scopes.md): child containers and per-scope finalization. +- [Factories](factories.md): `CacheSettings` is configured on the factory itself. +- [Async resources via lifespan](../recipes/async-lifespan.md): sync creator + async finalizer is the most common shape. diff --git a/docs/providers/scopes.md b/docs/providers/scopes.md index 5e920d46..0637fbc0 100644 --- a/docs/providers/scopes.md +++ b/docs/providers/scopes.md @@ -15,7 +15,7 @@ APP → SESSION → REQUEST → ACTION → STEP | `APP` | One-per-process resources: settings, the database engine, a Redis client, a Kafka producer. The default if you omit `scope=`. | | `SESSION` | One-per-websocket-connection resources. Framework integrations enter SESSION automatically when a websocket opens. | | `REQUEST` | One-per-HTTP-request resources: the database session, the per-request user repository, the current `Request` object. Framework integrations create the REQUEST child container for each incoming request. | -| `ACTION` | A sub-step inside a request — e.g. one item in a batch handler that should get its own cached values. Enter manually with `build_child_container`. | +| `ACTION` | A sub-step inside a request, e.g. one item in a batch handler that should get its own cached values. Enter manually with `build_child_container`. | | `STEP` | A sub-step inside an ACTION. Same idea, one level deeper. | `APP` and `REQUEST` cover the vast majority of real apps. Reach for `SESSION` only for websockets; `ACTION`/`STEP` are for cases where you want isolated caching inside a request. @@ -40,9 +40,9 @@ Children share their parent's `providers_registry` (provider definitions) and `o ## The scope dependency rule -**A provider can only depend on providers at the same scope or a broader (lower int) scope.** A REQUEST-scoped session can consume the APP-scoped engine. The engine cannot consume the session. +A provider can only depend on providers at the same scope or a broader (lower int) scope. A REQUEST-scoped session can consume the APP-scoped engine. The engine cannot consume the session. -Why: lifetime safety. If an APP-scoped singleton held a reference to a REQUEST-scoped session, the session would outlive its request and produce stale state. This is called a **captive dependency**: a wide-scoped (long-lived) provider "captive" to a narrower-scoped (shorter-lived) one it cannot actually hold onto. `container.validate()` enforces this, so call it at startup. See [Good and bad practices](../recipes/good-and-bad-practices.md#1-captive-dependency-a-wide-scoped-provider-holding-a-narrow-scoped-one) for a worked example of the mistake and the fix. +The rule exists for lifetime safety. If an APP-scoped singleton held a reference to a REQUEST-scoped session, the session would outlive its request and produce stale state. This is called a **captive dependency**: a wide-scoped (long-lived) provider "captive" to a narrower-scoped (shorter-lived) one it cannot actually hold onto. `container.validate()` enforces this, so call it at startup. See [Good and bad practices](../recipes/good-and-bad-practices.md#1-captive-dependency-a-wide-scoped-provider-holding-a-narrow-scoped-one) for a worked example of the mistake and the fix. ### How to choose a scope @@ -56,9 +56,8 @@ If you pick a broader scope than the rule allows, `container.validate()` catches ## Building child containers -Two patterns: - -**Manual.** Use the child container as a context manager so finalizers run on exit: +You can build child containers yourself or let a framework integration do it. To build one +yourself, use the child container as a context manager so finalizers run on exit: ```python with app_container.build_child_container(scope=Scope.REQUEST) as request_container: @@ -72,7 +71,7 @@ async with app_container.build_child_container(scope=Scope.REQUEST) as request_c Use `async with` only when the scope holds providers with async finalizers; otherwise plain `with` is enough. Resolution itself is always synchronous. -**Framework-managed.** The [framework integrations](../integrations/fastapi.md) build the per-request child container for each request (or per-message for brokers) and tear it down at the end. You only declare `scope=Scope.REQUEST` on the providers that need it. +Otherwise, the [framework integrations](../integrations/fastapi.md) build the per-request child container for each request (or per-message for brokers) and tear it down at the end. You only declare `scope=Scope.REQUEST` on the providers that need it. ## Resolving across scopes @@ -155,6 +154,6 @@ A group declared without a `scope=` kwarg stamps nothing, so a provider listed o ## See also -- [Lifecycle](lifecycle.md) — finalizers and `close_async()` work per-scope. -- [Container provider](container.md) — injecting the active container into a creator. -- [Async resources via lifespan](../recipes/async-lifespan.md) — pattern for APP-scoped async setup. +- [Lifecycle](lifecycle.md): finalizers and `close_async()` work per-scope. +- [Container provider](container.md): injecting the active container into a creator. +- [Async resources via lifespan](../recipes/async-lifespan.md): pattern for APP-scoped async setup. diff --git a/docs/recipes/async-lifespan.md b/docs/recipes/async-lifespan.md index 1dbe4fca..bafab2d5 100644 --- a/docs/recipes/async-lifespan.md +++ b/docs/recipes/async-lifespan.md @@ -1,6 +1,6 @@ # Async resources via lifespan -**Problem.** A resource genuinely needs an `await` (or a running event loop) to construct: `aiohttp.ClientSession`, an `asyncpg` connection pool, an authenticated client whose construction does a token exchange. `modern-di` resolves synchronously, so the construction has to happen outside the resolve path. +Some resources need an `await` (or a running event loop) to construct, such as `aiohttp.ClientSession`, an `asyncpg` connection pool, an authenticated client whose construction does a token exchange. `modern-di` resolves synchronously, so the construction has to happen outside the resolve path. ## Solution @@ -62,11 +62,11 @@ lifespan. ## When a sync creator works instead -Many "async" resources actually construct synchronously: `redis.asyncio.Redis.from_url(...)`, `sqlalchemy.ext.asyncio.create_async_engine(...)`, and `httpx.AsyncClient(...)` all return without awaiting. For those, prefer a normal `Factory` with `cache=CacheSettings(finalizer=async_close_fn)` and skip the lifespan + `set_context` dance entirely. Use this recipe only when construction genuinely needs `await` or a running event loop. +Many "async" resources actually construct synchronously: `redis.asyncio.Redis.from_url(...)`, `sqlalchemy.ext.asyncio.create_async_engine(...)`, and `httpx.AsyncClient(...)` all return without awaiting. For those, prefer a normal `Factory` with `cache=CacheSettings(finalizer=async_close_fn)` and skip the lifespan + `set_context` dance entirely. Use this recipe only when construction needs `await` or a running event loop. ## See also -- [Lifecycle](../providers/lifecycle.md) — `close_async()` and finalizers. -- [Context providers](../providers/context.md) — `ContextProvider` and `set_context` in depth. -- [Scopes](../providers/scopes.md) — APP vs SESSION vs REQUEST. -- [Async SQLAlchemy recipe](sqlalchemy.md) — the sync-creator-with-async-finalizer pattern for comparison. +- [Lifecycle](../providers/lifecycle.md): `close_async()` and finalizers. +- [Context providers](../providers/context.md): `ContextProvider` and `set_context` in depth. +- [Scopes](../providers/scopes.md): APP vs SESSION vs REQUEST. +- [Async SQLAlchemy recipe](sqlalchemy.md): the sync-creator-with-async-finalizer pattern for comparison. diff --git a/docs/recipes/good-and-bad-practices.md b/docs/recipes/good-and-bad-practices.md index 73de1413..acd6e2f1 100644 --- a/docs/recipes/good-and-bad-practices.md +++ b/docs/recipes/good-and-bad-practices.md @@ -19,7 +19,7 @@ class Dependencies(Group): user_cache = providers.Factory(UserCache, scope=Scope.REQUEST) ``` -**Caught by:** an explicit `container.validate()` call, which raises `ValidationFailedError` +An explicit `container.validate()` call catches this: it raises `ValidationFailedError` carrying an `InvalidScopeDependencyError` for this exact graph before anything is ever resolved. See [Scope chain violation](../troubleshooting/scope-chain.md). Nothing validates automatically, so if the graph is never validated, the runtime failure is a `ScopeNotInitializedError`/`ScopeSkippedError` @@ -31,8 +31,8 @@ than at startup. Prefer catching it statically with an explicit `validate()` cal `validate()` is the only thing that checks the *whole* graph (cycles, inverted scopes, and missing dependencies). Nothing calls it for you: not construction, not `open()`, not `add_providers`, not -`resolve()`. Skipping it doesn't remove the bugs, it just delays finding them to whichever resolve -happens to hit one first. +`resolve()`. Skipping it leaves the bugs in place until whichever resolve happens to hit one +first. ```python # Broken: never validated, so wiring bugs surface one at a time, in production, on whatever request trips them @@ -43,7 +43,7 @@ container = Container(groups=[Dependencies]) container.validate() # raises ValidationFailedError here if the graph is broken ``` -**Caught by:** an explicit `container.validate()` call. It is the only thing that finds every issue +An explicit `container.validate()` call catches this. It is the only thing that finds every issue in the graph up front; without it, each wiring bug surfaces individually, at whichever resolve first reaches it. `Container(validate=...)` is deprecated and does nothing (see [Migration: To 3.x](../migration/to-3.x.md#4-validate-runs-at-container-entry-on-by-default)). An unvalidated cyclic @@ -52,7 +52,7 @@ graph still isn't a silent hang; see ## 3. A cached factory resolved before `set_context` -Context values are read live on every resolve of a **non-cached** factory. A **cached** factory is +Context values are read live on every resolve of a non-cached factory. A cached factory is built once, and a later `set_context` does not rebuild it. ```python @@ -70,7 +70,7 @@ If a request container resolves `tenant_config` before the real tenant ID is kno setup), the cached version keeps serving that first value for the rest of the request even after `request.set_context(str, real_tenant_id)` runs. Either drop `cache=True` for anything whose correctness depends on context set later, or make sure `set_context` runs before the first resolve. -**Caught by:** nothing automatic. This is a timing bug, not a wiring bug, so `validate()` cannot +Nothing catches this automatically. It is a timing bug, not a wiring bug, so `validate()` cannot see it. See [Context propagation](../providers/context.md#context-propagation) for how `set_context` timing interacts with a provider's scope, and [Lifecycle](../providers/lifecycle.md) for caching. @@ -92,7 +92,7 @@ def create_api_key(settings: Settings) -> str: return settings.api_key ``` -**Caught by:** nothing enforces this; it's a style discipline, not a validation rule. Reserve +Nothing enforces this; it is a matter of style discipline. Reserve `container_provider` for cases that are actually about the container (building a child container, introspecting the current scope), and declare everything else as a typed parameter so `validate()` and [Resolving dependencies](../introduction/resolving.md) can see it. @@ -119,8 +119,8 @@ def frozen_clock() -> Mock: container.reset_override(Dependencies.clock) ``` -**Caught by:** nothing automatic mid-suite. `reset_override(provider)` (or `reset_override()` with no -arguments, to clear everything) is the fix, and closing the **root** container clears every override +Nothing catches this automatically mid-suite. `reset_override(provider)` (or `reset_override()` with no +arguments, to clear everything) is the fix, and closing the root container clears every override in the shared registry as a last resort. See [Testing with overrides](testing-overrides.md#pitfalls). @@ -143,12 +143,12 @@ providers.Factory( ) ``` -**Caught by:** a `UserWarning` at declaration time. It's easy to miss in test output, so treat it as -a signal to add `bound_type=`, not as noise to ignore. +A `UserWarning` at declaration time catches this. It's easy to miss in test output, so treat it +as a signal to add `bound_type=`. ## See also -- [Errors and exceptions](../providers/errors-and-exceptions.md) — the full catalog this page draws +- [Errors and exceptions](../providers/errors-and-exceptions.md): the full catalog this page draws its mechanisms from. -- [Testing with overrides](testing-overrides.md) — the full override lifecycle. -- [Lifecycle](../providers/lifecycle.md) — caching, finalizers, and `validate()`. +- [Testing with overrides](testing-overrides.md): the full override lifecycle. +- [Lifecycle](../providers/lifecycle.md): caching, finalizers, and `validate()`. diff --git a/docs/recipes/multi-group.md b/docs/recipes/multi-group.md index 20e33e30..72ac84aa 100644 --- a/docs/recipes/multi-group.md +++ b/docs/recipes/multi-group.md @@ -1,6 +1,6 @@ # Organize a large container with multiple Groups -**Problem.** Your application has 30+ providers and stuffing them all into one `Group` is unreadable. +Your application has 30+ providers, and one `Group` holding all of them is unreadable. ## Solution @@ -103,5 +103,5 @@ See the [Litestar integration](../integrations/litestar.md) for the full pattern ## See also - [Factories](../providers/factories.md), [Scopes](../providers/scopes.md). -- [Litestar integration](../integrations/litestar.md) — `autowired_groups`. -- [Async SQLAlchemy recipe](sqlalchemy.md) — the building blocks for the `Database` group above. +- [Litestar integration](../integrations/litestar.md): `autowired_groups`. +- [Async SQLAlchemy recipe](sqlalchemy.md): the building blocks for the `Database` group above. diff --git a/docs/recipes/request-scoped-engine.md b/docs/recipes/request-scoped-engine.md index 272ec935..7b7cc027 100644 --- a/docs/recipes/request-scoped-engine.md +++ b/docs/recipes/request-scoped-engine.md @@ -1,8 +1,8 @@ # Request-scoped engine selection (read replicas) -> **Advanced.** Use this only if you have actual read-replica traffic to route. For a single-database setup, the [Async SQLAlchemy recipe](sqlalchemy.md) is what you want. +> This is an advanced recipe. Use it only if you have actual read-replica traffic to route. For a single-database setup, use the [Async SQLAlchemy recipe](sqlalchemy.md). -**Problem.** Route read-only requests (`GET`, `HEAD`) to a read-replica engine and mutating requests to the primary, without changing handler code. +This recipe routes read-only requests (`GET`, `HEAD`) to a read-replica engine and mutating requests to the primary, without changing handler code. ## Solution @@ -79,11 +79,11 @@ Why the `PrimaryEngine` / `ReplicaEngine` subclasses: type-based resolution need - The choice factory must be REQUEST-scoped. It depends on the per-request `Request` object. An APP-scoped factory cannot consume request-scoped data and `container.validate()` will reject it. - The framework integration provides `fastapi.Request` (or `litestar.Request`) automatically. No need to declare a `ContextProvider` for it. For Litestar, use `litestar.Request`. -- Don't apply this to per-connection pooling decisions. Engines (and their pools) are APP-scoped, so the choice you make per request just selects which long-lived pool the session checks out from. Trying to make the engine itself REQUEST-scoped would create and dispose a pool every request. +- Don't apply this to per-connection pooling decisions. Engines (and their pools) are APP-scoped, so the choice you make per request selects which long-lived pool the session checks out from. Trying to make the engine itself REQUEST-scoped would create and dispose a pool every request. - Watch for write-after-read in a single request. If a `GET` handler ends up doing a write (e.g. updating a `last_seen_at` field), it'll go to the replica and fail. Either move the side-effect out of the read path, or pick a different routing predicate than HTTP method. ## See also -- [Async SQLAlchemy recipe](sqlalchemy.md) — the simpler single-engine pattern. -- [Context providers](../providers/context.md) — how `Request` is injected. -- [Scopes](../providers/scopes.md) — why the engines are APP but the choice is REQUEST. +- [Async SQLAlchemy recipe](sqlalchemy.md): the simpler single-engine pattern. +- [Context providers](../providers/context.md): how `Request` is injected. +- [Scopes](../providers/scopes.md): why the engines are APP but the choice is REQUEST. diff --git a/docs/recipes/sqlalchemy.md b/docs/recipes/sqlalchemy.md index 491dacc4..395c2588 100644 --- a/docs/recipes/sqlalchemy.md +++ b/docs/recipes/sqlalchemy.md @@ -1,14 +1,14 @@ # Async SQLAlchemy: engine, session, repository -**Problem.** Wire `create_async_engine` + `AsyncSession` + repository classes through `modern-di` so the engine is shared process-wide, sessions are per-request, and cleanup happens automatically at shutdown and at the end of each request. +This recipe wires `create_async_engine` + `AsyncSession` + repository classes through `modern-di` so the engine is shared process-wide, sessions are per-request, and cleanup happens automatically at shutdown and at the end of each request. ## Solution -Three providers, three scopes: +The recipe uses three providers at two scopes: -- **Engine** at `Scope.APP`: one per process, cached, disposed at shutdown. -- **Session** at `Scope.REQUEST`: one per request, cached inside that request, closed at the end of the request. -- **Repositories** at `Scope.REQUEST`: depend on the session by type; one per request. +- The engine is at `Scope.APP`: one per process, cached, disposed at shutdown. +- The session is at `Scope.REQUEST`: one per request, cached inside that request, closed at the end of the request. +- Repositories are at `Scope.REQUEST` and depend on the session by type, one per request. ```python import sqlalchemy.ext.asyncio as sa_async @@ -88,7 +88,7 @@ The integration creates a REQUEST child container per request, so the session an ## See also -- [Lifecycle](../providers/lifecycle.md) — finalizers and `close_async()`. -- [Scopes](../providers/scopes.md) — why the engine is APP and sessions are REQUEST. +- [Lifecycle](../providers/lifecycle.md): finalizers and `close_async()`. +- [Scopes](../providers/scopes.md): why the engine is APP and sessions are REQUEST. - [Litestar integration](../integrations/litestar.md), [FastAPI integration](../integrations/fastapi.md). - Reference templates: [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template), [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template). diff --git a/docs/recipes/testing-overrides.md b/docs/recipes/testing-overrides.md index a64be5e0..d75d6f89 100644 --- a/docs/recipes/testing-overrides.md +++ b/docs/recipes/testing-overrides.md @@ -1,6 +1,6 @@ # Testing with overrides -**Problem.** Tests need to swap a real dependency (database, HTTP client, clock) for a fake one without touching production wiring. +Tests often need to swap a real dependency (database, HTTP client, clock) for a fake one without touching production wiring. ## Solution @@ -13,7 +13,7 @@ with container.override(MyGroup.api_client, mock_client) as client: The override applies at the `override()` call, not at `__enter__`. `__exit__` restores the snapshot taken at that call: a previously stacked override if there was one, otherwise no override. It does so even on exception, and even if `reset_override()` ran inside the block, or a root `close_sync()`/`close_async()` (which clears all overrides) did. Exit still restores the snapshot. Nested overrides of the same provider unwind in order: each handle restores whatever was active before it. Handles are expected to exit in reverse order of creation, which `with`-block nesting does naturally; manually exiting handles out of order can restore stale state. -`container.override(provider, replacement)` also works as a plain imperative call: reset with `container.reset_override(provider)` (or `container.reset_override()` to clear all). This pair remains fully supported (see the patterns below), and `close_sync`/`close_async` on the root container also clear all overrides automatically. Either way, the replacement is keyed by **provider reference** (not name) and is shared across the container tree, so an override on the root APP container applies to all child REQUEST containers too. +`container.override(provider, replacement)` also works as a plain imperative call: reset with `container.reset_override(provider)` (or `container.reset_override()` to clear all). This pair remains fully supported (see the patterns below), and `close_sync`/`close_async` on the root container also clear all overrides automatically. Either way, the replacement is keyed by provider reference (not name) and is shared across the container tree, so an override on the root APP container applies to all child REQUEST containers too. ## Pattern 1: Simple mock override @@ -108,5 +108,5 @@ Combine with `container.override(...)` in a setup fixture to swap underlying pro ## See also - [Pytest integration](../integrations/pytest.md). -- [Async SQLAlchemy recipe](sqlalchemy.md) — the engine/session/repository chain being overridden here. -- Reference template: [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template) — full transactional fixture setup. +- [Async SQLAlchemy recipe](sqlalchemy.md): the engine/session/repository chain being overridden here. +- Reference template: [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template) has the full transactional fixture setup. diff --git a/docs/troubleshooting/alias-source-not-registered-error.md b/docs/troubleshooting/alias-source-not-registered-error.md index bb8a5357..ec4f32fb 100644 --- a/docs/troubleshooting/alias-source-not-registered-error.md +++ b/docs/troubleshooting/alias-source-not-registered-error.md @@ -32,5 +32,5 @@ resolve. ## See also -- [Alias](../providers/alias.md) — binding one type to an already-registered provider. -- [No provider registered for type](missing-provider.md) — the same "unregistered type" problem, without an alias in the way. +- [Alias](../providers/alias.md) covers binding one type to an already-registered provider. +- [No provider registered for type](missing-provider.md) covers the same "unregistered type" problem, without an alias in the way. diff --git a/docs/troubleshooting/argument-resolution-error.md b/docs/troubleshooting/argument-resolution-error.md index 339ca922..68efe3d4 100644 --- a/docs/troubleshooting/argument-resolution-error.md +++ b/docs/troubleshooting/argument-resolution-error.md @@ -30,11 +30,11 @@ class Dependencies(Group): service2 = providers.Factory(Service, scope=Scope.APP, kwargs={"clock": clock}) ``` -**Integration-supplied context types.** If the missing type is one a framework +If the missing type is one a framework integration provides at runtime (`fastapi.Request`, `taskiq.TaskiqMessage`, …), its `ContextProvider` is registered by `setup_di()`, so a `container.validate()` call made *before* `setup_di()` runs sees no provider for it yet and raises. Either call -`validate()` **after** `setup_di()` (the provider is registered by then), or make the +`validate()` after `setup_di()` (the provider is registered by then), or make the parameter optional (`request: fastapi.Request | None = None`) so validation skips it regardless of ordering; the integration still injects the real value at runtime either way. See [Framework context objects](../providers/context.md#framework-context-objects). @@ -44,5 +44,5 @@ registered instead. ## See also -- [No provider registered for type](missing-provider.md) — the direct-resolve form of this same gap. -- [Factories](../providers/factories.md#creator) — how parameters are parsed and wired. +- [No provider registered for type](missing-provider.md) describes the direct-resolve form of this same gap. +- [Factories](../providers/factories.md#creator) explains how parameters are parsed and wired. diff --git a/docs/troubleshooting/child-container-registration-error.md b/docs/troubleshooting/child-container-registration-error.md index a78b01e2..27c79f9f 100644 --- a/docs/troubleshooting/child-container-registration-error.md +++ b/docs/troubleshooting/child-container-registration-error.md @@ -8,8 +8,7 @@ Raised from `Container.add_providers()`, naming the scope of the child container `add_providers()` was called on a child container rather than the root. The providers registry is shared tree-wide (every container in the chain points at the same registry), so registering from a -child would silently mutate every container in the tree. This is disallowed rather than done -implicitly. +child would silently mutate every container in the tree, so the call is disallowed. ## Fix diff --git a/docs/troubleshooting/circular-dependency.md b/docs/troubleshooting/circular-dependency.md index 74f403a0..78deaa69 100644 --- a/docs/troubleshooting/circular-dependency.md +++ b/docs/troubleshooting/circular-dependency.md @@ -29,8 +29,7 @@ resolve overflows the stack, and `Container.resolve_provider` catches that `Recu re-walks the static graph from the failing provider, and, since a cycle is reachable, raises `CircularDependencyError` (with the same cycle-path rendering shown above) `from` the original `RecursionError`. A creator that merely recurses on its own, with no actual cycle in the provider -graph, still raises the original `RecursionError` unchanged. Only a real static cycle gets -converted. This guard runs on every resolve, whether or not `validate()` was ever called. +graph, still raises the original `RecursionError` unchanged. This guard runs on every resolve, whether or not `validate()` was ever called. ### Cycle detection with `validate()` @@ -46,11 +45,11 @@ container.validate() # raises ValidationFailedError (wraps CircularDependencyEr ## Fix -1. **Break the cycle with an interface/protocol**: introduce an abstraction that one side depends on instead of the concrete type -2. **Use `kwargs` to inject one dependency manually**: pass a factory or value via `kwargs` instead of relying on automatic resolution -3. **Restructure your dependencies**: extract shared logic into a third provider that both can depend on without forming a cycle +1. Break the cycle by introducing an interface or protocol that one side depends on instead of the concrete type. +2. Inject one dependency manually by passing a factory or value via `kwargs` instead of relying on automatic resolution. +3. Restructure your dependencies by extracting shared logic into a third provider that both can depend on without forming a cycle. ## See also - [Errors and exceptions](../providers/errors-and-exceptions.md) -- [Lifecycle](../providers/lifecycle.md) — the validation section. +- [Lifecycle](../providers/lifecycle.md), the validation section. diff --git a/docs/troubleshooting/container-closed-error.md b/docs/troubleshooting/container-closed-error.md index 3c9c8cf5..c9a45ba7 100644 --- a/docs/troubleshooting/container-closed-error.md +++ b/docs/troubleshooting/container-closed-error.md @@ -1,6 +1,6 @@ # ContainerClosedError -**No longer raised.** As of modern-di 3.1, a container is usable immediately after construction, and +modern-di 3.1 and later do not raise this error. A container is usable immediately after construction, and there is no unopened state that raises. This page stays (every concrete `modern-di` error keeps a troubleshooting page) to document the class's back-compat status and the warning that replaced its failure mode. @@ -8,13 +8,13 @@ failure mode. ## Cause Through 3.0, resolving from (or building a child of) a container that had never been opened, or one -closed after use, raised `ContainerClosedError`. As of 3.1: +closed after use, raised `ContainerClosedError`. In 3.1 and later: -- A container is **open from construction**: `closed = False` the moment `Container(...)` returns, +- A container is open from construction: `closed = False` the moment `Container(...)` returns, with no `open()` step required and nothing to raise. `build_child_container()` never checks or touches any container's open/closed state (it only reads the parent's shared registries and scope map), and the child it returns starts open too, same as any freshly-constructed container. -- Reusing a container **after an explicit close** (`close_sync()`, `close_async()`, or exiting a +- Reusing a container after an explicit close (`close_sync()`, `close_async()`, or exiting a `with`/`async with` block) self-heals the moment the container is actually resolved from, either directly or through a descendant whose resolve reaches back into its scope: the container reopens and the call succeeds, but it first emits `ContainerClosedWarning`, a `RuntimeWarning` carrying @@ -26,7 +26,7 @@ closed after use, raised `ContainerClosedError`. As of 3.1: `ContainerClosedError` itself is kept importable for 3.x back-compat, so an `except exceptions.ContainerClosedError` clause does not break at import time, but nothing in the library -raises it anymore. It is removed in 4.0. +raises it. It is removed in 4.0. ## Fix @@ -34,11 +34,11 @@ Seeing it means a reference to an already-closed container was resolved from, ei through a child container whose resolve reached back into the closed container's scope, without going back through `open()`/`with` first. Two ways to respond: -- **Deliberate reuse** (e.g. a test harness or a callback-style lifecycle that closes and later - restarts the same container object): call `container.open()`, or re-enter it with `with`/`async +- If the reuse is deliberate (e.g. a test harness or a callback-style lifecycle that closes and later + restarts the same container object), call `container.open()`, or re-enter it with `with`/`async with`, before the next use. That reopens silently, with no warning, since a deliberate reopen is not diagnostic-worthy. -- **Unintentional reuse**: the warning is telling you a reference to the container is being held +- If the reuse is unintentional, the warning is telling you a reference to the container is being held past its lifetime (e.g. a request handler cached the container from a previous unit of work instead of fetching a fresh one). Find where that reference is coming from and fix the leak instead of silencing the warning. @@ -58,6 +58,6 @@ silently either way, since there is nothing to warn about there). ## See also -- [Migration: To 3.x](../migration/to-3.x.md#1-closed-containers-raise-instead-of-self-healing) — the +- [Migration: To 3.x](../migration/to-3.x.md#1-closed-containers-raise-instead-of-self-healing) covers the 3.0 behavior this page used to describe, and the 3.1 note relaxing it. - [Lifecycle: closing and reopening](../providers/lifecycle.md#closing-and-reopening). diff --git a/docs/troubleshooting/context-not-set.md b/docs/troubleshooting/context-not-set.md index b211e0c7..878e6d9e 100644 --- a/docs/troubleshooting/context-not-set.md +++ b/docs/troubleshooting/context-not-set.md @@ -1,6 +1,6 @@ # ContextProvider has no value -A `ContextProvider(SomeType)` resolves by looking up `SomeType` in the container's context registry. If no value was registered, the outcome depends on how the provider is consumed: resolving it directly raises `ContextValueNotSetError`, while injecting it into a `Factory` parameter that has no value raises `ArgumentResolutionError`, **unless** that parameter has a default (the default is used; `None` is not injected) or is nullable `X | None` (then `None` is injected). +A `ContextProvider(SomeType)` resolves by looking up `SomeType` in the container's context registry. If no value was registered, the outcome depends on how the provider is consumed: resolving it directly raises `ContextValueNotSetError`, while injecting it into a `Factory` parameter that has no value raises `ArgumentResolutionError`, unless that parameter has a default (the default is used; `None` is not injected) or is nullable `X | None` (then `None` is injected). ## Symptom @@ -26,7 +26,7 @@ app_container.set_context(TenantId, TenantId("acme")) # ignored for REQUEST- request_container = app_container.build_child_container(scope=Scope.REQUEST) ``` -Fix: set the value on the container whose scope matches the provider's scope: +To fix it, set the value on the container whose scope matches the provider's scope: ```python # Option A: pass directly to the child when building it @@ -44,16 +44,16 @@ request_container.set_context(TenantId, TenantId("acme")) `ContextProvider(TenantId, scope=Scope.APP)` looks up the value on the APP container. If you `set_context` on the REQUEST child container, the APP-scope provider doesn't see it. -Fix: match the scope. If the value is per-request, declare `ContextProvider(TenantId, scope=Scope.REQUEST)` and `set_context` on the request container (or pass via `build_child_container(context=...)`). +Make the scopes match. If the value is per-request, declare `ContextProvider(TenantId, scope=Scope.REQUEST)` and `set_context` on the request container (or pass via `build_child_container(context=...)`). ### 3. Framework integration didn't inject the expected request Framework integrations (`modern-di-fastapi`, `modern-di-litestar`) register the per-request `Request`/`WebSocket` automatically. If your code expects, say, `fastapi.Request` but you're outside the framework's request lifecycle (a background task, a CLI command), no `Request` is in context and the lookup fails. -Fix: only depend on framework-injected context inside the framework's request handling. For background tasks, build the REQUEST child container yourself and pass the necessary context. +Depend on framework-injected context only inside the framework's request handling. For background tasks, build the REQUEST child container yourself and pass the necessary context. ## See also -- [Context providers](../providers/context.md) — the full `ContextProvider` and `set_context` API. -- [Scopes](../providers/scopes.md) — per-container context registries, why context never propagates between containers. -- [Async resources via lifespan](../recipes/async-lifespan.md) — the canonical "construct in lifespan, inject as context" pattern. +- [Context providers](../providers/context.md) documents the full `ContextProvider` and `set_context` API. +- [Scopes](../providers/scopes.md) explains per-container context registries and why context never propagates between containers. +- [Async resources via lifespan](../recipes/async-lifespan.md) shows the canonical "construct in lifespan, inject as context" pattern. diff --git a/docs/troubleshooting/creator-call-error.md b/docs/troubleshooting/creator-call-error.md index 4f392bc9..17973c8e 100644 --- a/docs/troubleshooting/creator-call-error.md +++ b/docs/troubleshooting/creator-call-error.md @@ -8,10 +8,10 @@ check `kwargs` and `skip_creator_parsing` usage. ## Cause Argument binding failed when calling the creator: the set of arguments `modern-di` assembled (static -`kwargs` plus resolved dependencies) doesn't match the creator's signature: a required argument is +`kwargs` plus resolved dependencies) doesn't match the creator's signature. Either a required argument is missing, or an unexpected one was passed. This typically happens with `skip_creator_parsing=True` (where every required argument must be covered by `kwargs`) or a `kwargs` dict that drifted from the -signature. This is a **wiring problem, not a bug inside your constructor**. An exception raised +signature. This is a wiring problem, not a bug inside your constructor. An exception raised inside the creator's body (even a `TypeError`) propagates unchanged as itself, never wrapped in this error. @@ -43,5 +43,5 @@ the page below. ## See also -- [Unknown factory kwarg](unknown-factory-kwarg-error.md) — the declaration-time form of a kwargs mismatch. +- [Unknown factory kwarg](unknown-factory-kwarg-error.md) covers the declaration-time form of a kwargs mismatch. - [Factories: skip_creator_parsing](../providers/factories.md#skip_creator_parsing). diff --git a/docs/troubleshooting/duplicate-type-error.md b/docs/troubleshooting/duplicate-type-error.md index ba34be0b..6a949f31 100644 --- a/docs/troubleshooting/duplicate-type-error.md +++ b/docs/troubleshooting/duplicate-type-error.md @@ -75,7 +75,7 @@ class MyGroup(Group): ## See also -- [Factories](../providers/factories.md#bound_type) — the `bound_type` section. +- [Factories](../providers/factories.md#bound_type), the `bound_type` section. - [Errors and exceptions](../providers/errors-and-exceptions.md) - [Missing provider](../troubleshooting/missing-provider.md) diff --git a/docs/troubleshooting/finalizer-error.md b/docs/troubleshooting/finalizer-error.md index b3191ae5..84ac6aa0 100644 --- a/docs/troubleshooting/finalizer-error.md +++ b/docs/troubleshooting/finalizer-error.md @@ -8,8 +8,7 @@ during cleanup and whether the close was sync or async. ## Cause One or more cached providers' finalizers raised while the container was closing. Closing never stops -at the first failure: every finalizer runs regardless, so this error aggregates all of them rather -than surfacing just one. +at the first failure: every finalizer runs regardless, so this error aggregates every failure. ## Fix @@ -30,7 +29,7 @@ whether `close_sync()` or `close_async()` produced the error. ## Escape hatches If one entry in `.finalizer_errors` is an `AsyncFinalizerInSyncCloseError`, that specific resource's -cache was retained (not lost), and calling `await container.close_async()` afterward finalizes it +cache was retained, and calling `await container.close_async()` afterward finalizes it and completes cleanup. ## See also diff --git a/docs/troubleshooting/group-instantiation-error.md b/docs/troubleshooting/group-instantiation-error.md index c2513513..f47722cd 100644 --- a/docs/troubleshooting/group-instantiation-error.md +++ b/docs/troubleshooting/group-instantiation-error.md @@ -34,4 +34,4 @@ annotation or default value. ## See also -- [Multi-Group organization](../recipes/multi-group.md) — organizing providers across several `Group` classes. +- [Multi-Group organization](../recipes/multi-group.md) covers organizing providers across several `Group` classes. diff --git a/docs/troubleshooting/group-scope-conflict-error.md b/docs/troubleshooting/group-scope-conflict-error.md index ab959378..63268029 100644 --- a/docs/troubleshooting/group-scope-conflict-error.md +++ b/docs/troubleshooting/group-scope-conflict-error.md @@ -15,7 +15,7 @@ which one wins. ## Fix -Three ways to resolve it, pick whichever fits: +There are three ways to resolve it; pick whichever fits: ```python # 1. Set scope= explicitly on the shared provider — explicit always wins over a group default. @@ -41,4 +41,4 @@ the exception to see exactly which provider and groups collided. ## See also -- [Scopes](../providers/scopes.md) — the scope hierarchy and how a provider's scope is chosen. +- [Scopes](../providers/scopes.md) explains the scope hierarchy and how a provider's scope is chosen. diff --git a/docs/troubleshooting/invalid-child-scope-error.md b/docs/troubleshooting/invalid-child-scope-error.md index 9296dac7..21457109 100644 --- a/docs/troubleshooting/invalid-child-scope-error.md +++ b/docs/troubleshooting/invalid-child-scope-error.md @@ -36,4 +36,4 @@ caught exception for the exact list of valid choices at that point in the tree. ## See also -- [Scopes](../providers/scopes.md#the-scope-dependency-rule) — the scope hierarchy and ordering rule. +- [Scopes](../providers/scopes.md#the-scope-dependency-rule) explains the scope hierarchy and ordering rule. diff --git a/docs/troubleshooting/invalid-scope-type-error.md b/docs/troubleshooting/invalid-scope-type-error.md index 047acec7..c33276e7 100644 --- a/docs/troubleshooting/invalid-scope-type-error.md +++ b/docs/troubleshooting/invalid-scope-type-error.md @@ -33,4 +33,4 @@ values are ordered the way you want the hierarchy to resolve, and use that inste ## See also -- [Scopes](../providers/scopes.md) — the `IntEnum` hierarchy and why membership is required. +- [Scopes](../providers/scopes.md) explains the `IntEnum` hierarchy and why membership is required. diff --git a/docs/troubleshooting/max-scope-reached-error.md b/docs/troubleshooting/max-scope-reached-error.md index 851d2e49..8443bc16 100644 --- a/docs/troubleshooting/max-scope-reached-error.md +++ b/docs/troubleshooting/max-scope-reached-error.md @@ -33,4 +33,4 @@ Root containers rarely need this. Reconsider whether the provider actually needs ## See also -- [Scopes](../providers/scopes.md) — the built-in hierarchy and how to extend it with a custom `IntEnum`. +- [Scopes](../providers/scopes.md) explains the built-in hierarchy and how to extend it with a custom `IntEnum`. diff --git a/docs/troubleshooting/missing-provider.md b/docs/troubleshooting/missing-provider.md index b9607858..b634cc08 100644 --- a/docs/troubleshooting/missing-provider.md +++ b/docs/troubleshooting/missing-provider.md @@ -4,14 +4,14 @@ This error fires when a creator parameter is typed `Foo` and the container has n ## Symptom -**Direct miss.** Resolving an unregistered type directly: +Resolving an unregistered type directly raises: ``` ProviderNotRegisteredError: Provider of type is not registered in providers registry. See: https://modern-di.modern-python.org/troubleshooting/missing-provider/ ``` -**Nested miss.** A registered factory whose creator depends on an unregistered type: +Resolving a registered factory whose creator depends on an unregistered type raises: ``` ArgumentResolutionError: Cannot resolve dependency chain: @@ -20,7 +20,7 @@ ArgumentResolutionError: Cannot resolve dependency chain: See: https://modern-di.modern-python.org/troubleshooting/argument-resolution-error/ ``` -The resolver walked the creator's signature, found a parameter typed `MissingDep`, and looked it up in the providers registry. Nothing was there. The "dependency chain" header shows where in the resolution graph the miss occurred. +The resolver walked the creator's signature, found a parameter typed `MissingDep`, and found nothing for it in the providers registry. The "dependency chain" header shows where in the resolution graph the miss occurred. ## Cause @@ -50,22 +50,22 @@ def create_engine(...) -> sa_async.AsyncEngine: return sa_async.create_async_engine(...) ``` -Fix: add the return annotation, or set `bound_type=SomeType` on the provider explicitly. +To fix it, add the return annotation, or set `bound_type=SomeType` on the provider explicitly. ### 3. `bound_type=None` was set on the provider you want to resolve `bound_type=None` makes the provider unresolvable by type. It's a deliberate opt-out for cases where two providers return the same type (see [Duplicate provider type](duplicate-type-error.md)). If you set it on the wrong provider, the type lookup misses. -Fix: leave `bound_type` at its default on the provider you want resolvable by type. If both providers really do produce the same type, resolve the unresolvable one by reference (`container.resolve_provider(...)`). +Leave `bound_type` at its default on the provider you want resolvable by type. If both providers really do produce the same type, resolve the unresolvable one by reference (`container.resolve_provider(...)`). ### 4. The parameter is a union and the chosen branch isn't registered For `dep: A | B`, `modern-di` resolves the *first* type in the union order that has a registered provider. If neither is registered, the resolver fails. -Fix: register a provider for one of the union types, or annotate the parameter with a concrete type. +Register a provider for one of the union types, or annotate the parameter with a concrete type. ## See also -- [Resolving](../introduction/resolving.md) — the by-type lookup algorithm. -- [Duplicate provider type](duplicate-type-error.md) — the inverse problem, where two providers compete for the same type. -- [Factories: `bound_type`](../providers/factories.md) — how the bound type is inferred and how to override it. +- [Resolving](../introduction/resolving.md) describes the by-type lookup algorithm. +- [Duplicate provider type](duplicate-type-error.md) covers the inverse problem, where two providers compete for the same type. +- [Factories: `bound_type`](../providers/factories.md) explains how the bound type is inferred and how to override it. diff --git a/docs/troubleshooting/provider-scope-frozen-error.md b/docs/troubleshooting/provider-scope-frozen-error.md index e38c4f07..332a1ed4 100644 --- a/docs/troubleshooting/provider-scope-frozen-error.md +++ b/docs/troubleshooting/provider-scope-frozen-error.md @@ -9,15 +9,15 @@ already registered with a container. ## Cause A provider created without an explicit `scope=` takes its scope from whichever -`class ...(Group, scope=...)` body stamps it first. A group declared **without** a `scope=` kwarg +`class ...(Group, scope=...)` body stamps it first. A group declared without a `scope=` kwarg stamps nothing, so a provider listed only in such a group keeps the `Scope.APP` default and stays -unclaimed, so a later group is still free to stamp it. +unclaimed, which leaves a later group free to stamp it. That is fine until the provider has been registered with a container. Registration compiles a -resolver for the provider, and that resolver **captures the scope as it was at compile time**. +resolver for the provider, and that resolver captures the scope as it was at compile time. Changing the scope afterwards would apply only to resolvers compiled later, so the same provider -would resolve one way through the existing container and another way through a fresh one. Rather -than let the two disagree silently, the scope is frozen at registration and the change is rejected. +would resolve one way through the existing container and another way through a fresh one. To keep +the two from disagreeing silently, the scope is frozen at registration and the change is rejected. ```python shared = providers.Factory(SomeService) # no explicit scope -> APP default, unclaimed @@ -62,5 +62,5 @@ group would change the scope of a provider that a container has already compiled ## See also -- [Scopes](../providers/scopes.md) — the scope hierarchy and how a provider's scope is chosen. -- [GroupScopeConflictError](group-scope-conflict-error.md) — two groups disagreeing about a scope. +- [Scopes](../providers/scopes.md) explains the scope hierarchy and how a provider's scope is chosen. +- [GroupScopeConflictError](group-scope-conflict-error.md) covers two groups disagreeing about a scope. diff --git a/docs/troubleshooting/scope-chain.md b/docs/troubleshooting/scope-chain.md index b9243d3a..9aee4129 100644 --- a/docs/troubleshooting/scope-chain.md +++ b/docs/troubleshooting/scope-chain.md @@ -16,7 +16,7 @@ InvalidScopeDependencyError (1): caused by: UserCache (scope APP) declares parameter 'session' typed as a provider of Session at deeper scope REQUEST. A provider cannot depend on a deeper-scoped provider. ``` -The fix is always to make the depender's scope equal to or shorter than the dependee's. In the example above, `UserCache` should be REQUEST-scoped, not APP-scoped. +The fix is always to make the depender's scope equal to or shorter than the dependee's. In the example above, `UserCache` is APP-scoped and should be REQUEST-scoped. The chain is the same arrow tree `ScopeNotInitializedError` and `ScopeSkippedError` draw at runtime, so the same violation reads identically whether you find it with `validate()` or by resolving. @@ -37,9 +37,9 @@ InvalidScopeDependencyError (1): ## Cause -1. **Forgot `scope=Scope.REQUEST` on a repository.** Defaults to `Scope.APP` if omitted. A repository that holds a session needs `scope=Scope.REQUEST`. -2. **Helper or utility provider auto-defaulted to APP.** Same as above: anything that consumes the session is REQUEST-scoped. -3. **Choice factory consuming the request.** A factory that depends on the framework's `Request` is REQUEST-scoped; you cannot resolve it from the APP container. +1. A repository is missing `scope=Scope.REQUEST`. The scope defaults to `Scope.APP` if omitted, and a repository that holds a session needs `scope=Scope.REQUEST`. +2. A helper or utility provider auto-defaulted to APP. As above, anything that consumes the session is REQUEST-scoped. +3. A choice factory consumes the request. A factory that depends on the framework's `Request` is REQUEST-scoped; you cannot resolve it from the APP container. ## How to detect @@ -70,5 +70,5 @@ class Dependencies(Group): ## See also -- [Scopes](../providers/scopes.md#the-scope-dependency-rule) — the lifetime model and the "max of dependencies' scopes" rule. -- [Lifecycle](../providers/lifecycle.md) — `container.validate()` and other startup checks. +- [Scopes](../providers/scopes.md#the-scope-dependency-rule) explains the lifetime model and the "max of dependencies' scopes" rule. +- [Lifecycle](../providers/lifecycle.md) covers `container.validate()` and other startup checks. diff --git a/docs/troubleshooting/scope-not-initialized-error.md b/docs/troubleshooting/scope-not-initialized-error.md index f44a78b5..bda3c3bd 100644 --- a/docs/troubleshooting/scope-not-initialized-error.md +++ b/docs/troubleshooting/scope-not-initialized-error.md @@ -35,5 +35,5 @@ dependency rule below, which `validate()` catches ahead of time as `InvalidScope ## See also -- [Scope chain violation](scope-chain.md) — the related, statically-detected form of this problem. +- [Scope chain violation](scope-chain.md) covers the related, statically-detected form of this problem. - [Scopes: the scope dependency rule](../providers/scopes.md#the-scope-dependency-rule). diff --git a/docs/troubleshooting/scope-skipped-error.md b/docs/troubleshooting/scope-skipped-error.md index 657e4678..c0faba8a 100644 --- a/docs/troubleshooting/scope-skipped-error.md +++ b/docs/troubleshooting/scope-skipped-error.md @@ -16,8 +16,7 @@ The container chain skipped an intermediate scope when it was built. For example ## Fix -Build child containers through every intermediate scope your providers need, rather than jumping -straight to a deep one: +Build child containers through every intermediate scope your providers need: ```python app_container = Container(scope=Scope.APP, groups=[MyGroup]) @@ -37,5 +36,5 @@ request/message and align your providers to those, not to the full built-in hier ## See also -- [Scope chain violation](scope-chain.md) — the related, statically-detected form of this problem. -- [Scopes](../providers/scopes.md) — how container chains map to the scope hierarchy. +- [Scope chain violation](scope-chain.md) covers the related, statically-detected form of this problem. +- [Scopes](../providers/scopes.md) explains how container chains map to the scope hierarchy. diff --git a/docs/troubleshooting/validation-failed-error.md b/docs/troubleshooting/validation-failed-error.md index 2c9024d2..18950ea5 100644 --- a/docs/troubleshooting/validation-failed-error.md +++ b/docs/troubleshooting/validation-failed-error.md @@ -9,7 +9,7 @@ error class name, with the count of each kind and every individual issue indente The provider graph has one or more problems: a circular dependency, a provider depending on a deeper-scoped one, a creator parameter with no way to be resolved, or an alias whose source type has -no registered provider. `validate()` collects **every** +no registered provider. `validate()` collects every issue across the whole graph in one pass rather than stopping at the first one, so `.errors` (a `list[Exception]`) may hold several distinct exception types at once. @@ -35,4 +35,4 @@ construction, not `open()`, not `resolve()`. ## See also - [Lifecycle: validation](../providers/lifecycle.md#validation). -- [Circular dependency](circular-dependency.md), [Scope chain violation](scope-chain.md), [Argument resolution error](argument-resolution-error.md), [Alias source not registered](alias-source-not-registered-error.md) — the underlying issue kinds. +- The underlying issue kinds: [Circular dependency](circular-dependency.md), [Scope chain violation](scope-chain.md), [Argument resolution error](argument-resolution-error.md), [Alias source not registered](alias-source-not-registered-error.md). From 3d2a3ec8c99cbca9f73f6a7e95263a1d5fcaf328 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 3 Oct 2026 17:31:41 +0300 Subject: [PATCH 2/2] docs: restore qualifiers dropped in the prose pass --- docs/introduction/design-decisions.md | 2 +- docs/introduction/for-fastapi-users.md | 2 +- docs/introduction/performance.md | 2 +- docs/providers/scopes.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/introduction/design-decisions.md b/docs/introduction/design-decisions.md index ba57cef0..bf3f9c55 100644 --- a/docs/introduction/design-decisions.md +++ b/docs/introduction/design-decisions.md @@ -4,7 +4,7 @@ ## 1. Resolution is sync-only; finalizers may be sync or async -Since 2.x, `Container.resolve(...)` and `resolve_provider(...)` are synchronous. There is no `await container.resolve(...)`, no `AsyncFactory`, no `AsyncSingleton`. Async work belongs in the framework's lifespan and per-request hooks; the container holds the already-constructed objects (see [Async resources via lifespan](../recipes/async-lifespan.md)). Teardown is separate from resolution: finalizers may be sync or async (`close_sync` / `close_async`). +Since 2.x, `Container.resolve(...)` and `resolve_provider(...)` are synchronous. There is no `await container.resolve(...)`, no `AsyncFactory`, no `AsyncSingleton`. Async work belongs in the framework's lifespan and per-request hooks; the container holds the already-constructed objects (see [Async resources via lifespan](../recipes/async-lifespan.md)). Teardown is separate from resolution: finalizers may be sync or async (`close_sync` / `close_async`), so async cleanup is fully supported. Async resolution will not be added. diff --git a/docs/introduction/for-fastapi-users.md b/docs/introduction/for-fastapi-users.md index 2a6b6d05..da5a048f 100644 --- a/docs/introduction/for-fastapi-users.md +++ b/docs/introduction/for-fastapi-users.md @@ -62,7 +62,7 @@ class Dependencies(Group): This is the modern-di equivalent of a FastAPI `yield`-dependency that hands out one session per request and closes it afterward. The cleanup runs as the container's finalizer instead of code -after `yield`. +after `yield`, and `Scope.REQUEST` names how long the session lives, not when it is torn down. ## See also diff --git a/docs/introduction/performance.md b/docs/introduction/performance.md index 927e8774..2ff1663e 100644 --- a/docs/introduction/performance.md +++ b/docs/introduction/performance.md @@ -210,7 +210,7 @@ own way: modern-di seeds a child container's context and resolves by reference; placeholder factory; that-depends supplies it through `container_context(global_context=)`; and dependency-injector injects by reference via `providers.Dependency` + `.override()`, a structural analog rather than an equivalent. modern-di's timed body builds the child, resolves, and closes -it. It calls no `open()`: a freshly built child is already open, so timing one would +it. It calls no `open()`: a freshly built child is already open as of 3.1, so timing one would charge modern-di a redundant lock acquire (81 ns, ~6% of the cell) with no counterpart in any rival's body. It does close, because all four rivals exit their scope inside the timed body; that teardown is ~110 ns, and omitting it would have flattered modern-di by more than the `open()` diff --git a/docs/providers/scopes.md b/docs/providers/scopes.md index 0637fbc0..73cfd302 100644 --- a/docs/providers/scopes.md +++ b/docs/providers/scopes.md @@ -71,7 +71,7 @@ async with app_container.build_child_container(scope=Scope.REQUEST) as request_c Use `async with` only when the scope holds providers with async finalizers; otherwise plain `with` is enough. Resolution itself is always synchronous. -Otherwise, the [framework integrations](../integrations/fastapi.md) build the per-request child container for each request (or per-message for brokers) and tear it down at the end. You only declare `scope=Scope.REQUEST` on the providers that need it. +If you use a [framework integration](../integrations/fastapi.md), it builds the per-request child container for each request (or per-message for brokers) and tears it down at the end. You only declare `scope=Scope.REQUEST` on the providers that need it. ## Resolving across scopes