Skip to content

Releases: modern-python/modern-di

2.28.0

Choose a tag to compare

@github-actions github-actions released this 13 Jul 10:55
752012e

modern-di 2.28.0 — the integration kit

Back-compatible: no behavior flips, no new warnings, no changes to any
existing public symbol. This release adds one new module,
modern_di.integrations, for framework-integration authors — nothing else
changes.

Feature

  • modern_di.integrations — shared primitives for building an
    integration.
    Every framework adapter (FastAPI, Starlette, gRPC, ...) has
    hand-rolled the same framework-agnostic skeleton: deriving a child
    container's scope/context from a connection, and scanning a handler's
    Annotated hints for DI markers. This module extracts that skeleton so
    adapters can compose it instead of duplicating it.

    Connection derivation — never wraps Container.build_child_container
    itself, only computes what to pass it:

    from modern_di import integrations
    
    match = integrations.classify_connection(connection, (request_provider, websocket_provider))
    child = root.build_child_container(
        scope=match.scope if match else None,
        context=match.context if match else None,
    )

    For a single connection kind with no dispatch, integrations.bind(provider, connection)
    skips straight to the derivation.

    The Annotated-marker injector:

    from modern_di import integrations
    
    service: typing.Annotated[Service, integrations.from_di(Deps.service)]
    
    markers = integrations.parse_markers(handler)             # decoration time
    resolved = integrations.resolve_markers(child, markers)   # call time

    integrations.is_injected/mark_injected guard against double-wrapping a
    handler an auto-inject sweep visits more than once.

    See architecture/integration-kit.md
    for the full design, and the updated
    writing-integrations guide
    for how an adapter composes these.

Downstream

  • No action needed for existing integrations — this release changes
    nothing they already depend on.
  • Integration authors adopting the kit (starlette is next, one adapter
    per PR per the rollout plan): bump the floor to modern-di>=2.28,<3 to
    import modern_di.integrations.

Internals

  • 100% line coverage maintained across Python 3.10–3.14; ruff and ty
    clean; docs built under mkdocs --strict in CI.
  • Built via subagent-driven-development: 9 planned tasks, each with an
    independent spec+quality review; a whole-branch review before merge.

2.27.0

Choose a tag to compare

@github-actions github-actions released this 08 Jul 15:12
02a13cb

modern-di 2.27.0 — shorter registrations, self-resetting overrides, anchored errors

Back-compatible: no behavior flips, no new warnings. Four features from the
3.0 UX research land together — the registration line gets shorter twice,
test overrides clean up after themselves, and error messages now point at
the source line that declared the failing provider.

Feature

  • Positional subject arguments (API-5). Factory, ContextProvider,
    and Alias accept their subject as the first positional argument —
    providers.Factory(UserRepository),
    providers.ContextProvider(HttpRequest), providers.Alias(Concrete).
    The keyword spellings (creator=, context_type=, source_type=)
    remain first-class; nothing is deprecated. Docs teach the positional
    form throughout.

  • Group-level default scope (API-4). A Group subclass may declare its
    members' default scope:

    class RequestGroup(Group, scope=Scope.REQUEST):
        repo = providers.Factory(UserRepository)              # takes REQUEST
        audit = providers.Factory(AuditLog, scope=Scope.APP)  # explicit wins

    Priority: explicit scope= > the group's kwarg (inherited via MRO;
    a subclass's kwarg applies to providers declared in its own body) >
    Scope.APP. Alias never participates (its scope derives from its
    source). A scope-defaulted provider shared by two groups with different
    defaults raises the new GroupScopeConflictError at class creation —
    import order never decides a provider's scope.

  • Context-manager override (INT-4). container.override(provider, obj)
    still applies immediately and now returns an OverrideHandle:

    with container.override(MyGroup.api_client, mock_client) as client:
        ...  # resolution returns mock_client
    # prior override state restored here — even on exception

    Exit restores the snapshot taken at the call (a previously stacked
    override, or none); nested overrides of the same provider unwind in
    order. Imperative override()/reset_override() callers are unaffected.

  • Definition-site anchors on error paths (ERR-6). Breadcrumb lines and
    cycle hops now end with the creator's declaration site:

    Cannot resolve dependency chain:
      REQUEST  UserService (app.services:17)
      APP      └─> SessionFactory (app.db:42)
    

    Captured lazily on the error path only (zero import/resolution cost),
    memoized, with silent fallback for callables without source. Cycle
    errors expose the parallel .cycle_locations; .cycle_path stays bare
    type names.

Downstream

  • Tests asserting exact error messages will break on the new trailing
    (module:line) anchors in breadcrumb and cycle lines. As with 2.24.0
    and 2.26.0: prefer pytest.raises(..., match=...) with a substring —
    anchors are trailing additions, so substring matches survive.
  • Container.override now returns an OverrideHandle instead of None
    invisible to callers that ignore the return value.
  • New exception: GroupScopeConflictError (a RegistrationError), with
    its troubleshooting page.
  • No floor bumps required.

Internals

  • The eager warm-up capability (API-8/INT-3, init_cache()-style) is
    explicitly deferred with a revisit trigger recorded in planning.
  • 100% line coverage maintained across Python 3.10–3.14; ruff and ty
    clean; docs built under mkdocs --strict in CI.

2.26.0

Choose a tag to compare

@github-actions github-actions released this 08 Jul 05:45
59aafa3

modern-di 2.26.0 — every exception links its troubleshooting page

Back-compatible: no behavior flips, no new warnings, no attribute or
hierarchy changes. The only change is message text — every exception now
ends with a stable docs URL, and the docs grew a page for each one.

Feature

  • Stable per-exception docs URLs (ERR-4). Every concrete
    ModernDIError subclass carries a class-level docs_slug, and its
    message now ends with a uniform final line:
    See: https://modern-di.modern-python.org/troubleshooting/<slug>/.
    The trailer composes with every message shape — plain errors,
    dependency-path breadcrumbs, and ValidationFailedError's grouped
    report alike (path first, trailer always last).
    DuplicateProviderTypeError's hand-rolled inline URL is replaced by
    the same mechanism (URL unchanged).
  • Complete troubleshooting registry (DOC-6). 16 new pages under
    /troubleshooting/ — one per concrete exception, 21 in total — each
    with Symptom, Cause, Fix, and escape hatches where real. The
    exception catalog
    links each entry to its page.
  • Warnings link their exact migration section. The three transitional
    warnings (UnvalidatedContainerWarning, ContainerClosedWarning,
    ContextValueNoneWarning) now link the specific
    to-3.x guide
    section anchor for their switch instead of the guide's landing page.

Why

ERR-4 and DOC-6 from the 2026-07-05 3.0 UX research: a user holding a
production traceback had no stable link to follow — of ~20 exception
classes, only one carried a URL. Field precedent: Angular's NGxxxx
registry, Spring's Description/Action reports, wireup's remedy+URL
messages. A census test now walks modern_di.exceptions and asserts
every concrete class has a unique slug and an existing page — new
exceptions cannot ship without one.

Downstream

Tests asserting exact exception messages will break on the new
trailer line. As with 2.24.0: prefer pytest.raises(..., match=...)
with a substring — the trailer is a separate final line, so substring
matches survive. No floor bumps required.

Internals

  • 100% line coverage maintained across Python 3.10–3.14; ruff and
    ty clean; docs built under mkdocs --strict in CI (which validates
    every slug URL has a page).

2.25.0

Choose a tag to compare

@github-actions github-actions released this 07 Jul 18:57
c9f5e52

modern-di 2.25.0 — blessed integration seams: add_providers and resolve_dependency

Back-compatible: no behavior flips, no new warnings. Container gains the two
methods framework integrations have been reaching into internals for — a
post-construction registration verb and a provider-or-type resolve dispatch —
plus a correctness fix that makes registration safe in any order relative to
resolution.

Feature

  • Container.add_providers(*providers). The blessed way for integrations
    (and applications) to register providers after the container is built —
    previously only possible by reaching into container.providers_registry.
    Root-only: called on a child container it raises the new
    ChildContainerRegistrationError (inspect .scope), because registries are
    shared tree-wide. On a container that has validated (constructed with
    validate=True, or after a manual validate()), the batch is re-validated
    immediately, so wiring errors surface at the registration site — and the
    call is atomic: if re-validation fails for any reason, the whole batch
    is rolled back and the container is exactly as it was.
  • Container.resolve_dependency(dep). One entry point that accepts a
    provider or a type and dispatches to resolve_provider/resolve
    the isinstance dance every integration's marker handling re-implements
    today, now in core. Overrides, caching, scope checks, and "did you mean"
    suggestions behave exactly as on the underlying paths.

Fix

  • Wiring plans rebuild after registration. A provider's memoized wiring
    plan is now stamped with the providers-registry version and rebuilt when
    registration has changed the registry. Previously, a provider resolved
    before a later registration kept its stale plan silently — an optional
    dependency stayed None, a missing required one kept raising
    ArgumentResolutionError even after its provider was registered. This
    affected the old providers_registry reach-in path too.

Why

INT-1 and INT-2 from the 2026-07-05 3.0 UX research: all seven official
integrations reach two attributes deep to register context providers, and all
seven copy-paste the same marker dispatch. Blessing both as Container
methods gives the 3.0 surface a stable integration contract (and, per .NET /
dishka precedent, one place for its semantics). Registration on a validated
container re-validating by default keeps the seam correct under 3.0's
validate-at-construction.

Downstream

No floor bump required — the providers_registry reach-in keeps working in
2.x. Integrations should migrate to add_providers /
resolve_dependency (and bump their floor to modern-di>=2.25 when they
do); the reach-in path is slated for privatization after the org-wide
migration. The
writing-integrations spec
already documents the new contract points.

Internals

  • Docs site deduplicated: every core concept now has one canonical page with
    links elsewhere; the historical 0.x→1.x and 1.x→2.x guides are condensed.
    Two stub pages merged away (testing/fixtures,
    introduction/that-depends-or-modern-di — their URLs now 404; content
    lives in integrations/pytest and introduction/comparison).
  • Docstring policy pass over the package: internal helpers carry one-line
    contracts; two stale references to long-removed functions fixed. No
    behavior change.
  • 100% line coverage maintained across Python 3.10–3.14; ruff and ty
    clean; docs built under mkdocs --strict in CI.

2.23.0

Choose a tag to compare

@github-actions github-actions released this 05 Jul 08:58
91c694a

modern-di 2.23.0 — private Container internals

Back-compatible. Two Container attributes become private, with deprecated aliases; one member is reclassified as a supported extension point.

Deprecation

  • Container.scope_map and Container.lock are now private (_scope_map / _lock). Reading the old names still works but emits DeprecationWarning and will be removed in a future release. These attributes were already documented "internal, no stability guarantee"; the thread-safety knob use_lock= and the Container(...) constructor are unchanged.

Clarification

  • find_container(scope) is a supported extension point. It is the primitive a custom AbstractProvider.resolve calls to locate the container at its scope, and is now documented as such (moved out of the internals section).

Downstream

No action required. Nothing in the official integrations reads scope_map or lock. Advanced users who inspected them should switch to _scope_map / _lock (or, for scope lookup, find_container).

Internals

  • 100% line coverage maintained across Python 3.10–3.14; ruff and ty clean.

2.22.0

Choose a tag to compare

@github-actions github-actions released this 05 Jul 08:08
7a16f69

modern-di 2.22.0 — ergonomic Factory(cache=…); closed-container reuse softened to a deprecation

Two user-facing changes. Factory gains a cache= toggle so the common "just
cache it" case is cache=True instead of cache_settings=CacheSettings(), and
cache_settings= becomes a deprecated alias. Separately, reusing a closed
container no longer raises — it emits a ContainerClosedWarning and self-reopens,
restoring pre-2.16 behavior; the hard ContainerClosedError returns in 3.0.

Feature

  • Factory(cache=…) — one axis for the whole caching spectrum (#259).
    Enabling a cached singleton used to require constructing an empty settings
    object: Factory(scope=Scope.APP, creator=Database, cache_settings=CacheSettings()).
    Now:

    • cache=True — cached with defaults (the common case).
    • cache=CacheSettings(...) — cached and tuned (finalizer, clear_cache).
    • cache=False / cache=None / omitted — not cached.

    CacheSettings and all resolution/finalizer/lifecycle behavior are unchanged;
    the sugar lives entirely in Factory.__init__. No Singleton class was added.

Fix

  • Closed-container reuse is transitional again (#261). The
    ContainerClosedError introduced in 2.16.0 (#202) was a breaking change for
    lifecycles that close then keep resolving. resolve / build_child_container
    (and nested providers resolving at a closed ancestor scope) now warn
    (exceptions.ContainerClosedWarning, a DeprecationWarning) and self-reopen
    instead of raising. The warning fires once per container per closed→reopened
    transition — a resolve that crosses several distinct closed containers emits
    one warning per container.
  • Concurrency note. Under concurrent close + reuse without external
    synchronization, a resolving thread may now self-reopen and rebuild after
    another thread's finalizers already ran, matching pre-2.16 behavior instead
    of the 2.16–2.21 raise. This is user-managed synchronization territory, not
    a new bug.

Deprecations

  • cache_settings= on Factory (#259) — use cache= instead
    (cache=True for defaults, cache=CacheSettings(...) to tune). The old
    kwarg still works but emits a DeprecationWarning and will be removed in a
    future release. Passing both cache= and cache_settings= is a TypeError.
  • Reusing a closed container (#261) — deprecated; it will raise
    ContainerClosedError in modern-di 3.0. Wrap the container in with /
    async with, or call open(), before reuse. To fail fast today:
    warnings.filterwarnings("error", category=exceptions.ContainerClosedWarning).

Docs

  • New integration usage guides for aiohttp (#257, #258) and Starlette
    (#254, #255), a "Writing an integration" guide (#253), and an
    accuracy/integration-parity pass across the docs set (#260). The retired,
    separately-maintained skills/modern-di/ agent-skill set was removed (#256).

Downstream

No action required to keep working. Migrating off the deprecations is
mechanical: rename cache_settings=X to cache=X (or cache=True where you
passed an empty CacheSettings()), and reopen a container (with / open())
before reusing it after close. Integrations that reopen the root container on
startup are already on the recommended path.

Internals

  • 100% line coverage across supported Python versions retained.

2.21.1

Choose a tag to compare

@github-actions github-actions released this 01 Jul 18:07
4999a4e

modern-di 2.21.1 — release pipeline on PyPI Trusted Publishing

No library changes. The package is identical to 2.21.0; this release exercises the new publish path end-to-end.

CI

  • Releases now authenticate to PyPI via Trusted Publishing (OIDC) instead of a long-lived PYPI_TOKEN secret. uv publish auto-detects the GitHub Actions id-token; the release job runs under a pypi environment that scopes the trusted publisher (#251).

Downstream

No action required. Nothing about the installed package changes.

2.21.0

Choose a tag to compare

@github-actions github-actions released this 26 Jun 19:00
8df6361

modern-di 2.21.0 — ContextProvider.context_type

Purely additive. One newly-public attribute; the contract of existing code is unchanged.

Feature

  • ContextProvider.context_type is now a public attribute — the type the provider supplies and the key its value is set under in context. It was stored privately (_context_type) with no accessor.

Implemented as a plain public slot, matching its sibling bound_type (both derive from the same constructor argument) and the base provider's public scope / bound_type. Properties in the providers are reserved for derived values (display_name); context_type is stored config, so it is a plain attribute.

Why

Framework integrations build a connection → scope → context-key mapping. Without a public context_type they re-state, in an isinstance ladder, the very type they already passed into the provider — so the connection-kind knowledge is split across two places. Exposing context_type lets an integration drive that dispatch off the provider objects themselves, single-sourcing the mapping.

Downstream

No action required. Integrations that want to single-source their connection-kind dispatch (the modern-di-fastapi refactor, and the same pattern in modern-di-litestar / modern-di-faststream / modern-di-typer) can read provider.context_type and bump their floor to modern-di>=2.21.0. Nothing breaks for consumers that don't.

Internals

  • 100% line coverage maintained across Python 3.10–3.14; ruff and ty clean.

2.20.0

Choose a tag to compare

@github-actions github-actions released this 26 Jun 07:45
1d027a1

modern-di 2.20.0 — Group.get_named_providers()

Purely additive. One new public method; the contract of existing code is unchanged.

Feature

  • Group.get_named_providers() -> dict[str, AbstractProvider] — an MRO-walking accessor that maps each declared attribute name to its provider. Group.get_providers() is now list(cls.get_named_providers().values()), so the traversal and dedup/masking logic lives in one place.

The new method preserves the exact semantics of the old get_providers() traversal:

  • MRO order (most-derived first)
  • first-seen name wins (diamond inheritance returns each provider once)
  • a non-provider override masks the parent provider of the same name

get_providers()'s contract (return type, order, dedup, masking) is unchanged.

Why

get_providers() discarded the attribute name each provider was declared under. Downstream integrations that need names (notably modern-di-litestar's autowiring) reconstructed them with a fragile id()-keyed reverse lookup over group.__dict__. That lookup only sees the subclass __dict__ while get_providers() walks the full MRO, so autowiring a Group that inherits a provider raised KeyError. Exposing names at the source — where Group owns provider declaration and traversal — fixes the bug for every consumer.

Downstream

Unblocks the modern-di-litestar autowiring fix, which consumes get_named_providers() and bumps its floor to modern-di>=2.20.0. The FastAPI, FastStream, Typer, and modern-di-pytest integrations do not need to bump.

Internals

  • 100% line coverage maintained across Python 3.10–3.14; ruff and ty clean.

2.19.2

Choose a tag to compare

@github-actions github-actions released this 25 Jun 08:24
ed9b123

modern-di 2.19.2 — Docs and tag-driven releases, no code change

Maintenance release. No API or behavior changes — the installed package is byte-for-byte identical to 2.19.1. It ships new documentation and moves the release process to a tag-driven workflow.

Documentation

  • Exploratory roadmap (ROADMAP.md) sketching possible future directions.
  • Comparison pages — a feature comparison and a "that-depends or modern-di?" guide to help choose between the two.

Release tooling

  • Releases are now tag-driven. Pushing a bare semver tag (e.g. 2.19.2) publishes to PyPI and creates the matching GitHub Release in one workflow, replacing the previous "publish a GitHub Release to trigger PyPI" flow. Pre-release tags use the PEP 440 form (2.0.0rc1). No change for installers.

Downstream

No action needed. There is no API change, so the FastAPI, Litestar, FastStream, Typer, and modern-di-pytest integrations do not need to bump their modern-di floor.

Internals

  • 100% line coverage maintained across Python 3.10–3.14; ruff and ty clean.