From 5a9bb671d23c55d60386f5b632b11a2200949097 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 10 Oct 2026 22:26:22 +0300 Subject: [PATCH] docs: rework the wiring, alias, group and kwargs troubleshooting pages Quote the real error text on every page, fix examples that hid a DuplicateProviderTypeError, correct the missing-provider and circular-dependency fix advice, and make every Works example build a container so the docs tests catch regressions. --- .../alias-bound-to-source-error.md | 48 ++++---- .../alias-source-not-registered-error.md | 59 +++++----- .../argument-resolution-error.md | 108 ++++++++++++----- docs/troubleshooting/circular-dependency.md | 87 +++++++++++--- docs/troubleshooting/context-not-set.md | 59 +++++----- docs/troubleshooting/creator-call-error.md | 84 +++++++++----- docs/troubleshooting/duplicate-type-error.md | 109 +++++++++++------- .../group-instantiation-error.md | 47 +++++--- .../group-scope-conflict-error.md | 89 ++++++++++---- docs/troubleshooting/missing-provider.md | 84 ++++++++------ .../provider-scope-frozen-error.md | 84 ++++++++++---- .../unknown-factory-kwarg-error.md | 53 +++++---- .../unsupported-creator-parameter-error.md | 95 +++++++++++---- 13 files changed, 676 insertions(+), 330 deletions(-) diff --git a/docs/troubleshooting/alias-bound-to-source-error.md b/docs/troubleshooting/alias-bound-to-source-error.md index 04d1c8b4..61165357 100644 --- a/docs/troubleshooting/alias-bound-to-source-error.md +++ b/docs/troubleshooting/alias-bound-to-source-error.md @@ -2,51 +2,59 @@ ## Symptom -Raised when the `Alias(...)` is declared, before any container exists: - ``` -modern_di.exceptions.registration.AliasBoundToSourceError: Alias of is bound to its own source type, so it would resolve to itself. Pass bound_type= with the type the alias should answer for, such as a base class or Protocol, or bound_type=None to resolve it by reference only. +modern_di.exceptions.registration.AliasBoundToSourceError: Alias of is bound to its own source type, so it would resolve to itself. Pass bound_type= with the type the alias should answer for, such as a base class or Protocol, or bound_type=None to resolve it by reference only. See: https://modern-di.modern-python.org/troubleshooting/alias-bound-to-source-error/ ``` -`.source_type` holds the type the alias points at. It descends from `RegistrationError`, see +It is raised by the `Alias(...)` call itself, before any container exists. `.source_type` holds the +type the alias points at. The class descends from `RegistrationError`; see [Errors and exceptions](../providers/errors-and-exceptions.md). ## Cause An alias is registered under its `bound_type`. With `bound_type` equal to `source_type`, the alias -registers itself as the provider for its own source. If nothing else provides -that type, the alias resolves to itself, a one-node cycle. If something does, the two collide as -duplicates. Neither does anything useful, so the declaration is rejected. - -## Fix - -Pass the type the alias should answer for: +registers itself as the provider for its own source. If nothing else provides that type, the alias +resolves to itself, a one-node cycle. If something does, the two collide as duplicates. Neither does +anything useful, so the declaration is rejected. -```python + + + +```python +# Broken: +providers.Alias(Implementation, bound_type=Implementation) +``` +## Fix + +Pass the type the alias should answer for, such as a base class or a `Protocol`: + +```python +# Works: class Dependencies(Group): impl = providers.Factory(Implementation) - - # Broken: providers.Alias(Implementation, bound_type=Implementation) - # Works: interface_alias = providers.Alias(Implementation, bound_type=Interface) + + +assert isinstance(Container(groups=[Dependencies]).resolve(Interface), Implementation) ``` -To use the alias only by reference, through `container.resolve_provider(...)` or as a -`kwargs=` value, pass `bound_type=None`. +To use the alias only by reference, through `container.resolve_provider(...)` or as a `kwargs=` +value, pass `bound_type=None`. ## See also -- [Alias](../providers/alias.md) covers binding one type to an already-registered provider. -- [AliasSourceNotRegisteredError](alias-source-not-registered-error.md) covers an alias whose source has no provider. +- [Alias](../providers/alias.md): binding one type to an already registered provider. +- [AliasSourceNotRegisteredError](alias-source-not-registered-error.md): an alias whose source has no provider. diff --git a/docs/troubleshooting/alias-source-not-registered-error.md b/docs/troubleshooting/alias-source-not-registered-error.md index df6b8aea..aa5296af 100644 --- a/docs/troubleshooting/alias-source-not-registered-error.md +++ b/docs/troubleshooting/alias-source-not-registered-error.md @@ -2,53 +2,60 @@ ## Symptom -Raised naming the `source_type` an `Alias` points at, saying no provider is registered for it. - -## Cause +``` +modern_di.exceptions.resolution.AliasSourceNotRegisteredError: Cannot resolve dependency chain: + APP Interface + caused by: Alias source type is not registered in providers registry. Register a provider for before the alias is resolved. +See: https://modern-di.modern-python.org/troubleshooting/alias-source-not-registered-error/ +``` -`Alias(X, bound_type=...)` 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. +`resolve()` and `resolve_provider()` raise it with the chain shown above. `container.validate()` +reports it inside [`ValidationFailedError`](validation-failed-error.md), without the chain. `.source_type` +holds the type the alias points at. -## Fix +## Cause -Register (and include) a provider for the source type. It only has to be registered by the time the alias is first resolved: +`Alias(X, bound_type=...)` delegates to whatever provider is registered under `X`, and none is. One of +these applies: - +Register a provider for the source type under that type, and pass its group to the container: ```python from typing import Protocol -from modern_di import Group, Scope, providers +from modern_di import Container, Group, Scope, providers class Interface(Protocol): ... +class Implementation: ... + + class Dependencies(Group): - # The alias's source must resolve by type — no bound_type=None here. impl = providers.Factory(Implementation, scope=Scope.APP) - interface_alias = providers.Alias(Implementation, bound_type=Interface) -``` - +container = Container(groups=[Dependencies]) +container.validate() +assert isinstance(container.resolve(Interface), Implementation) +``` -If the source provider lives in a different `Group`, make sure that group is also passed to -`Container(groups=[...])`. Call `container.validate()` so this is caught at startup rather than on first -resolve. +The source only has to be registered by the time the alias is first resolved, so a provider added +later with `container.add_providers(...)` also works. Calling `container.validate()` at startup +reports a missing source before the first request. ## See also -- [Alias](../providers/alias.md) covers binding one type to an already-registered provider. -- [No provider registered for type](missing-provider.md) covers the same "unregistered type" problem, without an alias in the way. +- [Alias](../providers/alias.md): binding one type to an already registered provider. +- [ProviderNotRegisteredError](missing-provider.md): the same gap without an alias in the way. +- [ValidationFailedError](validation-failed-error.md): how `validate()` groups the errors it finds. diff --git a/docs/troubleshooting/argument-resolution-error.md b/docs/troubleshooting/argument-resolution-error.md index 441aff37..e4a9b0c1 100644 --- a/docs/troubleshooting/argument-resolution-error.md +++ b/docs/troubleshooting/argument-resolution-error.md @@ -2,24 +2,47 @@ ## 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. +``` +modern_di.exceptions.resolution.ArgumentResolutionError: Cannot resolve dependency chain: + APP Service (myapp.clock:10) + caused by: Argument clock of type cannot be resolved. Trying to build dependency . +Did you mean: + - SystemClock (registered subclass, scope=APP) +See: https://modern-di.modern-python.org/troubleshooting/argument-resolution-error/ +``` + +The chain shows the provider being built, and the `caused by:` line names the parameter and its +annotation. The `Did you mean:` block appears only when a registered type looks like the one +requested: a registered subclass, a registered base class, or a type with a similar name. Two other +forms of the `caused by:` line exist: + +- `Argument dep of type A | B cannot be resolved.` for a union parameter where no member has a provider. +- `Argument x has no usable type annotation, so it cannot be resolved by type. Pass it via the kwargs parameter or add a type annotation.` for an unannotated parameter. + +`container.validate()` reports the same error inside +[`ValidationFailedError`](validation-failed-error.md). The exception carries `.parameter_name`, +`.parameter_type`, `.member_types` (the union members, for a union parameter), `.bound_type`, +`.creator` and `.suggestions`. ## 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. A required `ContextProvider`-backed parameter whose -context value is unset raises [`ContextValueNotSetError`](context-not-set.md) instead. +A creator parameter has nothing to inject: no provider is registered for its annotated type, it has +no default, it is not nullable (`X | None`), and `kwargs` has no entry for it. The usual reasons: -## Fix +- The type has no provider, or its group was not passed to `Container(groups=[...])`. +- The provider exists under another type. A `Factory(SystemClock)` registers under `SystemClock`, so a + parameter typed `Clock` does not find it. This is the case the `registered subclass` hint points at. +- The parameter has no type annotation. +- The parameter is a union. `modern-di` injects the first member, in the order the union lists them, + that has a provider, and raises this error when none has one. -Pick whichever applies: register a provider for the missing type, give the parameter a default, or -pass it explicitly via `kwargs`: +A required parameter backed by a `ContextProvider` whose value is unset raises +[`ContextValueNotSetError`](context-not-set.md) instead. + +## Fix + + +```python +# Broken: +class Dependencies(Group): + system_clock = providers.Factory(SystemClock, scope=Scope.APP) + service = providers.Factory(Service, scope=Scope.APP) + + +Container(groups=[Dependencies]).resolve(Service) +``` + +Register the provider under the type the parameter asks for: + ```python +# Works: class Dependencies(Group): - # missing: no provider for `Clock` anywhere - service = providers.Factory(Service, scope=Scope.APP) # Service(clock: Clock) + system_clock = providers.Factory(SystemClock, scope=Scope.APP, bound_type=Clock) + service = providers.Factory(Service, scope=Scope.APP) - # fix option 1: register a provider - clock = providers.Factory(SystemClock, scope=Scope.APP, bound_type=Clock) - # fix option 2: pass explicitly - service2 = providers.Factory(Service, scope=Scope.APP, kwargs={"clock": clock}) +assert isinstance(Container(groups=[Dependencies]).resolve(Service).clock, SystemClock) ``` -If the missing type is one a framework -integration provides at runtime (`fastapi.Request`, `taskiq.TaskiqMessage`, …), its -`ContextProvider` is registered by `setup_di()`, so a `container.validate()` call made -*before* `setup_di()` runs sees no provider for it yet and raises. Call `validate()` after -`setup_di()`, when the provider is registered. A factory that must also resolve outside a -request can take `request: fastapi.Request | None = None`: once `setup_di()` has registered the -provider, the parameter gets the request inside one and `None` outside. See +Or pass the provider explicitly through `kwargs`. With `bound_type=None` the clock is reachable only +by reference: + +```python +# Works: +class Dependencies(Group): + system_clock = providers.Factory(SystemClock, scope=Scope.APP, bound_type=None) + service = providers.Factory(Service, scope=Scope.APP, kwargs={"clock": system_clock}) + + +assert isinstance(Container(groups=[Dependencies]).resolve(Service).clock, SystemClock) +``` + +Giving the parameter a default, or annotating it as `Clock | None`, also stops the error; the creator +then gets the default or `None`. + +### Framework types + +A type such as `fastapi.Request` or `taskiq.TaskiqMessage` gets its `ContextProvider` from the +integration's `setup_di()`. A `container.validate()` call made before `setup_di()` sees no provider +for it and reports this error, so call `validate()` after `setup_di()`. A factory that must also +resolve outside a request can take `request: fastapi.Request | None = None`: it gets the request +inside one and `None` outside. See [Framework context objects](../providers/context.md#framework-context-objects) and [Optional parameters](../providers/context.md#optional-parameters). -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) describes the direct-resolve form of this same gap. -- [Factories](../providers/factories.md#creator) explains how parameters are parsed and wired. +- [ProviderNotRegisteredError](missing-provider.md): the same gap, hit by a direct `resolve`. +- [Factories: creator](../providers/factories.md#creator): how creator parameters are parsed and wired. +- [Factories: union type parameters](../providers/factories.md#union-type-parameters): which union member is injected. +- [Framework context objects](../providers/context.md#framework-context-objects): the providers integrations register. diff --git a/docs/troubleshooting/circular-dependency.md b/docs/troubleshooting/circular-dependency.md index 395d74f0..860f1030 100644 --- a/docs/troubleshooting/circular-dependency.md +++ b/docs/troubleshooting/circular-dependency.md @@ -1,10 +1,19 @@ # CircularDependencyError -This error occurs when providers form a dependency cycle, meaning A depends on B which depends back on A (directly or through intermediate providers). - ## Symptom -When you see this error: +Resolving a provider that sits on a cycle raises: + +``` +modern_di.exceptions.resolution.CircularDependencyError: Circular dependency detected: + APP ServiceA (myapp.cycle:6) + APP └─> ServiceB (myapp.cycle:11) + APP └─> ServiceA (myapp.cycle:6) +Check your provider graph for unintended cycles. +See: https://modern-di.modern-python.org/troubleshooting/circular-dependency/ +``` + +`container.validate()` reports the same cycle inside [`ValidationFailedError`](validation-failed-error.md): ``` + Exception Group Traceback (most recent call last): @@ -14,16 +23,22 @@ When you see this error: +-+---------------- 1 ---------------- | modern_di.exceptions.resolution.CircularDependencyError: Circular dependency detected: | APP ServiceA (myapp.cycle:6) - | APP └─> ServiceB (myapp.cycle:10) + | APP └─> ServiceB (myapp.cycle:11) | APP └─> ServiceA (myapp.cycle:6) | Check your provider graph for unintended cycles. | See: https://modern-di.modern-python.org/troubleshooting/circular-dependency/ +------------------------------------ ``` -It means the listed providers form a cycle that cannot be resolved. Each hop in the arrow chain may also end with a pointer to where that provider was declared (module and line number), making it easier to locate the offending provider in a large codebase. +Each line is one hop of the cycle, with its scope and, for a `Factory`, the module and line where its +creator is declared. `.steps` holds the hops, and `.cycle_path` and `.cycle_locations` hold their +names and locations. + +## Cause -## How to detect +The providers form a loop: A depends on B, which depends back on A, directly or through other +providers. The loop can run through a parameter's type annotation, a `kwargs` entry that holds a +provider, or an `Alias`. ### The runtime cycle guard (without `validate()`) @@ -31,22 +46,25 @@ Resolving from an unvalidated cyclic graph still raises `CircularDependencyError resolve overflows the stack, and `Container.resolve_provider` catches that `RecursionError`, re-walks the static graph from the failing provider, and, since a cycle is reachable, raises `CircularDependencyError` (with the same cycle-path rendering shown above) `from` the original -`RecursionError`. A creator that merely recurses on its own, with no actual cycle in the provider -graph, still raises the original `RecursionError` unchanged. This guard runs on every resolve, whether or not `validate()` was ever called. +`RecursionError`. A creator that recurses on its own, with no cycle in the provider graph, still +raises the original `RecursionError` unchanged. This guard runs on every resolve, whether or not +`validate()` was ever called. The guard covers resolution on one thread. Each cached factory locks its cache item while it is created, so two threads that cold-resolve different providers of the same cycle at the same time can each hold one of those locks and wait for the other forever, and neither reaches the guard. If the graph might have a cycle, call `validate()` at startup, before any thread resolves. -### Cycle detection with `validate()` +## Fix -Calling `validate()` up front finds the *same* cycle earlier, and finds *every* issue in the graph -in one pass (not just the one a particular resolve happens to hit). Prefer it in development: +Find every cycle at startup with `validate()`. It walks the whole graph in one pass and reports each +issue it finds, where a resolve reports only the cycle it happens to hit: ```python -# Broken: TenantId provider has scope=Scope.REQUEST, so it reads the REQUEST -# container's registry. Setting it on the APP parent does nothing. -app_container.set_context(TenantId, TenantId("acme")) # ignored for REQUEST-scoped providers +# 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) ``` -To fix it, set the value on the container whose scope matches the provider's scope: +Set the value on the container whose scope matches the provider's scope, either when building it or +afterwards: ```python -# Option A: pass directly to the child when building it +# Works: request_container = app_container.build_child_container( scope=Scope.REQUEST, context={TenantId: TenantId("acme")}, ) -# Option B: set on the request container after building it +# Works: request_container = app_container.build_child_container(scope=Scope.REQUEST) request_container.set_context(TenantId, TenantId("acme")) ``` -### 2. The `ContextProvider`'s scope doesn't match where you set the context - -`ContextProvider(TenantId, scope=Scope.APP)` looks up the value on the APP container. If you `set_context` on the REQUEST child container, the APP-scope provider doesn't see it. +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. -Make the scopes match. If the value is per-request, declare `ContextProvider(TenantId, scope=Scope.REQUEST)` and `set_context` on the request container (or pass via `build_child_container(context=...)`). - -### 3. Framework integration didn't inject the expected request +### 2. Framework integration didn't inject the expected request Framework integrations (`modern-di-fastapi`, `modern-di-litestar`) register the per-request `Request`/`WebSocket` automatically. If your code expects, say, `fastapi.Request` but you're outside the framework's request lifecycle (a background task, a CLI command), no `Request` is in context and the lookup fails. Depend on framework-injected context only inside the framework's request handling. For background tasks, build the REQUEST child container yourself and pass the necessary context. -### 4. The value is optional - -If a creator runs where no value is set, such as a handler that also runs outside a request, make its parameter optional. With no value set it gets its default, or `None` for an `X | None` parameter without one: +### 3. The value is optional -```python -class MyService: - def __init__(self, tenant: TenantId | None = None) -> None: - self.tenant = tenant -``` +If a creator also runs where no value is set, make its parameter optional (`tenant: TenantId | None = None`): +it then gets its default, or `None`. A parameter whose provider is another `Factory` that needs the +value still raises. See [Optional parameters](../providers/context.md#optional-parameters). -This works for an integration's provider too, which stays required for a direct resolve. It applies only to a parameter that takes the context value itself; a parameter whose provider is another `Factory` that needs the value still raises. - -To make the value optional for every consumer, direct resolves included, give the provider a default. The provider returns `default=` whenever nothing is set: +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): ```python tenant = providers.ContextProvider(TenantId, scope=Scope.REQUEST, default=None) ``` -See [When no value is set](../providers/context.md#when-no-value-is-set). - ## See also -- [Context providers](../providers/context.md) documents the full `ContextProvider` and `set_context` API. -- [Scopes](../providers/scopes.md) explains per-container context registries and why context never propagates between containers. -- [Async resources via lifespan](../recipes/async-lifespan.md) shows the canonical "construct in lifespan, inject as context" pattern. +- [Context providers](../providers/context.md): the full `ContextProvider` and `set_context` API. +- [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. diff --git a/docs/troubleshooting/creator-call-error.md b/docs/troubleshooting/creator-call-error.md index 154f42b9..53d6af65 100644 --- a/docs/troubleshooting/creator-call-error.md +++ b/docs/troubleshooting/creator-call-error.md @@ -2,53 +2,85 @@ ## 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. +``` +modern_di.exceptions.resolution.CreatorCallError: Cannot resolve dependency chain: + APP Service (myapp.creator:7) + caused by: Failed to call creator create_service: create_service() missing 1 required positional argument: 'port'. Check kwargs and skip_creator_parsing usage. +See: https://modern-di.modern-python.org/troubleshooting/creator-call-error/ +``` + +It is raised at resolve time. `.creator` holds the creator, and `.original_error` holds the binding +`TypeError`, which is also the exception's `__cause__`. ## 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. Either a required argument is -missing, or an unexpected one was passed. This typically happens with `skip_creator_parsing=True` -(where every required argument must be covered by `kwargs`) or a `kwargs` dict that drifted from the -signature. This is a wiring problem, not a bug inside your constructor. An exception raised -inside the creator's body (even a `TypeError`) propagates unchanged as itself, never wrapped in this -error. +The arguments `modern-di` assembled, static `kwargs` plus resolved dependencies, do not fit the +creator's signature. This usually comes from `skip_creator_parsing=True`, where nothing is wired +by type and `kwargs` must supply every required argument. The `TypeError` then says which part is +wrong: + +- `missing 1 required positional argument`: `kwargs` leaves out a required parameter. +- `got an unexpected keyword argument`: `kwargs` has a key the creator does not take. +- `got some positional-only arguments passed as keyword arguments`: `kwargs` names a positional-only + parameter. + +`container.validate()` cannot catch any of these, because with `skip_creator_parsing=True` there is +no parsed signature to check `kwargs` against. Without `skip_creator_parsing`, an unknown key is +rejected when the `Factory` is declared, as +[`UnknownFactoryKwargError`](unknown-factory-kwarg-error.md). + +An exception raised inside the creator's body, a `TypeError` included, propagates as itself and is +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: +Make `kwargs` cover exactly what the signature requires. -```python -def create_service(host: str, port: int) -> Service: ... +def create_service(host: str, port: int) -> Service: + return Service() +--> + + +```python +# Broken: class Dependencies(Group): - # Broken: skip_creator_parsing=True but kwargs misses `port` service = providers.Factory( - create_service, scope=Scope.APP, skip_creator_parsing=True, - bound_type=Service, kwargs={"host": "localhost"}, + create_service, + scope=Scope.APP, + skip_creator_parsing=True, + bound_type=Service, + kwargs={"host": "localhost"}, ) - # Works + +Container(groups=[Dependencies]).resolve(Service) +``` + +```python +# Works: +class Dependencies(Group): service = providers.Factory( - create_service, scope=Scope.APP, skip_creator_parsing=True, - bound_type=Service, kwargs={"host": "localhost", "port": 5432}, + create_service, + scope=Scope.APP, + 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. + +assert isinstance(Container(groups=[Dependencies]).resolve(Service), Service) +``` ## See also -- [Unknown factory kwarg](unknown-factory-kwarg-error.md) covers the declaration-time form of a kwargs mismatch. -- [Factories: skip_creator_parsing](../providers/factories.md#skip_creator_parsing). +- [UnknownFactoryKwargError](unknown-factory-kwarg-error.md): the declaration-time form of a `kwargs` mismatch. +- [Factories: skip_creator_parsing](../providers/factories.md#skip_creator_parsing): what turning off wiring changes. +- [UnsupportedCreatorParameterError](unsupported-creator-parameter-error.md): parameter shapes that cannot be wired by type. diff --git a/docs/troubleshooting/duplicate-type-error.md b/docs/troubleshooting/duplicate-type-error.md index d39d3c4b..f8c124d1 100644 --- a/docs/troubleshooting/duplicate-type-error.md +++ b/docs/troubleshooting/duplicate-type-error.md @@ -1,85 +1,114 @@ # DuplicateProviderTypeError -This error occurs when two or more providers are registered with the same `bound_type`. `modern-di` uses the `bound_type` to resolve dependencies by type, so each type must be unique in the providers registry. - ## Symptom -When you see this error: - ``` -modern_di.exceptions.registration.DuplicateProviderTypeError: Provider is duplicated by type . - - Factory (myapp.ioc:12) - - Factory (myapp.other:30) +modern_di.exceptions.registration.DuplicateProviderTypeError: Provider is duplicated by type . + - Factory (myapp.dup:7) + - Factory (myapp.dup:10) Set bound_type=None on one of them to make it resolvable by reference only. See: https://modern-di.modern-python.org/troubleshooting/duplicate-type-error/ ``` -Each line names one of the two providers, the one registered first on top: a `Factory` by where its -creator is declared, any other provider by its repr. `.first_provider` and `.second_provider` hold -the provider objects. +It is raised when the second provider is registered: by `Container(groups=[...])` at startup, or by +`container.add_providers(...)`. Each bullet names one provider, the one registered first on top. A +`Factory` is shown by where its creator is declared and any other provider by its repr, so two +factories that share a creator show the same location twice. `.first_provider` and +`.second_provider` hold the provider objects. -It descends from `RegistrationError` → `ModernDIError` → `RuntimeError`, so `except DuplicateProviderTypeError`, `except RegistrationError`, and `except RuntimeError` all catch it. See [Errors and exceptions](../providers/errors-and-exceptions.md). +The class descends from `RegistrationError`, then `ModernDIError`, then `RuntimeError`, so +`except DuplicateProviderTypeError`, `except RegistrationError` and `except RuntimeError` all catch +it. See [Errors and exceptions](../providers/errors-and-exceptions.md). -This typically happens when: +## Cause -1. You have multiple factories that return the same type -2. You're using the same class in different contexts with different configurations +`modern-di` resolves by type, so each type maps to at most one provider. Two providers end up with the +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. ## Fix -Set `bound_type=None` on one of the duplicate providers to make it unresolvable by type. A factory -that needs that provider then gets it explicitly through `kwargs`, since resolving by type finds only -the other one. The example below does both: - -```python -from modern_di import Group, Scope, providers +If the two providers stand for different things, give each its own type. A thin subclass is enough, +and each consumer asks for the one it needs: + +```python class DatabaseConfig: def __init__(self, connection_string: str) -> None: self.connection_string = connection_string +class PrimaryConfig(DatabaseConfig): ... + + +class ReplicaConfig(DatabaseConfig): ... + + +class Configs(Group): + primary = providers.Factory( + PrimaryConfig, scope=Scope.APP, kwargs={"connection_string": "postgresql://primary"} + ) + replica = providers.Factory( + ReplicaConfig, scope=Scope.APP, kwargs={"connection_string": "postgresql://replica"} + ) + + +container = Container(groups=[Configs]) +assert container.resolve(ReplicaConfig).connection_string == "postgresql://replica" +``` + +Otherwise set `bound_type=None` on one provider to take it out of the by-type lookup, and hand it to +its consumers through `kwargs`. Every provider that would collide needs it, consumers included: + +```python class Repository: def __init__(self, db_config: DatabaseConfig) -> None: self.db_config = db_config class MyGroup(Group): - # Step 1: Set bound_type=None on the secondary provider or for both providers - # This provider can be resolved by type: container.resolve(DatabaseConfig) primary_db_config = providers.Factory( DatabaseConfig, scope=Scope.APP, - kwargs={"connection_string": "postgresql://primary"} + kwargs={"connection_string": "postgresql://primary"}, ) - - # This provider cannot be resolved by type - # Must use: container.resolve_provider(MyGroup.secondary_db_config) secondary_db_config = providers.Factory( DatabaseConfig, scope=Scope.APP, - bound_type=None, # <-- Step 1: Makes it unresolvable by type - kwargs={"connection_string": "postgresql://secondary"} - ) - - # Step 2: Explicitly pass dependencies via kwargs for second repository or for both - primary_repository = providers.Factory( - Repository, # <-- Implicit dependency, no kwargs - scope=Scope.APP, + bound_type=None, + kwargs={"connection_string": "postgresql://secondary"}, ) + primary_repository = providers.Factory(Repository, scope=Scope.APP) secondary_repository = providers.Factory( Repository, scope=Scope.APP, - kwargs={"db_config": secondary_db_config} # <-- Step 2: Explicit dependency + bound_type=None, + kwargs={"db_config": secondary_db_config}, ) + + +container = Container(groups=[MyGroup]) +assert container.resolve(Repository).db_config.connection_string == "postgresql://primary" +secondary = container.resolve_provider(MyGroup.secondary_repository) +assert secondary.db_config.connection_string == "postgresql://secondary" ``` -## See also +`container.resolve(DatabaseConfig)` and `container.resolve(Repository)` return the primary pair. The +secondary pair is reachable only by reference, through `container.resolve_provider(...)` or as a +`kwargs` value. + +To make an abstract type resolve to a concrete implementation, use an [`Alias`](../providers/alias.md) +instead of a second factory. -- [Factories](../providers/factories.md#bound_type), the `bound_type` section. -- [Errors and exceptions](../providers/errors-and-exceptions.md) -- [Missing provider](../troubleshooting/missing-provider.md) +## See also -For binding an abstract type to a concrete implementation, `Alias` is preferred over duplicate factories. +- [Factories: `bound_type`](../providers/factories.md#bound_type): how the bound type is inferred and overridden. +- [Alias](../providers/alias.md): binding one type to an already registered provider. +- [ProviderNotRegisteredError](missing-provider.md): the opposite problem, a type with no provider. +- [Errors and exceptions](../providers/errors-and-exceptions.md): where this error sits in the hierarchy. diff --git a/docs/troubleshooting/group-instantiation-error.md b/docs/troubleshooting/group-instantiation-error.md index f6e14931..45055f9a 100644 --- a/docs/troubleshooting/group-instantiation-error.md +++ b/docs/troubleshooting/group-instantiation-error.md @@ -2,45 +2,56 @@ ## Symptom -Raised naming the `Group` subclass someone tried to instantiate, saying it cannot be created as an -object. +``` +modern_di.exceptions.lifecycle.GroupInstantiationError: Dependencies cannot be instantiated +See: https://modern-di.modern-python.org/troubleshooting/group-instantiation-error/ +``` + +`.group_name` holds the name of the `Group` subclass. ## Cause -A `Group` subclass was called like a constructor (`MyGroup()`). Groups are namespaces for declaring -providers as class attributes. They're never meant to be instantiated, only passed by class -reference to `Container(groups=[MyGroup])` or read via `MyGroup.some_provider`. +A `Group` subclass was called like a constructor, `Dependencies()`. A group is a namespace for +declaring providers as class attributes, and calling it always raises, with or without arguments. +You pass the class itself to `Container(groups=[Dependencies])` and read its providers as +`Dependencies.some_provider`. -## Fix +The habit often comes from `dependency-injector` and `that-depends`, where the container class is +also the runtime object you resolve from. The migration guides cover the difference: +[from `dependency-injector`](../migration/from-dependency-injector.md#2-key-conceptual-shifts) and +[from `that-depends`](../migration/from-that-depends.md#2-key-conceptual-shifts). -Use the class itself, not an instance: +## Fix - -```python class Dependencies(Group): service = providers.Factory(Service, scope=Scope.APP) +--> + -# Broken -deps = Dependencies() # raises GroupInstantiationError +```python +# Broken: +deps = Dependencies() +``` -# Works +Pass the class to a `Container` and resolve from the container: + +```python +# Works: container = Container(groups=[Dependencies]) service = container.resolve_provider(Dependencies.service) +assert isinstance(service, Service) ``` -This usually happens from a habit carried over from frameworks where a container/module *is* -instantiated, or from accidentally writing `Dependencies()` instead of `Dependencies` in a type -annotation or default value. - ## See also -- [Multi-Group organization](../recipes/multi-group.md) covers organizing providers across several `Group` classes. +- [Organize a large container with multiple Groups](../recipes/multi-group.md): splitting providers across several `Group` classes. +- [Migration from `dependency-injector`](../migration/from-dependency-injector.md): `Group` as schema, `Container` as runtime. +- [Migration from `that-depends`](../migration/from-that-depends.md): the same split, coming from `BaseContainer`. diff --git a/docs/troubleshooting/group-scope-conflict-error.md b/docs/troubleshooting/group-scope-conflict-error.md index dbc12147..7e426241 100644 --- a/docs/troubleshooting/group-scope-conflict-error.md +++ b/docs/troubleshooting/group-scope-conflict-error.md @@ -2,50 +2,99 @@ ## Symptom -Defining a `Group` subclass raises at class-creation (import) time. The error names a provider -and the two groups that disagree about its scope. - -## Cause +``` +modern_di.exceptions.registration.GroupScopeConflictError: Provider SomeService is shared by groups with conflicting default scopes: GroupA (scope REQUEST) and GroupB (scope ACTION). Set scope= explicitly on the provider, or align the group defaults. +See: https://modern-di.modern-python.org/troubleshooting/group-scope-conflict-error/ +``` -A module-level provider instance was created without an explicit `scope=`, so it takes its scope -from whichever `class ...(Group, scope=...)` body stamps it first. When that same instance is -also referenced from a second group whose default scope differs, the two stamps conflict: the -provider cannot have two different scopes, and import order must never be what silently decides -which one wins. +It is raised by the second group's `class` statement, usually at import time. The first group named +is the one that set the provider's scope. The exception carries `.provider`, `.first_group`, +`.first_scope`, `.second_group` and `.second_scope`. -## Fix +## Cause -There are three ways to resolve it; pick whichever fits: +A provider declared without `scope=` takes its scope from the first `class ...(Group, scope=...)` +that lists it. When a second group with a different default scope lists the same provider instance, +the provider would need two scopes at once. Import order must not pick the winner, so `modern-di` +rejects the second group. This happens whether or not a container has registered the provider yet. + + +```python +# Broken: +shared = providers.Factory(SomeService) + + +class GroupA(Group, scope=Scope.REQUEST): + svc = shared + + +class GroupB(Group, scope=Scope.ACTION): + svc = shared +``` + +## Fix + +Set `scope=` on the shared provider. An explicit scope always wins over a group default, so the two +groups no longer disagree: + ```python -# 1. Set scope= explicitly on the shared provider — explicit always wins over a group default. shared = providers.Factory(SomeService, scope=Scope.REQUEST) -# 2. Align the two groups' default scopes so they agree. + +class GroupA(Group, scope=Scope.REQUEST): + svc = shared + + +class GroupB(Group, scope=Scope.ACTION): + svc = shared + + +assert shared.scope == Scope.REQUEST +``` + +Or align the two groups' default scopes: + +```python +shared = providers.Factory(SomeService) + + class GroupA(Group, scope=Scope.REQUEST): svc = shared + class GroupB(Group, scope=Scope.REQUEST): svc = shared +``` -# 3. Give each group its own provider instance instead of sharing one. +In both of these, pass only one of the two groups to a given container. They list the same provider +instance, and `Container(groups=[GroupA, GroupB])` currently raises +[`DuplicateProviderTypeError`](duplicate-type-error.md) naming that one provider twice. + +Or give each group its own provider instance. The two then need different types, or `bound_type=None` +on one, to avoid a [`DuplicateProviderTypeError`](duplicate-type-error.md) when both groups go into +one container: + +```python class GroupA(Group, scope=Scope.REQUEST): svc = providers.Factory(SomeService) + class GroupB(Group, scope=Scope.ACTION): - svc = providers.Factory(SomeService) -``` + svc = providers.Factory(SomeService, bound_type=None) -Inspect `.provider`, `.first_group`/`.first_scope`, and `.second_group`/`.second_scope` on -the exception to see exactly which provider and groups collided. + +container = Container(groups=[GroupA, GroupB]) +``` ## See also -- [Scopes](../providers/scopes.md) explains the scope hierarchy and how a provider's scope is chosen. +- [Scopes: group-level default scope](../providers/scopes.md#group-level-default-scope): how a provider's scope is chosen. +- [ProviderScopeFrozenError](provider-scope-frozen-error.md): a group changing the scope of an already registered provider. diff --git a/docs/troubleshooting/missing-provider.md b/docs/troubleshooting/missing-provider.md index e4848c15..a23fc92e 100644 --- a/docs/troubleshooting/missing-provider.md +++ b/docs/troubleshooting/missing-provider.md @@ -1,32 +1,32 @@ -# No provider registered for type +# ProviderNotRegisteredError -This error fires when a creator parameter is typed `Foo` and the container has no registered provider for `Foo`. +`container.resolve(SomeType)` raises this when no provider is registered under `SomeType`. When the +missing type is a parameter of a registered provider's creator, the error is +[`ArgumentResolutionError`](argument-resolution-error.md) instead. ## Symptom -Resolving an unregistered type directly raises: - ``` modern_di.exceptions.resolution.ProviderNotRegisteredError: No provider is registered for MissingDep. See: https://modern-di.modern-python.org/troubleshooting/missing-provider/ ``` -Resolving a registered factory whose creator depends on an unregistered type raises: +When a subclass, a base class or a similarly named type is registered, the message lists it: ``` -modern_di.exceptions.resolution.ArgumentResolutionError: Cannot resolve dependency chain: - APP MyService (myapp.missing:7) - caused by: Argument dep of type cannot be resolved. Trying to build dependency . -See: https://modern-di.modern-python.org/troubleshooting/argument-resolution-error/ +modern_di.exceptions.resolution.ProviderNotRegisteredError: No provider is registered for Clock. +Did you mean: + - SystemClock (registered subclass, scope=APP) +See: https://modern-di.modern-python.org/troubleshooting/missing-provider/ ``` -The resolver walked the creator's signature, found a parameter typed `MissingDep`, and found nothing for it in the providers registry. The "dependency chain" header shows where in the resolution graph the miss occurred. +`.dependency_type` holds the type you asked for, and `.suggestions` holds the "Did you mean" entries. ## Cause ### 1. The group containing the provider was not passed to `Container` -This is the most common cause. If you split providers across `Database`, `UseCases`, `Cache`, you have to list them all: +If you split providers across `Database`, `UseCases` and `Cache`, list them all: + + ```python -# Broken: cannot resolve by type -def create_engine(settings: Settings): - return sa_async.create_async_engine(settings.database_url) +# Broken: +def create_engine(): + return Engine() -# Works: return-typed -def create_engine(settings: Settings) -> sa_async.AsyncEngine: - return sa_async.create_async_engine(settings.database_url) -``` -To fix it, add the return annotation, or set `bound_type=SomeType` on the provider explicitly. +container = Container(groups=[]) +container.add_providers(providers.Factory(create_engine, scope=Scope.APP)) +container.resolve(Engine) +``` -### 3. `bound_type=None` was set on the provider you want to resolve +Add the return annotation, or pass `bound_type=Engine` to the provider: -`bound_type=None` makes the provider unresolvable by type. It's a deliberate opt-out for cases where two providers return the same type (see [Duplicate provider type](duplicate-type-error.md)). If you set it on the wrong provider, the type lookup misses. +```python +# Works: +def create_engine() -> Engine: + return Engine() -Leave `bound_type` at its default on the provider you want resolvable by type. If both providers really do produce the same type, resolve the unresolvable one by reference (`container.resolve_provider(...)`). -### 4. The parameter is a union and the chosen branch isn't registered +container = Container(groups=[]) +container.add_providers(providers.Factory(create_engine, scope=Scope.APP)) +assert isinstance(container.resolve(Engine), Engine) +``` -For `dep: A | B`, `modern-di` resolves the *first* type in the union order that has a registered provider. If neither is registered, the resolver fails. +### 3. `bound_type=None` was set on the provider you want to resolve -Register a provider for one of the union types, or annotate the parameter with a concrete type. +`bound_type=None` takes a provider out of the by-type lookup. It is the opt-out for two providers that +return the same type (see [DuplicateProviderTypeError](duplicate-type-error.md)), and setting it on +the wrong one makes the lookup miss. Leave `bound_type` at its default on the provider you resolve by +type, and resolve the other one by reference with `container.resolve_provider(...)`. ## See also -- [Resolving](../introduction/resolving.md) describes the by-type lookup algorithm. -- [Duplicate provider type](duplicate-type-error.md) covers the inverse problem, where two providers compete for the same type. -- [Factories: `bound_type`](../providers/factories.md) explains how the bound type is inferred and how to override it. +- [Resolving dependencies](../introduction/resolving.md): the by-type lookup. +- [ArgumentResolutionError](argument-resolution-error.md): the same gap, hit by a creator parameter. +- [DuplicateProviderTypeError](duplicate-type-error.md): two providers competing for one type. +- [Factories: `bound_type`](../providers/factories.md#bound_type): how the bound type is inferred and overridden. diff --git a/docs/troubleshooting/provider-scope-frozen-error.md b/docs/troubleshooting/provider-scope-frozen-error.md index 07cf2a2b..0e1c5c3f 100644 --- a/docs/troubleshooting/provider-scope-frozen-error.md +++ b/docs/troubleshooting/provider-scope-frozen-error.md @@ -2,9 +2,13 @@ ## Symptom -Defining a `Group` subclass raises at class-creation (import) time. The error names a provider, the -group that tried to change its scope, and the two scopes involved. It also says the provider is -already registered with a container. +``` +modern_di.exceptions.registration.ProviderScopeFrozenError: Group ScopedGroup would change the scope of provider SomeService from APP to REQUEST, but it is already registered with a container. Resolvers compiled before this point captured APP, so the change would apply inconsistently. Declare ScopedGroup before building the container, or set scope= explicitly on the provider. +See: https://modern-di.modern-python.org/troubleshooting/provider-scope-frozen-error/ +``` + +It is raised by the group's `class` statement. The exception carries `.provider`, `.group_name`, +`.current_scope` and `.new_scope`. ## Cause @@ -29,47 +33,85 @@ class SomeService: ... ```python -shared = providers.Factory(SomeService) # no explicit scope -> APP default, unclaimed +# Broken: +shared = providers.Factory(SomeService) -class PlainGroup(Group): # no scope= -> stamps nothing + +class PlainGroup(Group): svc = shared -container = Container(scope=Scope.APP, groups=[PlainGroup]) # registers + compiles -class ScopedGroup(Group, scope=Scope.REQUEST): # ProviderScopeFrozenError +container = Container(scope=Scope.APP, groups=[PlainGroup]) + + +class ScopedGroup(Group, scope=Scope.REQUEST): svc = shared ``` +[`GroupScopeConflictError`](group-scope-conflict-error.md) is the related case where two groups both +declare a scope and disagree, whether or not anything is registered. This error fires when a single +group would change the scope of a provider that a container has already compiled. + ## Fix +Set `scope=` on the provider. An explicit scope always wins over a group default, so no group ever +changes it: + ```python -# 1. Set scope= explicitly on the provider — explicit always wins over a group default, -# and the provider is never left unclaimed in the first place. shared = providers.Factory(SomeService, scope=Scope.REQUEST) -# 2. Declare every group that lists the provider before building the container. + class PlainGroup(Group): svc = shared + +container = Container(scope=Scope.APP, groups=[PlainGroup]) + + class ScopedGroup(Group, scope=Scope.REQUEST): svc = shared +``` + +Or declare every group that lists the provider before building the container: + +```python +shared = providers.Factory(SomeService) + + +class PlainGroup(Group): + svc = shared -container = Container(scope=Scope.APP, groups=[ScopedGroup]) # now consistent -# 3. Give the scoped group its own provider instance instead of sharing one. class ScopedGroup(Group, scope=Scope.REQUEST): - svc = providers.Factory(SomeService) + svc = shared + + +container = Container(scope=Scope.APP, groups=[ScopedGroup]) +assert shared.scope == Scope.REQUEST ``` -Inspect `.provider`, `.group_name`, `.current_scope`, and `.new_scope` on the exception to see -exactly which provider and group collided. +Pass only one of the two groups to a given container. Both list the same provider instance, and +`Container(groups=[PlainGroup, ScopedGroup])` currently raises +[`DuplicateProviderTypeError`](duplicate-type-error.md) naming that one provider twice. -Note the difference from -[`GroupScopeConflictError`](group-scope-conflict-error.md): that one fires when two groups *both* -declare a scope and disagree, whether or not anything is registered. This one fires when a single -group would change the scope of a provider that a container has already compiled. +Or give the scoped group its own provider instance instead of sharing one: + +```python +shared = providers.Factory(SomeService) + + +class PlainGroup(Group): + svc = shared + + +container = Container(scope=Scope.APP, groups=[PlainGroup]) + + +class ScopedGroup(Group, scope=Scope.REQUEST): + svc = providers.Factory(SomeService) +``` ## See also -- [Scopes](../providers/scopes.md) explains the scope hierarchy and how a provider's scope is chosen. -- [GroupScopeConflictError](group-scope-conflict-error.md) covers two groups disagreeing about a scope. +- [Scopes: group-level default scope](../providers/scopes.md#group-level-default-scope): how a provider's scope is chosen. +- [GroupScopeConflictError](group-scope-conflict-error.md): two groups disagreeing about a provider's scope. diff --git a/docs/troubleshooting/unknown-factory-kwarg-error.md b/docs/troubleshooting/unknown-factory-kwarg-error.md index e789ca41..692ffb7b 100644 --- a/docs/troubleshooting/unknown-factory-kwarg-error.md +++ b/docs/troubleshooting/unknown-factory-kwarg-error.md @@ -2,48 +2,61 @@ ## Symptom -Raised at `Factory(...)` declaration time, listing the `kwargs` key(s) that don't match the creator's -signature, the known parameter names, and a "did you mean" suggestion when a close match exists. +``` +modern_di.exceptions.registration.UnknownFactoryKwargError: Factory kwargs contain unknown key(s) not in create_service signature: + - 'conection_string' (did you mean 'connection_string'?) +Known parameters: ['connection_string'] +See: https://modern-di.modern-python.org/troubleshooting/unknown-factory-kwarg-error/ +``` + +It is raised by the `Factory(...)` call, before any container exists. Each unknown key gets its own +line, in sorted order, with a `did you mean` hint when a parameter name is close to it. The exception +carries `.creator`, `.unknown_keys`, `.known_keys` and `.suggestions`. ## Cause -A key in `kwargs={...}` doesn't correspond to any parameter of the creator, usually a typo or a key -left over after the creator's signature was renamed/refactored. The creator has no `**kwargs` -catch-all, so `modern-di` can validate the keys eagerly at declaration time rather than only failing -at call time. +A key in `kwargs={...}` matches no parameter of the creator, usually a typo or a key left behind after +a parameter was renamed. + +The check runs only when `modern-di` reads the creator's signature. It is skipped when the creator +takes `**kwargs`, when its signature cannot be read, and when the factory sets +`skip_creator_parsing=True`. In the last case a bad key surfaces at resolve time as +[`CreatorCallError`](creator-call-error.md). ## Fix -Match the `kwargs` keys to the creator's actual parameter names: +Match the `kwargs` keys to the creator's parameter names. ```python -def create_service(connection_string: str) -> Service: ... - +# Broken: +providers.Factory(create_service, scope=Scope.APP, kwargs={"conection_string": "..."}) +``` +```python +# Works: class Dependencies(Group): - # Broken: typo — raises UnknownFactoryKwargError, suggests "connection_string" - service = providers.Factory( - create_service, scope=Scope.APP, kwargs={"conection_string": "..."} - ) - - # Works service = providers.Factory( create_service, scope=Scope.APP, kwargs={"connection_string": "..."} ) -``` -If the creator genuinely accepts arbitrary keyword arguments (`**kwargs` in its signature), this check -is skipped automatically, with no escape hatch needed. + +assert isinstance(Container(groups=[Dependencies]).resolve(Service), Service) +``` ## See also -- [Factories: kwargs](../providers/factories.md#kwargs). +- [Factories: kwargs](../providers/factories.md#kwargs): how static `kwargs` are passed to the creator. +- [CreatorCallError](creator-call-error.md): the resolve-time form of a `kwargs` mismatch. diff --git a/docs/troubleshooting/unsupported-creator-parameter-error.md b/docs/troubleshooting/unsupported-creator-parameter-error.md index e8df5f7e..634a2d3e 100644 --- a/docs/troubleshooting/unsupported-creator-parameter-error.md +++ b/docs/troubleshooting/unsupported-creator-parameter-error.md @@ -2,61 +2,108 @@ ## Symptom -Raised at `Factory(...)` declaration time, naming the creator, the parameter, and the reason it can't -be wired automatically. +For a parameterized generic annotation: + +``` +modern_di.exceptions.registration.UnsupportedCreatorParameterError: Parameter 'items' of create_thing cannot be injected: parameterized generic annotation list[myapp.things.Item] cannot be resolved by type; pass the value via the kwargs parameter or give the parameter a default +See: https://modern-di.modern-python.org/troubleshooting/unsupported-creator-parameter-error/ +``` + +For a positional-only parameter: + +``` +modern_di.exceptions.registration.UnsupportedCreatorParameterError: Parameter 'item' of create_thing cannot be injected: positional-only parameters cannot be passed by keyword; give the parameter a default +See: https://modern-di.modern-python.org/troubleshooting/unsupported-creator-parameter-error/ +``` + +Both are raised by the `Factory(...)` call, before any container exists. ## Cause -The creator has a parameter shape `modern-di` cannot resolve by type: a positional-only parameter with -no default (`def f(x, /)`), or a parameterized generic annotation (`list[X]`, `dict[str, Y]`, etc.) -with no default and no matching `kwargs` entry. Both are declaration-time checks, not resolve-time -ones. +The creator has a parameter `modern-di` cannot fill by type: + +- A parameterized generic annotation (`list[Item]`, `dict[str, Item]`) with no default and no + matching `kwargs` entry. +- A positional-only parameter with no default (`def create_thing(item: Item, /)`). `modern-di` passes + every argument by keyword, so it cannot fill this one even when a provider for `Item` is registered. ## Fix -For a parameterized generic, pick one of three escape routes, in order of preference: +For a parameterized generic, the preferred fix is a default: + + +```python +# Broken: +def create_thing(items: list[Item]) -> Thing: + return Thing(items) + + +providers.Factory(create_thing, scope=Scope.APP) +``` + ```python -def create_thing(items: list[Item]) -> Thing: ... +def create_thing(items: tuple[Item, ...] = ()) -> Thing: + return Thing(items) class Dependencies(Group): - # 1. Give the parameter a default - # def create_thing(items: list[Item] = ()) -> Thing: ... + thing = providers.Factory(create_thing, scope=Scope.APP) + - # 2. Supply the value via kwargs at declaration time +assert Container(groups=[Dependencies]).resolve(Thing).items == () +``` + +Or supply the value through `kwargs`: + +```python +def create_thing(items: list[Item]) -> Thing: + return Thing(items) + + +class Dependencies(Group): thing = providers.Factory(create_thing, scope=Scope.APP, kwargs={"items": []}) - # 3. Skip creator parsing entirely and supply every argument via kwargs - thing2 = providers.Factory( + +assert Container(groups=[Dependencies]).resolve(Thing).items == [] +``` + +Or turn off signature parsing and supply every argument through `kwargs`. This suits a creator +with several parameters `modern-di` cannot wire. Pass `bound_type`, since the return annotation is +not read either: + +```python +class Dependencies(Group): + thing = providers.Factory( create_thing, scope=Scope.APP, skip_creator_parsing=True, bound_type=Thing, kwargs={"items": []}, ) -``` -A positional-only parameter (`def create_thing(items: list[Item], /)`) can only be fixed with route -1. Routes 2 and 3 pass the value by keyword, so route 2 still raises this error and route 3 raises -`CreatorCallError` at resolve. If you can't give it a default, drop the `/` or wrap the creator in a -function that takes the parameter by keyword. -## Escape hatches +assert Container(groups=[Dependencies]).resolve(Thing).items == [] +``` -`skip_creator_parsing=True` bypasses signature parsing altogether (option 3 above). Use it when a -creator has several unsupported keyword-passable parameters rather than fixing each one individually. +A positional-only parameter (`def create_thing(items: list[Item], /)`) can only be fixed with a +default. The other two fixes pass the value by keyword: a `kwargs` entry still raises this error, and +`skip_creator_parsing=True` raises [`CreatorCallError`](creator-call-error.md) at resolve. If you cannot give it a default, drop +the `/` or wrap the creator in a function that takes the parameter by keyword. ## See also -- [Factories: creator-signature support matrix](../providers/factories.md#creator-signature-support-matrix). +- [Factories: creator-signature support matrix](../providers/factories.md#creator-signature-support-matrix): which parameter shapes are wired. +- [CreatorCallError](creator-call-error.md): a `kwargs` mismatch found when the creator is called.