diff --git a/docs/providers/advanced-api.md b/docs/providers/advanced-api.md index bc7ad2f3..1a8acdd2 100644 --- a/docs/providers/advanced-api.md +++ b/docs/providers/advanced-api.md @@ -9,9 +9,11 @@ Import public names from four modules, and only from them: - `modern_di`: `Container`, `Group`, `Scope`, `OverrideHandle` (what `override()` returns), `Suggestion` (an element of an error's `.suggestions`), `UNSET` and its type `UnsetType`, plus the three submodules below. -- `modern_di.providers`: the provider types, `CacheSettings` and `container_provider`. +- `modern_di.providers`: the provider types, their base `AbstractProvider`, `CacheSettings` and + `container_provider`. - `modern_di.exceptions`: every error class. -- `modern_di.integrations`: the building blocks for framework integrations. +- `modern_di.integrations`: the building blocks for framework integrations; see + [Writing an integration](../integrations/writing-integrations.md). Every other module is internal and can change in any release, even one that defines an exported name. That covers `modern_di.container`, `modern_di.group`, `modern_di.scope`, `modern_di.types`, @@ -24,24 +26,30 @@ rest. Names that start with an underscore are internal wherever they are. `Group.get_providers()` is a classmethod that traverses the MRO (excluding `Group` and `object`) and collects every class attribute that is an `AbstractProvider` instance, respecting -MRO override order (subclass attribute shadows parent attribute of the same name). Use it to -inspect or iterate all providers declared on a group hierarchy. `Group.get_named_providers()` -returns the same providers as a dict keyed by attribute name. +MRO override order: a subclass attribute shadows a parent attribute of the same name. Shadowing +with a non-provider value (`repo = None`) removes the inherited provider. Use it to inspect or +iterate all providers declared on a group hierarchy. `Group.get_named_providers()` returns the same +providers as a dict keyed by attribute name. + +A subclass already includes its parents' providers. Passing a group together with its subclass to +`Container(groups=[...])`, or one group twice, registers the same provider objects twice and raises +`DuplicateProviderTypeError`. !!! note "The provider set is closed: `AbstractProvider` is not an extension point" `Factory`, `Alias`, `ContextProvider`, and the pre-built `container_provider` are the only provider types. `AbstractProvider` is their shared base and the type that appears - in public signatures (`resolve_dependency`, `kwargs=`), but it is not a hook for - adding your own: resolution compiles a resolver per known provider type, so defining a - subclass of `AbstractProvider` (or of `Factory`) raises `TypeError` when the class is - created. Compose behavior in a creator function, or use `Alias`, instead of introducing a - provider type. + in public signatures (`resolve_provider`, `resolve_dependency`, `override`, + `reset_override`, `find_provider`, `add_providers`), but it is not a hook for adding your + own: resolution compiles a resolver per known provider type, so defining a subclass of + `AbstractProvider` (or of `Factory`) raises `TypeError` when the class is created. Compose + behavior in a creator function, or use `Alias`, instead of introducing a provider type. ## Container navigation - `parent_container` is a read-only property: the container a child was built from, or `None` for a root. `scope` is read-only too. `Container(...)` builds a root; children come only from - `build_child_container()`, which raises `InvalidChildScopeError` for a `scope` that is not deeper. + `build_child_container()`, which raises `InvalidChildScopeError` for a `scope` that is not deeper + (see [Building child containers](scopes.md#building-child-containers)). - `build_child_container()` on a `Container` subclass returns an instance of that subclass. It does not call the subclass's `__init__`, so state that `__init__` sets exists on the root only. - `find_container(scope)` returns `self` when `scope` is the container's own scope, otherwise the @@ -52,7 +60,19 @@ returns the same providers as a dict keyed by attribute name. `Container` subclass does not redirect navigation. `resolve` and `resolve_provider` are entry points, not hooks either: a compiled resolver calls its dependencies' resolvers directly, so an override of either sees only the top-level call. -- Each cached `Factory` gets its own `threading.RLock` in each container that caches it, created - with the cache item on the first resolve there. Building a child allocates no lock. On a cold - cache miss the factory holds its lock while it resolves its dependencies and calls the creator, - so one instance is created per cache key. A warm resolve does not take the lock. + +## Cached factory locking + +Each cached `Factory` gets its own `threading.RLock` in each container that caches it, created +with the cache item on the first resolve there. Building a child allocates no lock. On a cold +cache miss the factory holds its lock while it resolves its dependencies and calls the creator, +so one instance is created per cache key. A warm resolve does not take the lock. The guarantee +this gives callers is in [Cached factories](factories.md#cached-factories). + +## See also + +- [Container](container.md) — building containers and a map of their methods. +- [Writing an integration](../integrations/writing-integrations.md) — the contract built on + `modern_di.integrations`. +- [Design decisions](../introduction/design-decisions.md) — why the public surface is small and + `Container` subclassing is limited. diff --git a/docs/providers/alias.md b/docs/providers/alias.md index 574554b9..1dc4241f 100644 --- a/docs/providers/alias.md +++ b/docs/providers/alias.md @@ -1,25 +1,8 @@ # Alias -`Alias` lets one type resolve to whatever provider already handles a different type. The most common use is binding an abstract base or `Protocol` to a concrete implementation that is already registered, without registering the implementation twice. - -Resolving the alias calls the source's resolver directly, so overrides and caching on the source provider apply transparently. - -## Parameters - -`Alias(source_type, *, bound_type)`. The `source_type` may also be passed as a keyword -(`source_type=`). - -### source_type - -The type whose registered provider should answer the call. At resolution time, the alias looks up `source_type` in the providers registry and delegates to that provider. If `source_type` is not registered, an `AliasSourceNotRegisteredError` is raised. - -### bound_type - -The type the alias is registered under in the providers registry, i.e. the type you pass to `container.resolve(...)`. Required. Set it to the abstract or `Protocol` type you want resolvable, or to `None` to make the alias resolvable by reference only. An alias bound to its own source type would resolve to itself, so `bound_type=source_type` raises `AliasBoundToSourceError` at declaration. - -An alias holds no instance and applies no caching; its effective scope is derived from its source provider. - -## Basic usage +`Alias` makes one type resolve through the provider already registered for another type. Use it to +bind an abstract base or `Protocol` to a registered implementation without registering the +implementation twice: ```python import dataclasses @@ -56,31 +39,56 @@ container = Container(groups=[Dependencies]) concrete = container.resolve(PostgresRepository) abstract = container.resolve(Repository) -# Both resolve to the same instance — the alias delegates to the -# cached source factory. assert concrete is abstract ``` -## Sharing the source's cache +An alias holds no instance and caches nothing, so every resolve goes through the source provider. +With a cached source, as here, the concrete type, the alias type and a downstream parameter typed +as the alias all get the same instance. With an uncached source, each resolve creates a new one. +Aliases chain (an alias whose source type is another alias's `bound_type` works), and an alias can +be wired into `kwargs` like any other provider. + +## Parameters + +`Alias(source_type, *, bound_type)`. The `source_type` may also be passed as a keyword +(`source_type=`). -Because `Alias` does not cache anything itself, callers automatically share whatever instance the source provider returns. With a cached `Factory`, every resolution path returns the same singleton: by the concrete type, by the abstract type, or via a downstream factory parameter typed as the abstract. +### source_type -With an uncached source `Factory`, each resolution still goes through the source factory, so each call produces a new instance (matching the source factory's own behavior). +The type whose registered provider answers the call. The alias looks `source_type` up in the +providers registry and delegates to that provider. If nothing is registered for it, resolving the +alias raises `AliasSourceNotRegisteredError`. + +### bound_type + +The type the alias is registered under, the one you pass to `container.resolve(...)`. Required. Set +it to the abstract or `Protocol` type you want resolvable, or to `None` to make the alias resolvable +by reference only. An alias bound to its own source type would resolve to itself, so +`bound_type=source_type` raises `AliasBoundToSourceError` at declaration. modern-di does not check +that the source's instances satisfy `bound_type`. + +### No scope + +`Alias` takes no `scope=`, and a group's default scope does not apply to it. It resolves at its +source's scope: an alias of a REQUEST-scoped factory resolves from a REQUEST container and raises +`ScopeNotInitializedError` from the APP container, the same as its source. ## Overrides -Overrides are keyed by `provider_id`, so the alias and its source can be overridden independently. See [Testing with overrides](../recipes/testing-overrides.md) for the `container.override` / `reset_override` primitives. +Overrides are keyed by the provider, so the alias and its source can be overridden independently. +See [Testing with overrides](../recipes/testing-overrides.md) for `container.override` and +`reset_override`; the `with container.override(...)` form restores the previous state on exit. ```python mock_for_alias = PostgresRepository(dsn="alias-mock") container.override(Dependencies.abstract_repo, mock_for_alias) assert container.resolve(Repository) is mock_for_alias -# The source provider is untouched. assert container.resolve(PostgresRepository) is not mock_for_alias ``` -Note: an active override on the alias takes precedence over an override on its source for the aliased type, so reset the alias override first if you want the source override to win. +While both are overridden, the alias's override wins for the alias type. Reset the alias override +first if you want the source override to apply to both: ```python container.reset_override(Dependencies.abstract_repo) @@ -98,18 +106,49 @@ assert container.resolve(Repository) is mock_for_source ## Validation and cycle detection -`Alias` participates in `container.validate()`: +`container.validate()` treats the source provider as the alias's dependency and reports problems +inside one `ValidationFailedError`: -- If `source_type` is not registered, `AliasSourceNotRegisteredError` is raised eagerly. -- The alias reports the source provider as a dependency, so cycles that pass through an alias are - detected and reported via `CircularDependencyError`; see +- `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). +- `InvalidScopeDependencyError` when a provider depends, through an alias, on a source at a deeper + scope. + +Without `validate()`, the first resolve that reaches the problem raises +`AliasSourceNotRegisteredError`, `CircularDependencyError` or `ScopeNotInitializedError` directly. + +```python +class Cache(Protocol): ... + + +class InMemoryCache: ... + -!!! note "Scope is checked transitively through `validate()`" - `Container.validate()` checks scope transitively through aliases. A shallow-scoped caller that - depends, via an alias, on a deeper-scoped source is flagged with `InvalidScopeDependencyError` - at validation time. It is the same [scope dependency rule](scopes.md#the-scope-dependency-rule) - enforced everywhere else, applied through the alias's source chain instead of letting it surface - as `ScopeNotInitializedError` at runtime. The error names every hop of that chain and its - terminal source, since the alias's own type carries no scope to point at; see +class CacheDependencies(Group): + cache = providers.Alias(InMemoryCache, bound_type=Cache) + + +cache_container = Container(groups=[CacheDependencies]) +``` + + + +```python +cache_container.validate() # .exceptions holds an AliasSourceNotRegisteredError +``` + +!!! note "Scope is checked through the alias's source chain" + `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). + +## See also + +- [Factories: `bound_type`](factories.md#bound_type) — how a provider's registered type is chosen. +- [AliasSourceNotRegisteredError](../troubleshooting/alias-source-not-registered-error.md) — the + source type has no provider. +- [AliasBoundToSourceError](../troubleshooting/alias-bound-to-source-error.md) — an alias bound to + its own source type. +- [Testing with overrides](../recipes/testing-overrides.md) — overriding providers in tests. diff --git a/docs/providers/container.md b/docs/providers/container.md index 156cce92..6ae3ba73 100644 --- a/docs/providers/container.md +++ b/docs/providers/container.md @@ -1,79 +1,142 @@ # Container -The container provider is a special provider that you should not initialize. -It is automatically registered with each container, so you can resolve the container itself directly. +A `Container` resolves providers. You build one root container, usually at `Scope.APP`, and a child +for each shorter-lived scope such as a request. Every container in a tree shares the same providers +and overrides, and each one owns its own cache and context. See [Scopes](scopes.md) for the scope +model and [Resolving dependencies](../introduction/resolving.md) for how a resolve works. -## Injecting the container itself +## Building a container -You can inject the container into your dependencies in two ways: +`Container(scope=Scope.APP, *, context=None, groups=None)` builds a root container, open and ready +to resolve: -### Automatic injection (type-based) +- `scope`: any `IntEnum` member; anything else raises `InvalidScopeTypeError`. +- `context`: values for the root's [context providers](context.md). The dict is copied, so later + changes to it are not seen. +- `groups`: a list of `Group` classes whose providers it registers. Passing a group together with + its subclass, or one group twice, registers the same providers twice and raises + `DuplicateProviderTypeError`. -If your creator function has a parameter annotated with `Container`, it will be automatically resolved: + ```python +import dataclasses + from modern_di import Container, Group, Scope, providers -def my_creator(di_container: Container) -> str: - # Access the container's scope or other properties - return f"Container scope: {di_container.scope.name}" + +@dataclasses.dataclass(frozen=True) +class Settings: + debug: bool + class Dependencies(Group): - my_factory = providers.Factory(my_creator, scope=Scope.APP) + settings = providers.ContextProvider(Settings) -container = Container(groups=[Dependencies]) -result = container.resolve(str) -# result: "Container scope: APP" + +app_container = Container(groups=[Dependencies], context={Settings: Settings(debug=True)}) +assert app_container.resolve(Settings).debug +assert copy.copy(app_container) is app_container +assert copy.deepcopy(app_container) is app_container ``` -### Explicit injection +Children come from `build_child_container()`. `copy.copy()` and `copy.deepcopy()` return the +container itself, because a copy would own a cache whose finalizers never run. + +## Methods at a glance + +| Member | What it does | Covered in | +|---|---|---| +| `build_child_container(scope=, context=)` | builds a child at a deeper scope | [Scopes](scopes.md#building-child-containers) | +| `set_context(type, value)` | sets a context value on this container | [Context providers](context.md) | +| `resolve(type)`, `resolve_provider(provider)` | resolve by type or by provider | [Resolving dependencies](../introduction/resolving.md) | +| `resolve_dependency(dep)` | takes a provider or a type and calls `resolve_provider` or `resolve`; integrations use it for `FromDI`-style markers | [Writing an integration](../integrations/writing-integrations.md#the-contract) | +| `validate()` | checks the whole graph | [Lifecycle](lifecycle.md#validation) | +| `close_sync()`, `close_async()`, `with`, `async with` | run finalizers and close | [Lifecycle](lifecycle.md#closing-the-container) | +| `open()`, `closed` | reopen a closed container; whether it is closed | [Lifecycle](lifecycle.md#closing-and-reopening) | +| `override(provider, obj)`, `reset_override(provider)` | replace a provider's result, tree-wide | [Testing with overrides](../recipes/testing-overrides.md) | +| `add_providers(*providers)` | registers providers on a root | [below](#registering-providers-after-construction) | +| `find_provider(type)` | looks up the provider for a type | [below](#looking-up-a-provider) | +| `scope`, `parent_container`, `find_container(scope)` | navigate the container tree | [Advanced API](advanced-api.md#container-navigation) | + +## Injecting the container itself -You can also explicitly inject the container using `providers.container_provider`. Reach for this when the parameter is not annotated as `Container` (so type-based injection can't find it), or when you want an explicit binding instead of relying on the type: +Every container registers `providers.container_provider` under the `Container` type, so a creator +parameter annotated `Container` receives a container: + +```python +class JobRunner: + def __init__(self, container: Container) -> None: + self.container = container + + +class Jobs(Group): + runner = providers.Factory(JobRunner, scope=Scope.REQUEST) + + +jobs_container = Container(groups=[Jobs]) +request_container = jobs_container.build_child_container(scope=Scope.REQUEST) + +assert request_container.resolve(JobRunner).container is request_container +``` + +For a parameter not annotated `Container`, wire `providers.container_provider` through `kwargs`: ```python import typing -from modern_di import Container, Group, Scope, providers -def another_creator(di_container: typing.Any) -> str: - return f"resolved from {di_container.scope.name} scope" +class Reporter: + def __init__(self, di: typing.Any) -> None: + self.di = di -class Dependencies(Group): - another_factory = providers.Factory( - another_creator, - scope=Scope.APP, - kwargs={"di_container": providers.container_provider} + +class Reports(Group): + reporter = providers.Factory( + Reporter, scope=Scope.REQUEST, kwargs={"di": providers.container_provider} ) -container = Container(groups=[Dependencies]) -result = container.resolve(str) -# result: "resolved from APP scope" + +reports_container = Container(groups=[Reports]) +reports_request = reports_container.build_child_container(scope=Scope.REQUEST) + +assert reports_request.resolve(Reporter).di is reports_request ``` -## Which container you get +### Which container you get -Resolving `Container` returns the **calling container**: the deepest, most-specific container in -the active chain, not the `APP` root. The `container_provider` hands back whichever container -ran the resolve, so a `REQUEST` child resolves `Container` to *itself*: +`resolve(Container)` returns the container you call it on. A creator receives the container at its +factory's own scope, which is not always the one you called: an APP-scoped factory resolved from a +REQUEST container gets the APP container, because that is where it is built. ```python -app_container = Container(scope=Scope.APP) -request_container = app_container.build_child_container(scope=Scope.REQUEST) +class AppJobRunner(JobRunner): ... -assert app_container.resolve(Container) is app_container -assert request_container.resolve(Container) is request_container # the child, not the APP root + +class AppJobs(Group): + runner = providers.Factory(AppJobRunner, scope=Scope.APP) + + +root = Container(groups=[AppJobs]) +child = root.build_child_container(scope=Scope.REQUEST) + +assert child.resolve(Container) is child +assert child.resolve(AppJobRunner).container is root ``` -The same holds for type-based injection: a creator with a `Container` parameter receives the -container that is resolving it. This means request-scoped code reaches the request container (and its -context/cache), while app-scoped code reaches the app container. +`container_provider` is a prebuilt instance of a private class, so there is nothing to construct. +It is always APP-scoped and ignores a group's default scope. ## Registering providers after construction -`container.add_providers(*providers)` registers additional providers on a **root** container after -it's built. Framework integrations use it to register their connection providers. Raises -`ChildContainerRegistrationError` if called on a child container. See [Writing an integration](../integrations/writing-integrations.md#the-contract) for -the full contract. +`container.add_providers(*providers)` registers more providers on a root container after it is +built. Framework integrations use it to register their context providers. It raises +`ChildContainerRegistrationError` on a child, and `DuplicateProviderTypeError` when a provider's +type is already registered. It does not validate, and it clears the validated state, so call +`validate()` after the last registration. See +[Writing an integration](../integrations/writing-integrations.md#the-contract) for the full contract. ## Looking up a provider @@ -86,6 +149,7 @@ class UserRepository: ... app_container.add_providers(providers.Factory(UserRepository, scope=Scope.REQUEST)) +request_container = app_container.build_child_container(scope=Scope.REQUEST) --> ```python @@ -94,15 +158,10 @@ if provider is not None: repository = request_container.resolve_provider(provider) ``` -## Resolving a provider or type - -`container.resolve_dependency(dep)` accepts either a provider reference or a type and dispatches to -`resolve_provider` or `resolve` accordingly. It is the single entry point integrations use to -resolve a `FromDI`-style marker. See [Writing an integration](../integrations/writing-integrations.md#the-contract). - ## See also -- [Context providers](context.md#context-propagation) covers how context values reach (and don't +- [Context providers](context.md#context-propagation) — how context values reach (and don't reach) a `ContextProvider`. -- [Advanced / low-level API](advanced-api.md) lists the public modules and documents - `find_container`, `parent_container` and `Group.get_providers()`. +- [Scopes](scopes.md) — the scope model and child containers. +- [Advanced / low-level API](advanced-api.md) — the public modules, `find_container`, + `parent_container` and `Group.get_providers()`. diff --git a/docs/providers/context.md b/docs/providers/context.md index 37d54711..1b276719 100644 --- a/docs/providers/context.md +++ b/docs/providers/context.md @@ -1,69 +1,72 @@ # Context providers -Often, scopes are connected with external events: HTTP requests, messages from a queue, callbacks from a framework. -These events can be represented by objects which can be used for dependency creation. +A `ContextProvider` injects a value that exists only at runtime, such as an HTTP request, a queue +message or the current tenant, and each container gets that value from whoever builds it. Framework +integrations declare these providers for their own request and message objects; see +[Framework context objects](#framework-context-objects). -`ContextProvider` is a provider type that injects runtime context values into dependencies -(framework objects like requests or websockets, or your own custom context), extracting them from -the container's context at resolve time. - -In integrations, some context objects (like `fastapi.Request`, `litestar.WebSocket`, etc.) are -automatically provided; see [Framework context objects](#framework-context-objects) below. - -`ContextProvider(context_type, *, scope=Scope.APP, bound_type=UNSET, default=UNSET)`. The +`ContextProvider(context_type, *, scope=UNSET, bound_type=UNSET, default=UNSET)`. The `context_type` may also be passed as a keyword (`context_type=`). +- `scope`: the scope of the container whose context the provider reads. Left unset, it takes the + [group's default scope](scopes.md#group-level-default-scope), or `Scope.APP` when the group has + none. +- `bound_type`: the type the provider is registered under, `context_type` by default. Set it to + `None` to make the provider resolvable by reference only. Values are always keyed by + `context_type` in `context=` and `set_context()`, whatever `bound_type` is. +- `default`: what the provider returns when no value is set; see + [Optional context: `default=`](#optional-context-default). + ## Basic usage -Declare a `ContextProvider` for your context type, supply the value when you build the child container, and any [`Factory`](factories.md) that takes that type as a parameter receives it automatically: +Declare a `ContextProvider` for your context type, supply the value when you build the child +container, and any [`Factory`](factories.md) that takes that type as a parameter receives it: ```python -from modern_di import Group, Container, Scope, providers +import dataclasses -# Custom context type +from modern_di import Container, Group, Scope, providers + + +@dataclasses.dataclass(kw_only=True, slots=True, frozen=True) class CustomContext: - def __init__(self, user_id: str, tenant_id: str) -> None: - self.user_id = user_id - self.tenant_id = tenant_id + user_id: str + tenant_id: str + +@dataclasses.dataclass(kw_only=True, slots=True, frozen=True) +class UserInfo: + user_id: str + tenant_id: str -def create_user_info(custom_context: CustomContext) -> dict[str, str]: - return { - "user_id": custom_context.user_id, - "tenant_id": custom_context.tenant_id, - } + +def create_user_info(custom_context: CustomContext) -> UserInfo: + return UserInfo(user_id=custom_context.user_id, tenant_id=custom_context.tenant_id) class Dependencies(Group): - # Manually defined ContextProvider for custom context custom_context = providers.ContextProvider(CustomContext, scope=Scope.REQUEST) - - # Factory uses the custom context - user_info = providers.Factory( - create_user_info, - scope=Scope.REQUEST, - ) + user_info = providers.Factory(create_user_info, scope=Scope.REQUEST) -# Provide custom context when building the child container container = Container(groups=[Dependencies]) -custom_context = CustomContext(user_id="123", tenant_id="abc") request_container = container.build_child_container( scope=Scope.REQUEST, - context={CustomContext: custom_context} + context={CustomContext: CustomContext(user_id="123", tenant_id="abc")}, ) -# Now resolve the factory — it will receive the custom context automatically -user_info = request_container.resolve_provider(Dependencies.user_info) -# {"user_id": "123", "tenant_id": "abc"} +user_info = request_container.resolve(UserInfo) +assert user_info == UserInfo(user_id="123", tenant_id="abc") ``` -The provider is bound to a [scope](scopes.md) (here `Scope.REQUEST`) and the value is supplied via -[`build_child_container(context={...})`](container.md). +The root container takes the same argument, `Container(context={...})`, for APP-scoped values, and +`container.set_context(CustomContext, value)` sets a value on a container after it is built. A +`context=` entry only supplies a value: without a declared `ContextProvider` for that type, +resolving it raises `ProviderNotRegisteredError`. ## When no value is set -A `ContextProvider` reads its value from the context of the container at its bound scope. When +A `ContextProvider` reads its value from the context of the container at its own scope. When nothing was supplied, the result depends on how the value is used: - A direct resolve (`container.resolve(CustomContext)`) raises `ContextValueNotSetError`. @@ -74,7 +77,7 @@ nothing was supplied, the result depends on how the value is used: ``` Cannot resolve dependency chain: - REQUEST dict (myapp.deps:12) + REQUEST UserInfo (myapp.deps:18) caused by: No context value is set for (scope REQUEST), needed for argument custom_context. Pass context={...} to the container or call set_context(), or pass default= to the ContextProvider. See: https://modern-di.modern-python.org/troubleshooting/context-not-set/ ``` @@ -84,36 +87,50 @@ cases. See [ContextProvider has no value](../troubleshooting/context-not-set.md) ### Optional parameters -Make the parameter optional when a creator runs both with and without the value. This works with -an integration's provider too. With `modern-di-fastapi` set up, `fastapi.Request` is wired by type -to the integration's provider: +Make the parameter optional when a creator runs both with and without the value: ```python -import fastapi -from modern_di import Group, Scope, providers +class Request: + def __init__(self, client_host: str) -> None: + self.client_host = client_host class AuditLog: - def __init__(self, request: fastapi.Request | None = None) -> None: - self.client_host = request.client.host if request and request.client else None + def __init__(self, request: Request | None = None) -> None: + self.client_host = request.client_host if request else None -class Dependencies(Group): - audit_log = providers.Factory(AuditLog, scope=Scope.REQUEST) +class WebDependencies(Group, scope=Scope.REQUEST): + request = providers.ContextProvider(Request) + audit_log = providers.Factory(AuditLog) + + +web_container = Container(groups=[WebDependencies]) + +without_request = web_container.build_child_container(scope=Scope.REQUEST) +assert without_request.resolve(AuditLog).client_host is None + +with_request = web_container.build_child_container( + scope=Scope.REQUEST, context={Request: Request("10.0.0.1")} +) +assert with_request.resolve(AuditLog).client_host == "10.0.0.1" ``` -Inside a request, `AuditLog` gets the real `Request`. Where no request is set, for example in a -FastStream consumer that shares the container, it gets `None`. The integration's provider stays -required, so `container.resolve(fastapi.Request)` outside a request still raises. +The same works with an integration's provider. With `modern-di-fastapi` set up, an +`AuditLog(request: fastapi.Request | None = None)` gets the real request inside a FastAPI handler +and `None` in a FastStream consumer that shares the container. The provider itself stays required, +so `container.resolve(fastapi.Request)` outside a request still raises. -The parameter decides this however it is wired: by type, by a member of a union, through an -`Alias`, or with `kwargs={...}`. It applies only to an argument that comes straight from the -`ContextProvider`. If the parameter's provider is a `Factory` that itself needs the missing value, -the resolve raises. A creator with `skip_creator_parsing=True` or a `**kwargs` signature has no -parsed parameters, so its context arguments never fall back. +The fallback has limits: -A cached factory built while the value was unset keeps the fallback value for the lifetime of its -container. A later `set_context()` does not rebuild it. +- It applies however the parameter is wired: by type, by a member of a union, through an `Alias`, + or with `kwargs={...}`. +- It applies only to an argument that comes straight from the `ContextProvider`. If the + parameter's provider is a `Factory` that itself needs the missing value, the resolve raises. +- A creator with `skip_creator_parsing=True` or a `**kwargs` signature has no parsed parameters, + so its context arguments never fall back. +- A cached factory built while the value was unset keeps the fallback value for the lifetime of + its container. A later `set_context()` does not rebuild it. ### Optional context: `default=` @@ -122,19 +139,32 @@ included, give the provider a default. It returns `default=` whenever no value i value otherwise: ```python -class Dependencies(Group): - custom_context = providers.ContextProvider(CustomContext, scope=Scope.REQUEST, default=None) +GUEST = CustomContext(user_id="guest", tenant_id="public") + + +class GuestDependencies(Group): + custom_context = providers.ContextProvider(CustomContext, scope=Scope.REQUEST, default=GUEST) + user_info = providers.Factory(create_user_info, scope=Scope.REQUEST) + + +guest_container = Container(groups=[GuestDependencies]) +anonymous = guest_container.build_child_container(scope=Scope.REQUEST) + +assert anonymous.resolve(CustomContext) is GUEST +assert anonymous.resolve(UserInfo) == UserInfo(user_id="guest", tenant_id="public") ``` The provider's default wins over a parameter's default. The provider returns the default object -itself on every unset resolve; it does not call or copy it. Type checkers see `default=None` too: -the provider above is a `ContextProvider[CustomContext | None]`. +itself on every unset resolve, without calling or copying it. Type checkers see `default=None` too: +`ContextProvider(CustomContext, default=None)` is a `ContextProvider[CustomContext | None]`. ## Context propagation -Context never propagates between containers. A `ContextProvider` reads the context of the container **at the provider's own scope**; build order is irrelevant. - -Each container copies the `context=` dict it is built with, so containers built from one dict do not share values, and `set_context()` never writes into your dict. +Context never propagates between containers. A `ContextProvider` reads only the context of the +container at its own scope (see [Resolving across scopes](scopes.md#resolving-across-scopes)), so +build order and the direction of the hop make no difference. Each container copies the `context=` +dict it is built with, so containers built from one dict do not share values, and `set_context()` +never writes into your dict. + +```python +app_container.validate() +``` + +```python +app_container.add_providers(request_provider) +app_container.validate() +``` + +Call `validate()` after the setup call; see +[Writing an integration](../integrations/writing-integrations.md#lifecycle-rules) and +[Validation](lifecycle.md#validation). A parameter typed `Request | None = None` validates either +way, as in [Optional parameters](#optional-parameters). + +To wire the value explicitly, use the `ContextProvider` object the integration exports (for example +`fastapi_request_provider`, `litestar_request_provider`, `aiohttp_request_provider` or +`faststream_message_provider`) in `kwargs`. This helps with `skip_creator_parsing=True`, or when +the parameter is not annotated with the framework type: