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
5 changes: 5 additions & 0 deletions architecture/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,8 @@ Two edge cases in `Alias.effective_scope` are handled safely:
| `CircularDependencyError` | `ResolutionError` | recorded inside `validate()` on cycle detection |
| `InvalidScopeDependencyError` | `RegistrationError` | recorded inside `validate()` on inverted scope edge |
| `ArgumentResolutionError` | `ResolutionError` | yielded by `Factory.iter_validation_issues()` |

Every concrete `ModernDIError` subclass — these three included — carries a class-level `docs_slug`
naming its page under `docs/troubleshooting/`; the census test (`tests/test_docs_slug_census.py`)
pins both that every slug is set and unique and that its page actually exists, so a new exception
cannot ship without a matching troubleshooting page.
53 changes: 34 additions & 19 deletions docs/providers/errors-and-exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,35 +51,41 @@ Catch `ContainerError` for any container/scope failure.

- **`InvalidChildScopeError`** — 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.
scope). The error lists the scopes that *are* allowed. See
[Troubleshooting: InvalidChildScopeError](../troubleshooting/invalid-child-scope-error.md).
- **`MaxScopeReachedError`** — 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.
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`** — 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: Scope chain](../troubleshooting/scope-chain.md).
failed, not just the two scope names. See
[Troubleshooting: ScopeNotInitializedError](../troubleshooting/scope-not-initialized-error.md).
- **`ScopeSkippedError`** — 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: Scope chain](../troubleshooting/scope-chain.md).
[Troubleshooting: ScopeSkippedError](../troubleshooting/scope-skipped-error.md).
- **`InvalidScopeTypeError`** — raised by the `Container` constructor when `scope` is not an
`enum.IntEnum`.
`enum.IntEnum`. See
[Troubleshooting: InvalidScopeTypeError](../troubleshooting/invalid-scope-type-error.md).
- **`ContainerClosedError`** — raised in modern-di **3.0** when you resolve from, or build a child
of, a closed container. Until then that reuse emits a **`ContainerClosedWarning`** (a
`DeprecationWarning`) and the container self-reopens — see
[Lifecycle: closing and reopening](lifecycle.md#closing-and-reopening). Escalate the warning
today via the
[readiness recipe](../migration/to-3.x.md#readiness-recipe-escalating-warnings-to-errors-with-filterwarnings).
See [Troubleshooting: ContainerClosedError](../troubleshooting/container-closed-error.md).
- **`ValidationFailedError`** — raised by `Container.validate()` (and `Container(..., validate=True)`)
when the graph has problems. 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. Leaving `validate` unset on a root container skips the
check (as before) but emits **`UnvalidatedContainerWarning`** (a `FutureWarning`) — modern-di
**3.0** runs `validate()` at root construction by default; pass `validate=True` to adopt that now
or `validate=False` to keep validation off permanently. See
[Migration: To 3.x](../migration/to-3.x.md).
[Migration: To 3.x](../migration/to-3.x.md) and
[Troubleshooting: ValidationFailedError](../troubleshooting/validation-failed-error.md).

## `ResolutionError` — failures while resolving a type

Expand All @@ -94,25 +100,28 @@ the same `dependency_path` — the breadcrumb machinery is shared, not duplicate
the type. The message includes "did you mean…" suggestions when a close match exists. See
[Troubleshooting: Missing provider](../troubleshooting/missing-provider.md).
- **`AliasSourceNotRegisteredError`** — raised when an `Alias` points at a `source_type` that has no
registered provider (eagerly during `validate()`, or at resolution time).
registered provider (eagerly during `validate()`, or at resolution time). See
[Troubleshooting: AliasSourceNotRegisteredError](../troubleshooting/alias-source-not-registered-error.md).
- **`ArgumentResolutionError`** — raised when a creator parameter cannot be resolved: no provider
matches its annotated type, or the parameter is unannotated. See
[Troubleshooting: Context not set](../troubleshooting/context-not-set.md).
[Troubleshooting: ArgumentResolutionError](../troubleshooting/argument-resolution-error.md).
- **`CircularDependencyError`** — 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`** — raised when a creator's dependencies all resolved but the creator itself
raised while being called. The original exception is preserved on `.original_error` (and as the
`__cause__`).
- **`CreatorCallError`** — 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`** — raised in modern-di **3.0** when an unset `ContextProvider` is
resolved *directly* (`container.resolve(SomeContextType)` with no value set). Until then that
resolve emits **`ContextValueNoneWarning`** (a `DeprecationWarning`) and returns `None` —
escalate it today via the
[readiness recipe](../migration/to-3.x.md#readiness-recipe-escalating-warnings-to-errors-with-filterwarnings).
Only the direct-resolve path is affected — a `Factory` parameter backed by the same
`ContextProvider` keeps following its own default/nullable/required disposition. Inspect
`.context_type`.
`.context_type`. See [Troubleshooting: Context not set](../troubleshooting/context-not-set.md).

## `RegistrationError` — declaration / registration problems

Expand All @@ -125,14 +134,17 @@ Catch `RegistrationError` for declaration- and registration-time problems.
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).
[Container: registering after construction](container.md#registering-providers-after-construction) and
[Troubleshooting: ChildContainerRegistrationError](../troubleshooting/child-container-registration-error.md).
- **`UnknownFactoryKwargError`** — 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.
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`** — raised when a creator's signature has a parameter
`modern-di` cannot wire (e.g. an unsupported kind); names the parameter and the reason.
`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`** — 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()`.
`validate()`. See [Troubleshooting: Scope chain](../troubleshooting/scope-chain.md).

## Direct `ModernDIError` subclasses

Expand All @@ -141,13 +153,16 @@ These don't fit the register/resolve/validate grouping:
- **`FinalizerError`** — 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).
[Lifecycle](lifecycle.md#close-failure-semantics) and
[Troubleshooting: FinalizerError](../troubleshooting/finalizer-error.md).
- **`AsyncFinalizerInSyncCloseError`** — 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).
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`** — raised when a `Group` subclass is instantiated. Groups are
namespaces and must never be created as objects.
namespaces and must never be created as objects. See
[Troubleshooting: GroupInstantiationError](../troubleshooting/group-instantiation-error.md).

## Security note

Expand Down
36 changes: 36 additions & 0 deletions docs/troubleshooting/alias-source-not-registered-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# AliasSourceNotRegisteredError

**Symptom**

Raised naming the `source_type` an `Alias` points at, saying no provider is registered for it.

**Cause**

`Alias(source_type=X)` was declared, but no provider's `bound_type` resolves to `X` — either the
provider for `X` was never defined, its group wasn't passed to `Container(groups=[...])`, or it was
declared with `bound_type=None` (making it unresolvable by type, which an alias also can't reach).
This is checked eagerly during `validate()`, and again at resolve time if validation was skipped.

**Fix**

Register (and include) a provider for the source type before defining the alias:

```python
from modern_di import Group, Scope, providers


class Dependencies(Group):
# The alias's source must resolve by type — no bound_type=None here.
impl = providers.Factory(scope=Scope.APP, creator=Implementation)

interface_alias = providers.Alias(source_type=Implementation)
```

If the source provider lives in a different `Group`, make sure that group is also passed to
`Container(groups=[...])`. Run with `validate=True` so this is caught at startup rather than on first
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.
39 changes: 39 additions & 0 deletions docs/troubleshooting/argument-resolution-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# ArgumentResolutionError

**Symptom**

Raised naming a creator's parameter and the type it's annotated with, saying the argument couldn't be
resolved while building a given dependency — often rendered as a dependency-chain trace with a
`caused by:` line naming the specific parameter.

**Cause**

A creator parameter has no registered provider for its annotated type, no default value, and no
matching `kwargs` entry — so `modern-di` has nothing to inject. This also covers an unannotated
parameter with none of those escape routes, and a `ContextProvider`-backed parameter whose context
value is unset and required (not optional, no default).

**Fix**

Pick whichever applies: register a provider for the missing type, give the parameter a default, or
pass it explicitly via `kwargs`:

```python
class Dependencies(Group):
# missing: no provider for `Clock` anywhere
service = providers.Factory(scope=Scope.APP, creator=Service) # Service(clock: Clock)

# fix option 1: register a provider
clock = providers.Factory(scope=Scope.APP, creator=SystemClock, bound_type=Clock)

# fix option 2: pass explicitly
service2 = providers.Factory(scope=Scope.APP, creator=Service, kwargs={"clock": clock})
```

Check `.suggestions` on the caught exception for a "did you mean" hint when a similarly-named type is
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.
38 changes: 38 additions & 0 deletions docs/troubleshooting/async-finalizer-in-sync-close-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# AsyncFinalizerInSyncCloseError

**Symptom**

Arrives wrapped inside a `FinalizerError` (as one entry in `.finalizer_errors`), naming the type whose
cached instance has an async finalizer.

**Cause**

`close_sync()` cannot `await` anything. When it reaches a cached resource whose `CacheSettings`
finalizer is an async function, it can't run it synchronously, so it records this error for that entry
instead — and, unlike a normal finalizer failure, keeps the resource's cache entry intact rather than
discarding it.

**Fix**

Use `close_async()` (or `async with container:`) for containers that hold any resource with an async
finalizer — it's the only path that can actually run that cleanup:

```python
container.resolve(AsyncResource) # has an async finalizer

try:
container.close_sync()
except exceptions.FinalizerError as exc:
# exc.finalizer_errors contains an AsyncFinalizerInSyncCloseError — cache retained, not lost
...

await container.close_async() # recovers: runs the async finalizer, completes cleanup
```

Prefer `async with container:` (or `await close_async()`) by default whenever any provider might have
an async finalizer, and treat `close_sync()` as a fallback only for containers you know are entirely
sync.

## See also

- [Lifecycle: close-failure semantics](../providers/lifecycle.md#close-failure-semantics).
34 changes: 34 additions & 0 deletions docs/troubleshooting/child-container-registration-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# ChildContainerRegistrationError

**Symptom**

Raised from `Container.add_providers()`, naming the scope of the child container it was called on.

**Cause**

`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.

**Fix**

Call `add_providers()` on the root container instead:

```python
app_container = Container(scope=Scope.APP, groups=[MyGroup])
request_container = app_container.build_child_container(scope=Scope.REQUEST)

# Wrong
request_container.add_providers(late_provider) # raises ChildContainerRegistrationError

# Right
app_container.add_providers(late_provider)
```

If you only have a reference to the child container at the call site, keep a reference to the root
container around (e.g. store it at app startup) instead of walking up via `parent_container`.

## See also

- [Container: registering providers after construction](../providers/container.md#registering-providers-after-construction).
39 changes: 39 additions & 0 deletions docs/troubleshooting/container-closed-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# ContainerClosedError

**Symptom**

Raised when resolving from, or building a child of, a container after it was closed.

**Cause**

`container.close_sync()` / `close_async()` (or exiting a `with container:` block) marks the container
closed. In modern-di **3.0**, any further `resolve()`, `resolve_provider()`, or
`build_child_container()` call on that container raises this error instead of quietly working.

**Fix**

Re-enter the container before reusing it:

```python
with container:
container.resolve(Settings)
# closed here

with container: # reopened cleanly
container.resolve(Settings)
```

Or call `container.open()` explicitly if a context manager doesn't fit your flow. Build a fresh
container per unit of work (e.g. one per request) instead of trying to reuse a closed one across
units of work.

**Escape hatches**

Today (2.x), this reuse doesn't raise yet — it emits `ContainerClosedWarning` and self-reopens the
container so the call still succeeds. Escalate that warning to an error now to catch the pattern
before upgrading, using the readiness recipe on the migration page below.

## See also

- [Migration: closed containers raise instead of self-healing](../migration/to-3.x.md#1-closed-containers-raise-instead-of-self-healing).
- [Lifecycle: closing and reopening](../providers/lifecycle.md#closing-and-reopening).
47 changes: 47 additions & 0 deletions docs/troubleshooting/creator-call-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# CreatorCallError

**Symptom**

Raised naming the creator that could not be called and the underlying `TypeError`, with a pointer to
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
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
inside the creator's body (even a `TypeError`) propagates unchanged as itself, never wrapped in this
error.

**Fix**

Make `kwargs` cover exactly what the signature requires. `.original_error` (also the `__cause__`)
holds the binding `TypeError` naming the mismatched argument:

```python
def create_service(host: str, port: int) -> Service: ...


class Dependencies(Group):
# Wrong: skip_creator_parsing=True but kwargs misses `port`
service = providers.Factory(
scope=Scope.APP, creator=create_service, skip_creator_parsing=True,
bound_type=Service, kwargs={"host": "localhost"},
)

# Right
service = providers.Factory(
scope=Scope.APP, creator=create_service, skip_creator_parsing=True,
bound_type=Service, kwargs={"host": "localhost", "port": 5432},
)
```

Without `skip_creator_parsing`, unknown `kwargs` keys are caught earlier, at declaration time — see
the page below.

## See also

- [Unknown factory kwarg](unknown-factory-kwarg-error.md) — the declaration-time form of a kwargs mismatch.
- [Factories: skip_creator_parsing](../providers/factories.md#skip_creator_parsing).
Loading