diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md index c2994e00..717693a4 100644 --- a/docs/dev/contributing.md +++ b/docs/dev/contributing.md @@ -1,28 +1,61 @@ # Contributing + This is an open source project, and we are open to new contributors. ## Getting started -1. Make sure that you have [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/) installed. + +1. Install [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/). modern-di supports + Python 3.11 and later. 2. Clone the project: -``` -git clone git@github.com:modern-python/modern-di.git # or: git clone https://github.com/modern-python/modern-di.git -cd modern-di -``` -3. Install dependencies by running `just install` + + ```bash + git clone https://github.com/modern-python/modern-di.git + cd modern-di + ``` + +3. Run `just install`. It runs `uv lock --upgrade`, then syncs every extra and the lint group, so + you lint with the newest `ruff` and `ty`, as CI does. `uv.lock` is gitignored. Run it again in + every new checkout or worktree before `just lint`. ## Running linters -`Ruff` and `ty` do the static analysis. -Run all checks with `just lint`. +`ruff` and `ty` do the static analysis. Run all checks with `just lint`, which also applies +`ruff`'s fixes and formatting. ## Running tests + Run all tests with `just test`. Run a subset with `just test -k `. -CI runs the coverage-enforcing recipe `just test-ci` along with `just lint-ci`. +- `just test-ci` is the full run with coverage, and fails below 100% line and branch coverage of + `modern_di`. +- `just test-race` runs only the thread-race tests. CI repeats them on free-threaded Python builds. +- `just bench` runs the guard-tier benchmarks, and `just bench-compare` the cross-framework ones. + +## Working on the docs + +The site is built from `docs/` with MkDocs. Before you open a pull request that touches it: + +- `just test-docs` runs every Python block under `docs/` as a test, with Sybil. The framework + pages under `docs/integrations/` are skipped there. +- `just test-docs-integrations` runs every page, the framework pages included, with each + integration's latest release installed. It builds a throwaway virtualenv. +- `just docs-build` is the strict MkDocs build. Any warning fails it. + +The writing rules are in `docs/agents/docs-style.md`. + +## What CI runs + +Every pull request runs `just lint-ci` and `just adr-check`, `just test-ci` and `just test-docs` on +Python 3.11 to 3.14 (3.15 as a pre-release), `just test-race --count=50` on the free-threaded +builds, `just test-docs-integrations`, `just docs-build`, and an offline link check of every +Markdown file. ## Submitting changes + 1. Fork the repo and branch off `main`. -2. Make your change with tests; keep **100% line and branch coverage** of `modern_di` (CI runs `just test-ci`, which fails below the `report.fail_under = 100` gate in `pyproject.toml`). -3. Run `just lint` and `just test` locally before pushing (CI runs the non-fixing variants `just lint-ci` / `just test-ci`). -4. For non-trivial changes, the PR body is the spec; the pull-request template walks you through it (why, design, non-goals, verification). +2. Make your change with tests, and keep 100% line and branch coverage of `modern_di`. The gate is + `report.fail_under = 100` in `pyproject.toml`. +3. Run `just lint` and `just test` locally before pushing. +4. For a non-trivial change, the pull-request body is the spec. The template has Summary, Changes + and Checklist sections. 5. Open a pull request upstream. diff --git a/docs/dev/performance-history.md b/docs/dev/performance-history.md index bbadddc4..4e9b3533 100644 --- a/docs/dev/performance-history.md +++ b/docs/dev/performance-history.md @@ -84,9 +84,9 @@ builds a container per test would pay that in every test. [ADR 0001](https://github.com/modern-python/modern-di/blob/main/docs/adr/0001-resolver-hot-path-generated-source.md) records the decision. -## After 3.5: the request path +## 3.6: the request path -Three smaller changes moved the per-request path: +Three smaller changes in 3.6.0 moved the per-request path: - [#540](https://github.com/modern-python/modern-di/issues/540) inlined the cross-scope hop. The generated resolver reads the container's ancestor map itself instead of calling @@ -184,8 +184,9 @@ did not move then, because on G7 #585 measured −2.9% and #597 +4.6%. so the change in that ratio is modern-di's. The two publications after 4.0.0 (`ab327b3` and `8845e6a`) measured the 4.0.0 library plus -[#631](https://github.com/modern-python/modern-di/issues/631), which adds checks at provider -definition and runs nothing during a resolve, against that-depends 4.2.0 and wireup 2.12.1. Every -ratio stayed within 0.03 of 4.0.0 except dependency-injector's C2 (2.20 to 2.13, then 2.14) and, -at `8845e6a`, dishka's C4 (0.88 to 0.84). No cell crossed 1.0. modern-di's C4 fell from 1.85 to 1.80 µs at `ab327b3` and 1.74 µs at -`8845e6a`; #631 adds nothing to that path, and neither run explains the drop. +[#631](https://github.com/modern-python/modern-di/issues/631), which shipped in 4.1.0. It adds +checks at provider definition and runs nothing during a resolve. Both ran against that-depends 4.2.0 +and wireup 2.12.1. Every ratio stayed within 0.03 of 4.0.0 except dependency-injector's C2 (2.20 to +2.13, then 2.14) and, at `8845e6a`, dishka's C4 (0.88 to 0.84). No cell crossed 1.0. modern-di's C4 +fell from 1.85 to 1.80 µs at `ab327b3` and 1.74 µs at `8845e6a`; #631 adds nothing to that path, and +neither run explains the drop. diff --git a/docs/migration/from-dependency-injector.md b/docs/migration/from-dependency-injector.md index 5945ed4f..2c4518a5 100644 --- a/docs/migration/from-dependency-injector.md +++ b/docs/migration/from-dependency-injector.md @@ -1,6 +1,6 @@ # Migration from `dependency-injector` -This guide walks an existing [`dependency-injector`](https://github.com/ets-labs/python-dependency-injector) codebase (~4.9k GitHub stars, the largest Python DI user base) through the move to `modern-di`. Every provider type documented in `dependency-injector`'s [provider catalog](https://python-dependency-injector.ets-labs.org/providers/index.html) has either a mapping below or an explicit note that there is no direct equivalent (with a workaround), following the same rule as [the `that-depends` migration guide](from-that-depends.md), the in-house template for this page. +This guide walks an existing [`dependency-injector`](https://github.com/ets-labs/python-dependency-injector) codebase through the move to `modern-di`. Every provider class you declare in `dependency-injector` 4.x (checked against 4.49.1) has either a mapping below or a note that there is no direct equivalent, with a workaround. The base classes, and the helper classes behind `.provided` and `Configuration`, are covered by those rows. The [provider catalog](https://python-dependency-injector.ets-labs.org/providers/index.html) describes what each one does. [The `that-depends` migration guide](from-that-depends.md) follows the same layout. ## 1. Install @@ -24,28 +24,28 @@ Core package: poetry add modern-di ``` -Framework integrations and the pytest helper live in separate packages. Install only what you need: +Each framework integration, and the pytest helper, is a separate package named `modern-di-`. Install the ones you need: === "uv" ```bash + uv add modern-di-flask # Flask + uv add modern-di-aiohttp # aiohttp uv add modern-di-fastapi # FastAPI - uv add modern-di-litestar # Litestar - uv add modern-di-faststream # FastStream - uv add modern-di-typer # Typer - uv add modern-di-pytest # pytest fixtures + uv add --dev modern-di-pytest # pytest fixtures ``` === "pip" ```bash + pip install modern-di-flask + pip install modern-di-aiohttp pip install modern-di-fastapi - pip install modern-di-litestar - pip install modern-di-faststream - pip install modern-di-typer pip install modern-di-pytest ``` +The full list: [aiogram](../integrations/aiogram.md), [aiohttp](../integrations/aiohttp.md), [arq](../integrations/arq.md), [Celery](../integrations/celery.md), [FastAPI](../integrations/fastapi.md), [FastMCP](../integrations/fastmcp.md), [FastStream](../integrations/faststream.md), [Flask](../integrations/flask.md), [gRPC](../integrations/grpc.md), [Litestar](../integrations/litestar.md), [Starlette](../integrations/starlette.md), [taskiq](../integrations/taskiq.md) and [Typer](../integrations/typer.md). + ## 2. Key conceptual shifts Three things change in how you think about the framework. Most migration confusion comes from these: @@ -56,7 +56,7 @@ Three things change in how you think about the framework. Most migration confusi ## 3. Provider taxonomy -Use this table as the index for the rest of the guide. Every provider class documented in `dependency-injector`'s live docs is listed; "no direct equivalent" rows link to [§11](#11-no-direct-equivalent) for the workaround. +Use this table as the index for the rest of the guide. "No direct equivalent" rows link to [§11](#11-no-direct-equivalent) for the workaround. | `dependency-injector` | `modern-di` replacement | Where to look | |---|---|---| @@ -64,36 +64,42 @@ Use this table as the index for the rest of the guide. Every provider class docu | `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 always lock-guarded | [§4](#4-migrate-the-dependency-graph) | -| `ThreadLocalSingleton` | No direct equivalent; see [§11](#11-no-direct-equivalent) | [§11](#11-no-direct-equivalent) | +| `ThreadLocalSingleton` | No direct equivalent | [§11](#11-no-direct-equivalent) | +| `ContextLocalSingleton` | `providers.Factory(..., scope=Scope.REQUEST, cache=True)` resolved from a per-request child container | [§4](#4-migrate-the-dependency-graph) | | `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) | +| `Resource` (async initializer) | Lifespan + `ContextProvider` (or sync creator + async finalizer) | [Async resources via lifespan](../recipes/async-lifespan.md) | | `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, so do the `await` in the lifespan and inject the result, same as an async `Resource` | [§4](#4-migrate-the-dependency-graph) | +| `Coroutine` / `AbstractCoroutine` | No direct equivalent. Resolution is sync-only, so do the `await` in the lifespan and inject the result, same as an async `Resource` | [Async resources via lifespan](../recipes/async-lifespan.md) | | `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) | +| `Dependency` / `ExternalDependency` | `providers.ContextProvider(...)` | [§4](#4-migrate-the-dependency-graph) | +| `AbstractFactory` / `AbstractSingleton` / `AbstractCallable` | `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) | +| `Container` (a nested container) / `DependenciesContainer` | Pass every `Group` to one `Container(groups=[...])`; providers in one group depend on another group's by type | [§4](#4-migrate-the-dependency-graph) | +| `Self` | A creator parameter annotated `Container` | [§4](#4-migrate-the-dependency-graph) | +| `Delegate` and the `Delegated*` providers (`DelegatedFactory`, `DelegatedSingleton`, ...) | A creator parameter annotated `Container`, resolving the provider later | [§4](#4-migrate-the-dependency-graph) | +| `Selector` | No direct equivalent | [§11](#11-no-direct-equivalent) | +| `Aggregate` / `FactoryAggregate` | No direct equivalent | [§11](#11-no-direct-equivalent) | +| `.provided` (attribute / item / method-call access on a provider) | 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.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):` | [§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 1. Replace `DeclarativeContainer` with `Group`. -2. Add an explicit `scope=` to each provider (defaults to `Scope.APP`). +2. Give each provider that is not app-wide a `scope=`. A provider without one is `Scope.APP`. A group whose providers mostly share a scope can set a [group-level default](../providers/scopes.md#group-level-default-scope) with `class RequestGroup(Group, scope=Scope.REQUEST)`. 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. -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). +Nested containers and `DependenciesContainer` go away. Declare each part of the graph as its own `Group` and pass them all to one `Container(groups=[CoreGroup, BillingGroup])`. A provider in one group that needs a provider from another names its type as a creator parameter, so neither group refers to the other. + +Replace `Singleton` and `ThreadSafeSingleton` with `providers.Factory(SomeClass, cache=True)`. There is no separate thread-safe class, since `modern-di`'s cache is always lock-guarded. See [Cached factories](../providers/factories.md#cached-factories). 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: @@ -143,7 +149,9 @@ db_session = providers.Factory( ) ``` -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: +`ContextLocalSingleton` is the same lifetime without teardown: `providers.Factory(SomeClass, scope=Scope.REQUEST, cache=True)`. + +Replace `Callable` with a plain `Factory` whose creator is the callable. `modern-di` makes no distinction between wrapping a function and wrapping a class: `Factory`'s creator argument accepts any callable. The value a `dependency-injector` caller passes at call time (`"super secret"` below) has to become a static `kwargs` entry or a resolvable dependency; [§11](#11-no-direct-equivalent) covers values that vary per call: ```python # dependency-injector @@ -157,9 +165,7 @@ 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. - -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): +Replace `Object` with a `Factory` whose creator returns the value. Define a small typed function, because a lambda has no return annotation and so gets no type to resolve by: ```python # dependency-injector @@ -176,7 +182,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`. -Replace `List` and `Dict` with a `Factory` whose creator builds the collection: +Replace `List` and `Dict` with a `Factory` whose creator builds the collection, and pass that provider to its consumers through `kwargs`: ```python # dependency-injector @@ -190,8 +196,11 @@ def build_modules() -> list[Module]: return [Module("m1"), Module("m2")] modules = providers.Factory(build_modules) +registry = providers.Factory(ModuleRegistry, kwargs={"modules": modules}) ``` +A parameterized return type registers under its bare origin: `build_modules` is bound to `list`, not `list[Module]`, so `container.resolve(list[Module])` raises `ProviderNotRegisteredError`. A consumer parameter annotated `list[Module]` cannot be wired by type either, and `Factory(ModuleRegistry)` raises `UnsupportedCreatorParameterError` unless `kwargs` supplies it, as above. Two list-returning factories in one container both claim `list` and raise `DuplicateProviderTypeError`; give all but one `bound_type=None` and pass them by reference. + Replace `Dependency` with `ContextProvider`. Both are a typed placeholder filled in at runtime rather than constructed by a factory: ```python @@ -204,7 +213,9 @@ database = providers.ContextProvider(DbAdapter, scope=Scope.APP) # container = Container(groups=[AppGroup], context={DbAdapter: SqliteDbAdapter()}) ``` -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: +`validate()` does not check that a context value is set. Where calling an unset `Dependency` raises at that call, a `ContextProvider` with no value passes validation and raises `ContextValueNotSetError` when something resolves it. Give it `default=` if the value may be absent. + +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()` reports an alias whose source is not registered before the first resolve. `AbstractSingleton` and `AbstractCallable` map the same way, with `cache=True` on the concrete `Factory` for a singleton: ```python # dependency-injector @@ -216,6 +227,8 @@ redis_cache_client = providers.Factory(RedisCacheClient, cache=True) cache_client = providers.Alias(RedisCacheClient, bound_type=AbstractCacheClient) ``` +`Self`, `Delegate` and the `Delegated*` providers exist to hand a creator the container or a provider instead of a built object. In `modern-di`, a creator parameter annotated `Container` receives the container, and the creator resolves what it needs from it when it needs it. The container it gets is the one at the provider's own scope. See [Injecting the container itself](../providers/container.md#injecting-the-container-itself). A dependency resolved that way is hidden from `validate()`, so keep it for code that really needs the container, such as building a child container. + ## 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. 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: @@ -264,7 +277,7 @@ async def handler(service: Service = FromDI(Service)) -> None: More framework examples in [§8](#8-framework-integration-and-routes). -This also removes `dependency-injector`'s most-filed failure mode: an unwired function's marker is left as a raw `Provide` object, which surfaces as a confusing `AttributeError: 'Provide' object has no attribute ...` deep in your own code ([issue #658](https://github.com/ets-labs/python-dependency-injector/issues/658), [issue #521](https://github.com/ets-labs/python-dependency-injector/issues/521)) rather than a DI-specific error at the point of the mistake. `modern-di` fails at declaration time (`UnsupportedCreatorParameterError`) or resolve time (`ProviderNotRegisteredError`, with "did you mean" suggestions); see [§10](#10-diagnostics-comparison). +This also removes `dependency-injector`'s most-filed failure mode: an unwired function's marker is left as a raw `Provide` object, which surfaces as a confusing `AttributeError: 'Provide' object has no attribute ...` deep in your own code ([issue #658](https://github.com/ets-labs/python-dependency-injector/issues/658), [issue #521](https://github.com/ets-labs/python-dependency-injector/issues/521)) rather than a DI-specific error at the point of the mistake. In `modern-di` a missing dependency fails in `validate()`, or on the first resolve that needs it, with `ArgumentResolutionError` for a creator argument nothing can supply or `ProviderNotRegisteredError` for a type you resolve directly. When a type with a similar name is registered, the `ProviderNotRegisteredError` message suggests it. See [§10](#10-diagnostics-comparison). ## 7. Scopes @@ -286,7 +299,9 @@ See [the scope dependency rule](../providers/scopes.md#the-scope-dependency-rule ## 8. Framework integration and routes -Replace `container.wire(modules=[...])` (plus any per-framework glue such as `container` attributes on the app object) with the integration package's setup call ([FastAPI](../integrations/fastapi.md), [Litestar](../integrations/litestar.md), [FastStream](../integrations/faststream.md), [Typer](../integrations/typer.md)). That call creates per-request child containers, tears them down automatically, and calls `container.close_async()` at shutdown. There is no module list to maintain and no import-time patching. On routes, `FromDI(T)` replaces the `@inject` + `Provide[Container.x]` pair: resolution is by type, so no marker points at a specific container attribute and no `@inject` decorator is needed. See the integration pages for the full route examples. +Replace `container.wire(modules=[...])` (plus any per-framework glue such as `container` attributes on the app object) with the integration package's setup call, such as `setup_di(app, container)` for [Flask](../integrations/flask.md), [aiohttp](../integrations/aiohttp.md) or [FastAPI](../integrations/fastapi.md). There is no module list to maintain and no import-time patching. Integrations build a child container for each request or message and close it when the request ends. Most also close the APP container at shutdown. Flask, gRPC and Typer leave the APP container to you, and Typer builds its per-command container in `@modern_di_typer.inject`, not in its setup call; see [Closing the container](../providers/lifecycle.md#closing-the-container). Each integration page says which applies. + +On routes, `FromDI(T)` replaces the `@inject` + `Provide[Container.x]` pair: resolution is by type, so no marker points at a specific container attribute and no `@inject` decorator is needed. See the integration pages for the full route examples. ## 9. Testing and overrides @@ -329,12 +344,12 @@ See [Testing with overrides](../recipes/testing-overrides.md) for tree-wide shar | Failure mode | `dependency-injector` | `modern-di` | |---|---|---| -| 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`) | +| Circular dependency | No cycle detection; the first call into the cycle raises a bare `RecursionError` 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`; without `validate()`, a runtime cycle is caught once the recursion limit is hit and re-raised as `CircularDependencyError`, chained to the `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 in `validate()` or on the first resolve that needs it (`ArgumentResolutionError`, `ProviderNotRegisteredError`) | | 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 | +| 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; for an unregistered type, the error suggests registered types with a similar name | -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. +Call `container.validate()` at startup during the migration. Without it, a cycle is found only after the resolve hits the recursion limit, so the `CircularDependencyError` arrives chained to a `RecursionError` traceback thousands of lines long. ## 11. No direct equivalent @@ -344,8 +359,8 @@ A handful of `dependency-injector` features have no direct port. Workarounds: - 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. +- `modern-di` has no general-purpose injection decorator to replace `@inject` + `Provide[...]` 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)`. +- Providers are not partially applied callables. In `dependency-injector`, calling a provider with extra arguments (`container.some_factory(extra_arg)`) merges them with the declared ones for that one call. `modern-di`'s `resolve()` and `resolve_provider()` take no arguments, so every creator argument must come from its type, from `kwargs`, or from a default. Move a static value into `kwargs=`. For a value that varies per call site, use a `ContextProvider` set on the container you resolve from, or a deeper-scoped dependency. ## More diff --git a/docs/migration/from-that-depends.md b/docs/migration/from-that-depends.md index f094a692..1e481d75 100644 --- a/docs/migration/from-that-depends.md +++ b/docs/migration/from-that-depends.md @@ -1,6 +1,6 @@ # Migration from `that-depends` -This guide walks an existing `that-depends` codebase through the move to `modern-di`. Every provider type and core concept in `that-depends` has either a documented mapping below or an explicit note that there is no direct equivalent (with a workaround). +This guide walks an existing `that-depends` codebase through the move to `modern-di`. Every provider type and core concept in `that-depends` has either a mapping below or a note that there is no direct equivalent, with a workaround. ## 1. Install @@ -24,7 +24,7 @@ Core package: poetry add modern-di ``` -Framework integrations and the pytest helper live in separate packages. Install only what you need: +Framework integrations and the pytest helper live in separate packages. These cover the frameworks `that-depends` integrates with: === "uv" @@ -32,8 +32,7 @@ Framework integrations and the pytest helper live in separate packages. Install uv add modern-di-fastapi # FastAPI uv add modern-di-litestar # Litestar uv add modern-di-faststream # FastStream - uv add modern-di-typer # Typer - uv add modern-di-pytest # pytest fixtures + uv add --dev modern-di-pytest # pytest fixtures ``` === "pip" @@ -42,17 +41,18 @@ Framework integrations and the pytest helper live in separate packages. Install pip install modern-di-fastapi pip install modern-di-litestar pip install modern-di-faststream - pip install modern-di-typer pip install modern-di-pytest ``` +There are also integrations for [aiohttp](../integrations/aiohttp.md), [Starlette](../integrations/starlette.md), [Flask](../integrations/flask.md), [Celery](../integrations/celery.md), [arq](../integrations/arq.md), [taskiq](../integrations/taskiq.md), [gRPC](../integrations/grpc.md), [aiogram](../integrations/aiogram.md), [FastMCP](../integrations/fastmcp.md) and [Typer](../integrations/typer.md), each installed as `modern-di-`. + ## 2. Key conceptual shifts Three things change in how you think about the framework. Most migration confusion comes from these: - `Group` is a schema and `Container` is the runtime. In `that-depends`, a `BaseContainer` subclass is *both* the schema and the runtime: you resolve directly from the class. In `modern-di`, `Group` is a namespace-only class (you cannot instantiate it) and you create the runtime `Container(groups=[MyGroup])` separately, typically once at app start. All resolution, overrides, and lifecycle calls go through that `Container` instance. -- Resolution is sync-only. `modern-di` does not have `AsyncFactory`, `AsyncSingleton`, or `await container.resolve(...)`. Async work happens in the framework's lifespan (see [§6](#6-async-resources)). There is no plan to add async resolution back. -- Scopes are explicit. `Scope.APP → SESSION → REQUEST → ACTION → STEP`, with [the scope dependency rule](../providers/scopes.md#the-scope-dependency-rule) enforced at validation time. Framework integrations create the per-request child container automatically. +- Resolution is sync-only. `modern-di` does not have `AsyncFactory`, `AsyncSingleton`, or `await container.resolve(...)`. Async work happens in the framework's lifespan (see [§6](#6-async-resources)), and finalizers may be async. [Design decisions](../introduction/design-decisions.md#1-resolution-is-sync-only-finalizers-may-be-sync-or-async) explains why there are no plans to add async resolution. +- Scopes are ordered. `that-depends` has named scopes (`ContextScopes.APP`, `REQUEST` and so on) that `container_context(scope=...)` enters. `modern-di` scopes form a chain, `Scope.APP → SESSION → REQUEST → ACTION → STEP`, and a provider may depend only on providers of its own or a broader scope. `container.validate()` enforces [the scope dependency rule](../providers/scopes.md#the-scope-dependency-rule). Framework integrations create the per-request child container for you. ## 3. Provider mapping @@ -60,38 +60,45 @@ Use this table as the index for the rest of the guide. | `that-depends` | `modern-di` replacement | Where to look | |---|---|---| +| `BaseContainer` | `Group` (schema) + `Container(groups=[...])` (runtime) | [§4](#4-migrate-the-dependency-graph) | +| `BaseContainer.connect_containers(A, B)` | One `Container(groups=[A, B])`; closing it tears down every group | [§4](#4-migrate-the-dependency-graph) | | `Factory` | `providers.Factory(...)` | [§4](#4-migrate-the-dependency-graph) | | `Singleton` | `providers.Factory(..., cache=True)` | [§4](#4-migrate-the-dependency-graph) | | `Resource` (sync gen / ctx mgr) | `providers.Factory(..., cache=CacheSettings(finalizer=...))` | [§4](#4-migrate-the-dependency-graph) | | `Resource` (async gen / ctx mgr) | Lifespan + `ContextProvider` (or sync creator + async finalizer) | [§6](#6-async-resources) | -| `ContextResource` | `providers.Factory(..., scope=Scope.REQUEST)` | [§5](#5-context-resources-and-request-scope) | +| `ContextResource` | `providers.Factory(..., scope=Scope.REQUEST, cache=CacheSettings(finalizer=...))` | [§5](#5-context-resources-and-request-scope) | | `AsyncFactory` | Lifespan-managed; expose via `ContextProvider` | [§6](#6-async-resources) | | `AsyncSingleton` | Lifespan-managed; expose via `ContextProvider` | [§6](#6-async-resources) | | `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 | [§9](#9-no-direct-equivalent) | +| `AttrGetter` (`provider.attr`) | No direct equivalent | [§9](#9-no-direct-equivalent) | +| `ThreadLocalSingleton` | No direct equivalent | [§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) | -| `@inject` + `Provide[T]()` (non-web) | Explicit `container.resolve(T)` | [§9](#9-no-direct-equivalent) | +| `provider.bind(T)` | `providers.Factory(..., bound_type=T)`; add an `Alias` per extra type | [§4](#4-migrate-the-dependency-graph) | +| `ContextScopes` / `default_scope` | `Scope` / `class MyGroup(Group, scope=...)` | [§5](#5-context-resources-and-request-scope) | +| `@inject` + `Provide[Container.x]`, `Provide()` or `Provide["Container.x"]` (web) | `FromDI(T)` from the framework integration | [§8](#8-framework-integration-and-routes) | +| `fastapi.Depends(Container.x)` / `litestar.di.Provide(Container.x)` | `FromDI(T)` from the framework integration | [§8](#8-framework-integration-and-routes) | +| `@inject` + `Provide[...]` (non-web) | Explicit `container.resolve(T)` | [§9](#9-no-direct-equivalent) | +| `experimental.LazyProvider` | Not needed: type-based wiring never imports the provider | [§4](#4-migrate-the-dependency-graph) | +| `BaseContainer.resolve(func)` / `resolver(func)` | Register `func` as a `Factory` and resolve that | [§9](#9-no-direct-equivalent) | | `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) | +| `DIContextMiddleware` | The integration's setup call, such as `setup_di(app, container)` or `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) | | `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) | +| `with Container.override_providers_sync({...}):` | One `with container.override(provider, mock):` per provider | [§7](#7-lifecycle-and-testing) | | `provider.override_sync(mock)` | `container.override(provider, mock)` | [§7](#7-lifecycle-and-testing) | +| `with provider.override_context_sync(mock):` | `with container.override(provider, mock):` | [§7](#7-lifecycle-and-testing) | ## 4. Migrate the dependency graph 1. Replace `BaseContainer` with `Group`. -2. Add an explicit `scope=` to each provider (defaults to `Scope.APP`). -3. Create the runtime container with `Container(groups=[MyGroup])`. In `modern-di`, the `Group` class is a schema only; you cannot resolve from it directly. +2. Give each provider that is not app-wide a `scope=`. A provider without one is `Scope.APP`. For a group whose providers mostly share a scope, set a [group-level default](../providers/scopes.md#group-level-default-scope) with `class Dependencies(Group, scope=Scope.REQUEST)`, the way `default_scope` works in `that-depends`. +3. Create the runtime container with `Container(groups=[MyGroup])`. In `modern-di`, the `Group` class is a schema only; you cannot resolve from it directly. Containers you joined with `connect_containers` become several groups in one `Container`. -When a provider is passed inside `kwargs={...}`, `modern-di` detects it and resolves it like any other dependency. There is no `.cast` indirection, so drop those calls. +A provider passed inside `kwargs={...}` is resolved like any other dependency. There is no `.cast` indirection, so drop those calls. A creator parameter with a type annotation needs no `kwargs` entry at all: `modern-di` wires it to the provider registered for that type. Below, `create_sa_engine(settings: Settings)` gets the cached `Settings` that way, and `session` passes the engine by reference to show the other form. === "that-depends" @@ -100,9 +107,11 @@ When a provider is passed inside `kwargs={...}`, `modern-di` detects it and reso from app import repositories from app.resources.db import create_sa_engine, create_session + from app.settings import Settings class Dependencies(BaseContainer): + settings = providers.Singleton(Settings) database_engine = providers.Resource(create_sa_engine, settings=settings.cast) session = providers.ContextResource(create_session, engine=database_engine.cast) @@ -117,9 +126,11 @@ When a provider is passed inside `kwargs={...}`, `modern-di` detects it and reso from app import repositories from app.resources.db import close_sa_engine, close_session, create_sa_engine, create_session + from app.settings import Settings class Dependencies(Group): + settings = providers.Factory(Settings, cache=True) database_engine = providers.Factory( create_sa_engine, cache=providers.CacheSettings(finalizer=close_sa_engine), @@ -143,10 +154,11 @@ When a provider is passed inside `kwargs={...}`, `modern-di` detects it and reso ) - # Group is a schema. Create the runtime container once at app start. container = Container(groups=[Dependencies]) ``` +Type-based wiring also replaces `that-depends`' other ways of pointing at a provider. `Provide()` with a bound type, `Provide["Container.provider"]` strings and `experimental.LazyProvider` all exist to reference a provider without importing it. A `modern-di` creator names the type it needs and never imports the provider. + ### Per-provider replacements Replace `Singleton` with a cached `Factory` of `APP` scope: @@ -164,7 +176,7 @@ some_singleton = providers.Factory( 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. -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): +Replace `Object` with a `Factory` whose creator returns the value. Define a small typed function, because a lambda has no return annotation and so gets no type to resolve by: ```python # that-depends @@ -181,7 +193,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`. -Replace `List` and `Dict` with a `Factory` whose creator builds the collection: +Replace `List` and `Dict` with a `Factory` whose creator builds the collection, and pass that provider to its consumers through `kwargs`: ```python # that-depends @@ -192,22 +204,36 @@ def build_list(a: SomeType1, b: SomeType2) -> list[object]: return [a, b] some_list = providers.Factory(build_list) +consumer = providers.Factory(Consumer, kwargs={"items": some_list}) ``` -Replace `Provider.bind(Type)` with `Alias`. This is useful when you want an abstract type (`Protocol`, ABC) to resolve to a concrete registered provider: +A parameterized return type registers under its bare origin: `build_list` is bound to `list`, not `list[object]`, so `container.resolve(list[object])` raises `ProviderNotRegisteredError`. A consumer parameter annotated `list[X]` cannot be wired by type either, and `Factory(Consumer)` raises `UnsupportedCreatorParameterError` unless `kwargs` supplies it, as above. Two list-returning factories in one container both claim `list` and raise `DuplicateProviderTypeError`; give all but one `bound_type=None` and pass them by reference. + +Replace `provider.bind(Repository)` with `bound_type=`. `bind` replaces the provider's bindings, so the provider answers only for `Repository`, and `bound_type` does the same: ```python # that-depends repo = providers.Factory(PostgresRepository).bind(Repository) # modern-di -repo = providers.Factory(PostgresRepository, cache=True) -abstract_repo = providers.Alias(PostgresRepository, bound_type=Repository) +repo = providers.Factory(PostgresRepository, bound_type=Repository) +``` + +When the provider must answer for more than one type, as with `.bind(A, B)` or `contravariant=True`, keep it bound to one type and add an `Alias` for each other type: + +```python +repo = providers.Factory(PostgresRepository) +as_repository = providers.Alias(PostgresRepository, bound_type=Repository) +as_audit_repository = providers.Alias(PostgresRepository, bound_type=AuditRepository) ``` ## 5. Context resources and request scope -The `that-depends` `ContextResource` / `container_context()` / `State` / `fetch_context_item` family all collapse into two `modern-di` mechanisms: `Scope.REQUEST` (and below) providers, and `ContextProvider`. Declaring `scope=Scope.REQUEST` on a provider (e.g. `session` in the worked example above) is enough when a framework integration builds the per-request child container for you; `container_context()`'s manual case maps onto using that same child container as a context manager yourself. See [Building child containers](../providers/scopes.md#building-child-containers) for both forms. +The `that-depends` `ContextResource` / `container_context()` / `State` / `fetch_context_item` family all collapse into two `modern-di` mechanisms: `Scope.REQUEST` (and below) providers, and `ContextProvider`. + +A `ContextResource` is one instance per context with teardown when the context exits. In `modern-di` that is a cached `Factory` at `Scope.REQUEST` with a finalizer, like `session` in the worked example: each request container builds one instance and runs the finalizer when it closes. When a framework integration builds the per-request child container, declaring the scope is all you do. `container_context()`'s manual case maps onto using that child container as a context manager yourself. See [Building child containers](../providers/scopes.md#building-child-containers) for both forms. + +`ContextScopes.APP` and `ContextScopes.REQUEST` map to `Scope.APP` and `Scope.REQUEST`, and `default_scope` maps to a group-level default scope ([§4](#4-migrate-the-dependency-graph)). `ContextScopes.ANY` and `ContextScopes.INJECT` have no direct equivalent. The nearest is a child container you build and close around the call that needs it. For scopes of your own, see [Custom scopes](../providers/scopes.md#custom-scopes). ### Injecting custom context (replaces `State`, `fetch_context_item`, `fetch_context_item_by_type`) @@ -244,9 +270,16 @@ with container.build_child_container( `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. +!!! warning "An `async def` creator is not rejected" + + `providers.Factory(create_pool)` accepts an `async def` creator. Resolving it returns the + coroutine object, never awaited, and the only sign is a `RuntimeWarning: coroutine ... was + never awaited` when that object is garbage-collected. Porting an `AsyncFactory` by swapping the + class name produces exactly this, so move each async creator to one of the shapes below. + ### Sync creator, async finalizer -The most common shape. `CacheSettings.finalizer` accepts both sync and async functions; `await container.close_async()` (which the framework integrations call automatically at shutdown) awaits the async ones. +The most common shape. `CacheSettings.finalizer` accepts both sync and async functions, and `await container.close_async()` awaits the async ones. `close_sync()` cannot run an async finalizer and raises `FinalizerError`. ```python import sqlalchemy.ext.asyncio @@ -278,24 +311,40 @@ sync creator with an async finalizer instead. ### Per-request async construction -If a per-request resource genuinely needs `await` at construction time, the simplest path is to make the *creator* sync but have it return a pre-acquired object that you placed into the request container's context. Most cases (SQLAlchemy `AsyncSession`, `httpx.AsyncClient`) can be expressed as sync creator + async finalizer instead, and that path is preferred. +Most per-request resources, such as SQLAlchemy's `AsyncSession` or `httpx.AsyncClient`, construct synchronously, so a sync creator with an async finalizer covers them. When one really needs `await` to construct, do the `await` in a per-request hook or middleware, then put the result on that request's container with `set_context`, for a `ContextProvider` at `Scope.REQUEST`: + +```python +class Dependencies(Group): + connection = providers.ContextProvider(Connection, scope=Scope.REQUEST) + repo = providers.Factory(Repository, scope=Scope.REQUEST) # takes connection: Connection + + +async with container.build_child_container(scope=Scope.REQUEST) as request_container: + request_container.set_context(Connection, await acquire_connection()) + repo = request_container.resolve(Repository) +``` ## 7. Lifecycle and testing ### 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()` become `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. Which integrations close the APP container for you is in [§8](#8-framework-integration-and-routes). ### Overrides -Overrides are keyed by provider reference, not by name: +Overrides are keyed by provider reference, not by name. `container.override(...)` returns a handle that also works as a context manager and restores the previous state on exit, including on an exception: ```python # that-depends -container.override_providers_sync({"decks_service": fake_decks_service}) +with Container.override_providers_sync({"decks_service": fake_decks_service}): + ... -# modern-di +# modern-di, for one block +with container.override(Dependencies.decks_service, fake_decks_service): + ... + +# modern-di, until reset container.override(Dependencies.decks_service, fake_decks_service) ... container.reset_override(Dependencies.decks_service) # or reset_override() to clear all @@ -305,11 +354,13 @@ See [Testing with overrides](../recipes/testing-overrides.md) for override mecha ### Validation -`container.validate()` runs cycle detection and scope-chain checks. Call it explicitly at startup during migration: it catches missed scope changes and broken dependencies before the first request. +Call `container.validate()` at startup during the migration. It walks the whole graph and raises one `ValidationFailedError` listing every problem it finds: cycles, a provider that depends on a narrower scope, creator arguments nothing can supply, and aliases whose source is not registered. It does not check that context values are set; a missing one surfaces at resolve as `ContextValueNotSetError`. With a framework integration, call it after the setup call, which registers the integration's own providers, such as the one for the framework's request object. ## 8. Framework integration and routes -Replace `DIContextMiddleware` with the integration package's setup call ([FastAPI](../integrations/fastapi.md), [Litestar](../integrations/litestar.md), [FastStream](../integrations/faststream.md), [Typer](../integrations/typer.md)). That call creates per-request child containers, tears them down automatically, and calls `container.close_async()` at shutdown. On routes, `FromDI(T)` replaces both `fastapi.Depends(Provide[T]())` and `litestar.di.Provide`, resolving by type instead of by marker; see the integration pages for the full route examples. +Replace `DIContextMiddleware` with the integration package's setup call, such as `setup_di(app, container)` for [FastAPI](../integrations/fastapi.md) and [FastStream](../integrations/faststream.md) or `ModernDIPlugin(container)` for [Litestar](../integrations/litestar.md). Integrations build a child container for each request or message and close it when the request ends. Most also close the APP container at shutdown. Flask, gRPC and Typer leave the APP container to you, and Typer builds its per-command container in `@modern_di_typer.inject`, not in its setup call; see [Closing the container](../providers/lifecycle.md#closing-the-container). Each integration page says which applies. + +On routes, `FromDI(T)` replaces both `fastapi.Depends(Container.provider)` and `litestar.di.Provide(Container.provider)`, resolving by type instead of by provider reference; see the integration pages for the full route examples. ## 9. No direct equivalent @@ -318,7 +369,8 @@ A handful of `that-depends` features have no direct port. Workarounds: - 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)`. +- `modern-di` has no general-purpose injection decorator to replace `@inject` + `Provide[...]` 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)`. +- For `BaseContainer.resolve(func)` and `resolver(func)`, which call `func` with its parameters filled by name, register `func` as a `Factory` with `bound_type=None` and call `container.resolve_provider(...)` on it. Its parameters are then wired by type, not by name. ## More diff --git a/docs/migration/to-4.x.md b/docs/migration/to-4.x.md index 65ed93ec..3e8b6a8a 100644 --- a/docs/migration/to-4.x.md +++ b/docs/migration/to-4.x.md @@ -1,8 +1,10 @@ # Migration guide: upgrading to modern-di 4.x -This document describes the changes required to migrate from modern-di 3.x to modern-di 4.0. +This document describes the changes required to migrate from modern-di 3.x to 4.x. Most of them +came in 4.0. 4.1 added a few more that can break code written against 4.0; they are listed +separately at the end. -## Key changes +## Changes in 4.0 ### `Container` takes only `scope` positionally @@ -153,11 +155,8 @@ the finalizer errors inside it by type, `AsyncFinalizerInSyncCloseError` include was a list. - The constructor keyword `finalizer_errors=` is now `exceptions=`. - `.is_async` is unchanged, and a group that `except*` splits off keeps it. -- `except FinalizerError` and `except ModernDIError` still catch it. -- Its message is one line naming each kind of error with its count, such as - `Container.close_sync() found 1 finalizer error(s): ValueError (1)`, where 3.x listed every - finalizer exception. Each finalizer exception carries a note naming the type of the cached - instance whose finalizer raised it. +- `except FinalizerError` and `except ModernDIError` still catch it, and its message is unchanged. + 4.1 [shortened the message](#finalizererror-summarizes-its-errors). ```python # 3.x @@ -264,7 +263,8 @@ code that calls them raises `AttributeError`: `AbstractProvider` no longer derives from `abc.ABC`. It never declared an abstract method, and the provider set is closed. Only code that relied on `ABCMeta` is affected: `AbstractProvider.register(...)` raises `AttributeError`. `isinstance(x, AbstractProvider)` works -as before for every provider, and type hints that name `AbstractProvider` need no change. +as before for every provider, and type hints that name `AbstractProvider` need no change. From 4.1 +on, [subclassing any provider raises `TypeError`](#subclassing-a-provider-raises-typeerror). ### `NewType` and type alias annotations are wired @@ -384,30 +384,68 @@ None of these were meant for use outside the package. Accessing them raises `Att `resolve()` on a closed container raises `ContainerClosedError` even when the type is not registered. In 3.x that call raised `ProviderNotRegisteredError`. +### The 3.x deprecations are removed + +- `Container(validate=...)` raises `TypeError`, and `ValidateArgumentWarning` is gone with it. Drop + the argument and call `container.validate()` where you want the graph checked. +- `Container.scope_map` and `Container.lock` are removed. Nothing replaces them as public API; call + `find_container(scope)` to reach an ancestor. +- `ContextValueNoneWarning` and `UnvalidatedContainerWarning` are removed. Neither has been emitted + since 3.0, so delete any `filterwarnings` entry or import that names them. +- The `modern_di.exceptions.warnings` module held only these three warnings and is deleted. An + import from that path raises `ModuleNotFoundError`, so delete it. + +## Changes in 4.1 + ### `Alias` requires `bound_type` -In 3.x `bound_type` defaulted to `source_type`, so `Alias(X)` registered under `X` and resolved to -itself: `validate()` reported a `CircularDependencyError`, or registration raised -`DuplicateProviderTypeError` when a provider for `X` existed too. In 4.x `bound_type` has no +In 3.x and 4.0 `bound_type` defaulted to `source_type`, so `Alias(X)` registered under `X` and +resolved to itself: `validate()` reported a `CircularDependencyError`, or registration raised +`DuplicateProviderTypeError` when a provider for `X` existed too. In 4.1 `bound_type` has no default. `Alias(X)` raises `TypeError`, and `Alias(X, bound_type=X)` raises `AliasBoundToSourceError`. Pass the type the alias answers for, or `bound_type=None` to use it by reference only: ```python -# 3.x +# 3.x and 4.0 alias = providers.Alias(PostgresDatabase) -# 4.x +# 4.1 alias = providers.Alias(PostgresDatabase, bound_type=DatabaseProtocol) ``` -### The 3.x deprecations are removed +### Subclassing a provider raises `TypeError` -- `Container(validate=...)` raises `TypeError`, and `ValidateArgumentWarning` is gone with it. Drop - the argument and call `container.validate()` where you want the graph checked. -- `Container.scope_map` and `Container.lock` are removed. Nothing replaces them as public API; call - `find_container(scope)` to reach an ancestor. -- `ContextValueNoneWarning` and `UnvalidatedContainerWarning` are removed. Neither has been emitted - since 3.0, so delete any `filterwarnings` entry or import that names them. -- The `modern_di.exceptions.warnings` module held only these three warnings and is deleted. An - import from that path raises `ModuleNotFoundError`, so delete it. +A class that subclasses `AbstractProvider`, `Factory`, `ContextProvider`, `Alias` or any other +modern-di provider outside the package now raises `TypeError` when the class statement runs. In 4.0 +the class statement succeeded, and a resolve of the subclass raised +`TypeError: no compiled resolver for provider type MyFactory`. The error now comes at definition: + +```text +TypeError: MyFactory subclasses a modern-di provider. The provider set is closed, so a subclass would not resolve. Compose behavior in a creator function or an Alias instead. +``` + +Put the custom behavior in the creator function and use a stock `Factory`. + +### `FinalizerError` summarizes its errors + +`str(FinalizerError)` is one line naming each kind of error with its count, plus the +troubleshooting link: + +```text +Container.close_sync() found 1 finalizer error(s): ValueError (1) +See: https://modern-di.modern-python.org/troubleshooting/finalizer-error/ +``` + +In 4.0 and 3.x the message listed every finalizer exception, as in +`Errors during sync cleanup: [ValueError('bad')]`. Each finalizer exception in `.exceptions` now +carries a note naming the type of the cached instance whose finalizer raised it, such as +`raised by the finalizer of a cached Engine`. A traceback shows the notes; update any test that +matched the old message. + +### `DuplicateProviderTypeError` names both providers + +The message lists the two providers that claim the type, each with its definition site, and the +error stores them as `.first_provider` and `.second_provider`. `.provider_type` is unchanged. +Update any test that matched the 4.0 text, which started +`Provider is duplicated by type . To resolve this issue:`.