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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions docs/providers/advanced-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand All @@ -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
Expand All @@ -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.
117 changes: 78 additions & 39 deletions docs/providers/alias.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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])
```

<!-- raises: ValidationFailedError -->

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