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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/integrations/celery.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ with container:
## 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.
- [Organize a large container with multiple Groups](../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.

Expand Down
2 changes: 1 addition & 1 deletion docs/integrations/flask.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ assert request_app.test_client().get("/describe").json == [{"method": "GET", "ur
## 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.
- [Organize a large container with multiple Groups](../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.

Expand Down
7 changes: 4 additions & 3 deletions docs/integrations/litestar.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ container.open()
Mark every injected parameter with `NamedDependency[...]` from `litestar.di`. Litestar 2.23
deprecated inferring a dependency from a plain annotation, and Litestar 3.0 removes it.

Call `container.validate()` after the plugin is installed. A factory that takes a `litestar.Request`
depends on the context provider that the plugin registers, so validating before it fails with
Call `container.validate()` after `Litestar(...)` is constructed. The plugin registers its context
providers in `on_app_init`, which Litestar runs during construction, and a factory that takes a
`litestar.Request` depends on one of them, so validating before that fails with
`ValidationFailedError`.

## Injecting into handlers
Expand Down Expand Up @@ -223,7 +224,7 @@ with TestClient(request_app) as client:
## See also

- [Testing with overrides](../recipes/testing-overrides.md): swap providers in your tests.
- [Multi-group organization](../recipes/multi-group.md): splitting providers across groups, with `autowired_groups`.
- [Organize a large container with multiple Groups](../recipes/multi-group.md): splitting providers across groups, with `autowired_groups`.
- [Lifecycle](../providers/lifecycle.md): finalizers and `close_async()`.
- [Scopes](../providers/scopes.md): the APP → REQUEST lifetime model.

Expand Down
16 changes: 13 additions & 3 deletions docs/integrations/writing-integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,8 @@ When you rewrite:
- Guard sweeps that apply `@inject` for the user (Flask and aiogram `auto_inject`, Celery `DITask`)
against double wrapping with `integrations.is_injected(func)` and
`integrations.mark_injected(wrapper)`.
- Say which handlers a sweep leaves alone. aiogram's `auto_inject` skips handlers on `update`
observers, so those need `@inject` by hand, and its page says so.
- Leave nested scopes to the caller. Typer's `action_scope(ctx)` yields a fresh `ACTION` child of the
per-call container for each `with` block.

Expand Down Expand Up @@ -445,9 +447,17 @@ Each official integration is its own repository and PyPI package, with the same
`ruff` floats forward in CI, so keep `CPY001` in the lint `ignore` list.
- In the `modern-di` repo, add `docs/integrations/<framework>.md` and a `mkdocs.yml` nav entry under
the matching family: Web, Tasks & events, Bots, RPC, CLI or Testing. Model the page on an existing one
such as [Starlette](starlette.md): one compact example with the same two providers as `examples/app.py`,
`container.validate()` after `setup_di`, connection injection in its own "Framework context objects"
section, then framework-specific sections, a `## See also` block, and the `## API` table last.
in the same family, with one compact example using the same two providers as `examples/app.py` and
`container.validate()` after `setup_di`. Web pages such as [Starlette](starlette.md) use
`## Installation`, `## Usage`, `## Injecting into handlers`, `## Scopes and lifecycle`, and
`## Websockets` where the framework has them. Other pages such as [taskiq](taskiq.md) use
`## How to use`, `## Scopes`, `## Root container lifecycle`, and usually `## Testing`. Both kinds
give connection injection its own section, named `## Framework context objects` on every page but
gRPC's and present even when the integration registers no context provider. Framework-specific
sections follow, then a `## See also` block, and the `## API` table last. Write `FromDI` in the
`Annotated` form, except where the framework needs another: the [FastMCP](fastmcp.md) page uses the
default-value form, and the [Litestar](litestar.md) page puts `FromDI` in a route's `dependencies`
mapping.
For a provider that must also resolve outside a connection, show an optional parameter
(`request: FrameworkType | None = None`), as the [gRPC page](grpc.md#injecting-the-servicercontext)
does; [Optional parameters](../providers/context.md#optional-parameters) explains it.
Expand Down
2 changes: 1 addition & 1 deletion docs/introduction/resolving.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ raising. That includes the case where you meant to register the provider and for
## When resolution fails

- `resolve(SomeType)` with no provider bound to `SomeType` raises `ProviderNotRegisteredError`
(see [No provider registered for type](../troubleshooting/missing-provider.md)).
(see [ProviderNotRegisteredError](../troubleshooting/missing-provider.md)).
- A creator parameter that nothing can satisfy raises `ArgumentResolutionError`, which names the
whole dependency chain (see [ArgumentResolutionError](../troubleshooting/argument-resolution-error.md)).

Expand Down
2 changes: 1 addition & 1 deletion docs/migration/to-4.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ ancestor, emitted `ContainerClosedWarning` and reopened it. In 4.0 the same call
`ContainerClosedError` and the container stays closed. `ContainerClosedWarning` is removed, so delete
any `filterwarnings` entry that names it. To use a closed container again, reopen it first with
`open()` or by re-entering `with` / `async with`, which calls `open()`. See
[Troubleshooting: ContainerClosedError](../troubleshooting/container-closed-error.md).
[ContainerClosedError](../troubleshooting/container-closed-error.md).

### Closing a root container keeps overrides

Expand Down
4 changes: 2 additions & 2 deletions docs/providers/alias.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ inside one `ValidationFailedError`:

- `AliasSourceNotRegisteredError` when nothing is registered for `source_type`.
- `CircularDependencyError` for a cycle that passes through an alias; see
[Troubleshooting: Circular dependency](../troubleshooting/circular-dependency.md).
[CircularDependencyError](../troubleshooting/circular-dependency.md).
- `InvalidScopeDependencyError` when a provider depends, through an alias, on a source at a deeper
scope.

Expand Down Expand Up @@ -142,7 +142,7 @@ cache_container.validate() # .exceptions holds an AliasSourceNotRegisteredError
`validate()` applies the [scope dependency rule](scopes.md#the-scope-dependency-rule) through
aliases. The error names every hop of the chain and its terminal source, since the alias's own
type carries no scope to point at; see
[Troubleshooting: Scope chain](../troubleshooting/scope-chain.md#when-the-dependency-is-reached-through-an-alias).
[InvalidScopeDependencyError: when the dependency is reached through an alias](../troubleshooting/scope-chain.md#when-the-dependency-is-reached-through-an-alias).

## See also

Expand Down
2 changes: 1 addition & 1 deletion docs/providers/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ kwargs={"request": fastapi_request_provider} # explicit wiring, see Factories:
```

Each integration's page lists its provider names, scopes and types:
[aiohttp](../integrations/aiohttp.md#api),
[aiohttp](../integrations/aiohttp.md#framework-context-objects),
[aiogram](../integrations/aiogram.md#framework-context-objects),
[FastAPI](../integrations/fastapi.md#framework-context-objects),
[FastMCP](../integrations/fastmcp.md#framework-context-objects),
Expand Down
17 changes: 12 additions & 5 deletions docs/providers/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ A finalizer that raises still drops its cached instance, and the next resolve af
builds a fresh one. With `clear_cache=False` the instance is kept, as it is after a finalizer that
succeeds.

See [FinalizerError](../troubleshooting/finalizer-error.md) for reading the group and fixing the
finalizers that raised.

### Async finalizers and `close_sync()`

`close_sync()` cannot await. When it reaches an instance with an async finalizer, it adds an
Expand Down Expand Up @@ -209,8 +212,10 @@ except* exceptions.AsyncFinalizerInSyncCloseError:
await container.close_async() # runs the async finalizer
```

Use `async with container:` or `await container.close_async()` when any provider has an async
finalizer.
Until then the instance stays cached: a repeated `close_sync()` reports it again, and resolving after
a reopen returns the same object. Use `async with container:` or `await container.close_async()`
when any provider has an async finalizer; see
[AsyncFinalizerInSyncCloseError](../troubleshooting/async-finalizer-in-sync-close-error.md).

### Cancelled close

Expand Down Expand Up @@ -261,7 +266,7 @@ with container: # reopened by __enter__
container.resolve(Settings)
```

See [Troubleshooting: ContainerClosedError](../troubleshooting/container-closed-error.md) for what to
See [ContainerClosedError](../troubleshooting/container-closed-error.md) for what to
check when a container is closed where you did not expect it, and
[Migration: To 4.x](../migration/to-4.x.md#resolving-on-a-closed-container-raises) for the 3.x
behavior, which warned and reopened instead.
Expand Down Expand Up @@ -295,7 +300,7 @@ container.validate() # raises ValidationFailedError listing every issue
```

It reports every issue in one `exceptions.ValidationFailedError` instead of stopping at the first;
see [Troubleshooting: ValidationFailedError](../troubleshooting/validation-failed-error.md). An
see [ValidationFailedError](../troubleshooting/validation-failed-error.md). An
integration that registers its own providers with `add_providers` should validate after that; see
[Writing an integration](../integrations/writing-integrations.md#lifecycle-rules).

Expand All @@ -306,4 +311,6 @@ adds nothing to resolve time. Call it at startup or in one test.

- [Scopes](scopes.md): child containers and which container caches an instance.
- [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.
- [Async SQLAlchemy: engine, session, repository](../recipes/sqlalchemy.md): sync creators with async finalizers.
- [Async resources via lifespan](../recipes/async-lifespan.md): resources whose construction needs `await`.
- [FinalizerError](../troubleshooting/finalizer-error.md) and [AsyncFinalizerInSyncCloseError](../troubleshooting/async-finalizer-in-sync-close-error.md): the errors a close can raise.
2 changes: 1 addition & 1 deletion docs/recipes/good-and-bad-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Dependencies(Group):

`container.validate()` reports the broken version before anything resolves: it raises
`ValidationFailedError` carrying an `InvalidScopeDependencyError`; see
[Scope chain violation](../troubleshooting/scope-chain.md). Without `validate()`, the first resolve of
[InvalidScopeDependencyError](../troubleshooting/scope-chain.md). Without `validate()`, the first resolve of
`UserCache` raises `ScopeNotInitializedError`, whose message names both `UserCache` and `Session`, on
the first request that reaches it instead of at startup.

Expand Down
1 change: 1 addition & 0 deletions docs/recipes/multi-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ await container.close_async()

- Two providers for the same type make `Container(groups=[...])` raise `DuplicateProviderTypeError`, whichever groups they are in. Give one of them `bound_type=None` and pass it to its consumers through [`kwargs`](../providers/factories.md#kwargs); see [DuplicateProviderTypeError](../troubleshooting/duplicate-type-error.md).
- A provider object belongs to one group. Assigning it to a second group (`engine = Database.engine`) registers it twice and raises the same `DuplicateProviderTypeError`. To use another group's provider, depend on its type, or reference it in `kwargs={"engine": Database.engine}`.
- Passing one group twice, or a group together with its subclass, also registers the same providers twice and raises `DuplicateProviderTypeError`, with a `bound_type=None` hint that does not apply. Pass each group once; for a subclass, pass only the subclass. See [issue #669](https://github.com/modern-python/modern-di/issues/669).
- `Container` ignores attribute names, so two groups can both have a `session` attribute. Litestar's `autowired_groups` and `modern-di-pytest`'s `expose()` do use them: the first warns and keeps the provider from the last group, and the second raises `ValueError`.
- The order of `groups=[...]` does not matter. Nothing checks the combined graph for you, so call `container.validate()` at startup.

Expand Down
1 change: 1 addition & 0 deletions docs/recipes/testing-overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ Under the default mode, a `session.rollback()` in the code under test rolls back
- Overrides are global to the container tree. Override on any container and every container sharing its root sees the replacement. That is what tests want; keep it in mind if you ever override outside tests.
- `override` is keyed by provider object. Pass `Dependencies.user_repository`, not the string `"user_repository"`, which raises `AttributeError`.
- Always reset. A leaked override reaches every later test that shares the container, and nothing reports it. With a session-scoped container, set overrides in a function-scoped fixture with `with container.override(...)`, so they are reset after each test even when it fails.
- Watch cached dependents. A cached instance (`cache=True`) keeps the dependencies it was built with, so overriding one of its dependencies does not reach an instance already in the cache, and an instance first built under an override keeps the fake after the override ends. Override the cached provider itself, or build the tests that need the fake a fresh container. [Pytest integration: Overrides](../integrations/pytest.md#overrides) has the same caveat for session-scoped fixtures.
- Override the right level. If you override the engine but tests resolve the session, the session's creator still runs, so the replacement has to be something that creator accepts. If a test relies on a specific session, override the session itself.

## See also
Expand Down
50 changes: 13 additions & 37 deletions docs/troubleshooting/context-not-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,43 +20,11 @@ When a creator body calls `container.resolve(TenantId)` itself, the chain ends a

### 1. The value is set on a container of a different scope

A `ContextProvider` reads only the container of its own scope, and context never propagates between
containers; see [context propagation](../providers/context.md#context-propagation). A value set on
any other container is invisible to it, in either direction:

- A `ContextProvider(TenantId, scope=Scope.REQUEST)` does not see a value set on the APP parent.
- A `ContextProvider(TenantId, scope=Scope.APP)` does not see a value set on a REQUEST child.

<!-- invisible-code-block: python
from modern_di import Container, Scope, providers


class TenantId(str): ...


app_container = Container(scope=Scope.APP)
-->

```python
# Broken: the TenantId provider is REQUEST-scoped, so the APP value is never read.
app_container.set_context(TenantId, TenantId("acme"))
request_container = app_container.build_child_container(scope=Scope.REQUEST)
```

Set the value on the container whose scope matches the provider's scope, either when building it or
afterwards:

```python
# Works:
request_container = app_container.build_child_container(
scope=Scope.REQUEST,
context={TenantId: TenantId("acme")},
)

# Works:
request_container = app_container.build_child_container(scope=Scope.REQUEST)
request_container.set_context(TenantId, TenantId("acme"))
```
A `ContextProvider` reads only the container of its own scope, so a value set on a parent or a child
container never reaches it. Set the value on the container whose scope matches the provider's
scope, with `context=` when building it or `set_context()` afterwards;
[Context propagation](../providers/context.md#context-propagation) shows both, and the rule in each
direction.

If the value really is per request, declare the provider with `scope=Scope.REQUEST`. If it is one
value for the whole app, declare it with `scope=Scope.APP` and set it on the APP container.
Expand All @@ -76,6 +44,13 @@ value still raises. See [Optional parameters](../providers/context.md#optional-p
To make the value optional for every consumer, direct resolves included, give the provider a
default. See [Optional context: `default=`](../providers/context.md#optional-context-default):

<!-- invisible-code-block: python
from modern_di import Scope, providers


class TenantId(str): ...
-->

```python
tenant = providers.ContextProvider(TenantId, scope=Scope.REQUEST, default=None)
```
Expand All @@ -86,3 +61,4 @@ tenant = providers.ContextProvider(TenantId, scope=Scope.REQUEST, default=None)
- [When no value is set](../providers/context.md#when-no-value-is-set): what each kind of consumer gets.
- [Scopes](../providers/scopes.md): per-container context and why it never propagates.
- [Async resources via lifespan](../recipes/async-lifespan.md): the "construct in lifespan, inject as context" pattern.
- [Migration: a missing context value for a required parameter raises `ContextValueNotSetError`](../migration/to-4.x.md#a-missing-context-value-for-a-required-parameter-raises-contextvaluenotseterror): the 3.x behavior, and why `except ArgumentResolutionError` no longer catches it.
2 changes: 2 additions & 0 deletions docs/troubleshooting/creator-call-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ rejected when the `Factory` is declared, as

An exception raised inside the creator's body, a `TypeError` included, propagates as itself and is
never wrapped in this error.
A `ResolutionError` raised by a `container.resolve()` call in the body keeps its class too, and gains
the creator's step in its `.dependency_path`, so its message draws the chain down to that creator.

## Fix

Expand Down
7 changes: 7 additions & 0 deletions docs/troubleshooting/duplicate-type-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ same `bound_type` when:
- two factories' creators return the same type, such as a primary and a replica `DatabaseConfig`;
- two groups passed to the same container each declare a provider for the type.

The same provider object registered twice raises it too, with both bullets naming one provider and
`.first_provider is .second_provider`. That happens when a group and its subclass are both passed,
when one group is passed twice, or when one provider is assigned in two groups. The hint about
`bound_type=None` does not apply here. Pass each group once, and for a subclass pass only the
subclass, which already holds the base group's providers. This is tracked in
[issue #669](https://github.com/modern-python/modern-di/issues/669).

## Fix

If the two providers stand for different things, give each its own type. A thin subclass is enough,
Expand Down
6 changes: 4 additions & 2 deletions docs/troubleshooting/finalizer-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ What `except*` does not catch is raised again as a `FinalizerError` holding only
errors.

A finalizer that raised is not retried. Its instance is dropped from the cache, so a later close does
not call that finalizer again, and resolving after a reopen builds a new instance. Because every other
finalizer still ran, a broken one does not leak a resource that a later finalizer would have closed.
not call that finalizer again, and resolving after a reopen builds a new instance. With
`clear_cache=False` the instance stays cached and a reopen returns it, but its finalizer still does
not run again. Because every other finalizer still ran, a broken one does not leak a resource that a
later finalizer would have closed.

An `AsyncFinalizerInSyncCloseError` entry is the exception to this: `close_sync()` keeps that
instance cached, and a later `await container.close_async()` runs its finalizer. See
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ providers.Factory(create_thing, scope=Scope.APP)
```

```python
# Works:
def create_thing(items: tuple[Item, ...] = ()) -> Thing:
return Thing(items)

Expand Down
Loading
Loading