Skip to content

Releases: modern-python/modern-di

4.3.1

Choose a tag to compare

@github-actions github-actions released this 22 Oct 04:52
4.3.1
bc1dc9f

What's Changed

  • ci: test Python 3.15 and 3.15t as supported versions by @lesnik512 in #736

Full Changelog: 4.3.0...4.3.1

4.3.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 12:53
4.3.0
e328cdb

4.3.0 adds provider.derive(fn) and fixes the scope of providers written inline in a Factory's kwargs. The scope fix changes behaviour in ways that raise no error, so read "Changed" before upgrading.

Added

  • provider.derive(fn) returns a provider of fn(value), where value is what provider resolves to. Use it to pass one field of a settings object without writing a creator function for the component. Type checkers infer fn's argument from the provider. A derived provider is never cached, takes its scope from its source and is not registered under any type, so an override of the source reaches it. See Derived values. (#733)
class Dependencies(Group):
    settings = providers.Factory(Settings, cache=True)
    database = providers.Factory(Database, kwargs={"dsn": settings.derive(lambda s: s.db_dsn)}, cache=True)

Changed

A provider written inline in a Factory's kwargs without its own scope= used to resolve at APP scope whatever the scope of the provider using it. It now takes the shallowest scope among the providers that use it, fixed when the container registers them. (#734)

  • An inline Factory(..., cache=True) under a REQUEST-scoped parent used to be one instance shared by every request. It is now one instance per request. To keep a single app-wide instance, pass scope=Scope.APP to the inline provider.
  • An inline provider over a request-scoped source used to fail validate() with InvalidScopeDependencyError. It now validates and resolves.
  • An inline ContextProvider now reads the context at its parent's scope instead of raising ContextValueNotSetError from the APP container.
  • A module-level provider without scope=, used inline, now takes its parents' scope. If it must be APP-scoped, pass scope=Scope.APP.
  • A group declared after registration that would change an inline provider's scope raises ProviderScopeFrozenError, as it already did for class attributes.

See Inline providers.

Docs

  • The integration guide and the starlette, fastapi, fastmcp, litestar and faststream pages say a child container's close is shielded from cancellation and abandoned with a warning after 10 seconds. (#728, #729, #731, #735)

What's Changed

  • docs: say how an integration guarantees release after a cancelled child close by @lesnik512 in #728
  • docs: say the starlette and fastapi child close is shielded and bounded by @lesnik512 in #729
  • docs: say the fastmcp, litestar and faststream child close is shielded and bounded by @lesnik512 in #731
  • feat: add provider.derive(fn) to inject a value computed from another provider by @lesnik512 in #733
  • docs: describe the shielded, bounded child close in the integration guide by @lesnik512 in #735
  • fix: give inline providers in Factory kwargs the scope of the providers using them by @lesnik512 in #734

Full Changelog: 4.2.2...4.3.0

4.2.2

Choose a tag to compare

@github-actions github-actions released this 18 Oct 10:34
4.2.2
60ec39c

4.2.2 changes how a close walks its finalizer queue, which makes the per-request close faster than in 4.2.1 and 4.2.0. There are no API changes.

Changed

  • A cached instance that finishes building on another thread while a close runs is now finalized by that close. In 4.2.1 it waited for the next close, and before 4.2.1 it was never finalized. One that finishes after the close returns is still finalized by the next close. (#727)
  • A close now pops its queue newest first until it is empty, instead of copying it as 4.2.1 did. A request that closes one cached item takes about 1.07 µs in the G7b benchmark, against 1.16 µs on 4.2.1 and 1.11 µs on 4.2.0. With 10 cached items per request it is 1-3% slower than 4.2.0. (#727)

A cancelled or interrupted close keeps the documented behavior: everything not yet finalized, the interrupted item included, stays queued for the next close.

What's Changed

  • fix: finalize a build that finishes during a close in that close by @lesnik512 in #727

Full Changelog: 4.2.1...4.2.2

4.2.1

Choose a tag to compare

@github-actions github-actions released this 18 Oct 10:04
4.2.1
39b91a1

4.2.1 fixes three bugs: a finalizer that never ran, an optional dependency that was injected as None, and a generic class creator that wired nothing. There are no API changes.

Fixed

  • A cached instance whose creator finishes on another thread while the container is closing is now finalized by the next close, as the lifecycle docs say. Before, the close dropped it from the finalizer queue: it stayed cached, no later close or reopen finalized it, and whatever it held leaked. A close with cached items now copies its queue, which costs about 40 ns per close. (#719, #726)
  • A creator parameter annotated Annotated[X, ...] | None (or a union of annotated types) and read from the creator's __signature__, as for Pydantic models and msgspec Structs, is now resolved from X's provider. Before, it was injected as None even with X registered, and a union of two annotated members failed validate(). A common case is the Pydantic field Annotated[X, Field(...)] | None = None. (#720, #724)
  • Factory(Repo[int]), with a parameterized generic class as the creator, now wires Repo's constructor parameters by type, is checked by validate(), and is bound to Repo. Calling Repo[int] still builds the instance. Before, it wired nothing, had no bound type, passed validate(), and failed on first resolve with a raw TypeError. Two parameterizations of one class now share the bound type Repo, so registering both needs bound_type= on them. (#722, #725)

What's Changed

  • docs: add shutdown-order tests to the integration checklist by @lesnik512 in #707
  • docs: regenerate the performance tables after the per-factory code change by @lesnik512 in #708
  • docs: state that the performance tables were measured on 4.2.0 by @lesnik512 in #709
  • docs: explain that Litestar closes the request container before a stream body runs by @lesnik512 in #710
  • docs: note that modern-di-faststream 4.2.2 drops the request container context key by @lesnik512 in #711
  • docs: say Celery tasks resolve from their own app's container by @lesnik512 in #712
  • docs: plainer prose in the benchmarks README and AGENTS.md by @lesnik512 in #713
  • docs: replace emoji link headings with a Links section (RM3) by @lesnik512 in #714
  • docs: say a cancelled sync stream on the grpc aio server is never closed by @lesnik512 in #715
  • docs: match AGENTS.md to the standard's canonical justfile sentence by @lesnik512 in #716
  • docs: say aiogram error handlers get their own child container by @lesnik512 in #718
  • docs: use colons in place of em dashes in gloss bullets by @lesnik512 in #717
  • fix: wire an Annotated union member read from a creator's signature by @lesnik512 in #724
  • fix: wire a parameterized generic class creator from its origin by @lesnik512 in #725
  • fix: finalize a cached instance built during a close at the next close by @lesnik512 in #726

Full Changelog: 4.2.0...4.2.1

4.2.0

Choose a tag to compare

@github-actions github-actions released this 17 Oct 14:17
4.2.0
796d1d7

4.2.0 lets several apps share one container, rejects async creators where they are declared, and makes validate() and several error messages more accurate. Resolving a chain of factories is about 22% faster. Python 3.11 or newer is required, and modern-di still has no runtime dependencies.

Changes that can affect existing code

  • Factory(async_fn) raises AsyncCreatorError (#680). It is raised from the Factory(...) call, for an async def function or method, a functools.partial around one, and an instance with async def __call__. Before, the declaration was accepted and resolve() returned an un-awaited coroutine. Build async resources in the framework's lifespan instead; see Async resources via lifespan.
  • validate() checks reference-only providers (#686). A provider with bound_type=None, or with skip_creator_parsing=True and no bound_type, used to be checked only when a typed provider depended on it. A broken one now fails validate() with ValidationFailedError instead of failing on first resolve.
  • ResolutionStep.scope can be None (#693). An alias step whose source is not registered has no known scope. Error chains draw it as ?. Code that reads .dependency_path[i].scope or CircularDependencyError.steps[i].scope must handle None.
  • Error message text changed for InvalidChildScopeError (#682), DuplicateProviderTypeError (#688) and AliasSourceNotRegisteredError (#692). Code that matches the old text must change.

Fixed

  • Registering the same provider object more than once, for example from two groups, registers it once instead of raising DuplicateProviderTypeError. This also lets several apps share one container: calling setup_di on a second app with the same container no longer raises in the starlette, fastapi, aiohttp, litestar, flask, faststream, aiogram and taskiq integrations. The container is not reference-counted, so whichever app stops first closes it for the others; give it one owner, or use one container per app when apps stop independently. See Sharing a container. (#681)
  • A creator whose parameter types are only on its __signature__, such as a Pydantic model or a msgspec Struct, is now wired. Before, resolving it raised ArgumentResolutionError, and a function creator also lost its return type and was left out of by-type registration. (#685)
  • A provider whose bound type is a falsy class, for example one whose metaclass defines __len__ returning 0, is registered by type and named correctly in errors. (#687)
  • repr(alias) no longer reports a scope the alias doesn't have, and "did you mean" hints show an alias's effective scope. (#683)

Improvements

  • DuplicateProviderTypeError names each provider by its group attribute and call site, such as Dependencies.first: Factory (myapp.deps:12), so two factories with the same creator are told apart. (#688)
  • AliasSourceNotRegisteredError names the reference-only provider when the source type is only served with bound_type=None, and says how to fix it. (#692)
  • InvalidChildScopeError states the value rule instead of listing no scopes when the parent is the deepest built-in scope. (#682)

Performance

  • Each factory gets its own copy of its resolver code, so CPython's call-site caches stop missing between factories. A six-deep chain resolves 22% faster and a ten-wide one 22% faster. The first resolve of a cold container is about 35% slower, about 6 µs. (#694)
  • CacheItem is a plain slotted class. A request cycle is about 7% faster and closing a container with finalizers about 11% faster. (#690)

Docs

  • Most pages were reworked, including the lifecycle, factories, recipes, troubleshooting and integration pages (#651 to #675).
  • New recipe: Sharing a container, with manage_lifespan=False and one owner (#700, #702, #703).
  • The integration pages describe each plugin's current shutdown order: the root opens before user startup code and closes after user shutdown code (#704, #705, #706).
  • Every docs page runs in the test suite against the released integration packages (#654, #658).

What's Changed

  • docs: point integration scope-hierarchy links at the scope table by @lesnik512 in #651
  • test: run the writing-integrations examples in test-docs by @lesnik512 in #652
  • docs: rework the lifecycle page: finalizer forms, close order, and which integrations close the root by @lesnik512 in #653
  • docs: say every release from 2.15.0 on has a curated body by @lesnik512 in #655
  • test: run the framework integration pages against their packages by @lesnik512 in #654
  • docs: rework the factories page: kwargs errors, the skip-parsing warning, and the validate() gap by @lesnik512 in #657
  • test: run every docs page in the integration env and drop the last skip markers by @lesnik512 in #658
  • docs: declare Litestar handler dependencies with NamedDependency by @lesnik512 in #660
  • docs: rework the providers: context, container, alias, advanced api pages by @lesnik512 in #661
  • docs: rework the pytest and writing an integration pages by @lesnik512 in #662
  • docs: rework the wiring, alias, group and kwargs troubleshooting pages by @lesnik512 in #663
  • docs: rework the errors catalog and the scope, container and lifecycle troubleshooting pages by @lesnik512 in #664
  • docs: rework the web integrations pages by @lesnik512 in #665
  • docs: rework the migration guides and dev pages by @lesnik512 in #666
  • docs: rework the task, bot, rpc and cli integrations pages by @lesnik512 in #667
  • docs: rework the recipes pages by @lesnik512 in #668
  • docs: fix cross-page follow-ups from the docs rework by @lesnik512 in #675
  • fix: make _DependencyChainError._message abstract for ty 0.0.87 by @lesnik512 in #677
  • fix: drop ty ignore that ty 0.0.88 reports as unused by @lesnik512 in #679
  • fix: reject an async creator when a Factory is declared by @lesnik512 in #680
  • fix: register a repeated provider object once instead of raising by @lesnik512 in #681
  • fix: state the value rule in InvalidChildScopeError instead of an empty list by @lesnik512 in #682
  • fix: validate() checks reference-only providers by @lesnik512 in #686
  • fix: stop Alias repr from reporting a scope it does not have by @lesnik512 in #683
  • fix: name each provider in DuplicateProviderTypeError by its group attribute and call site by @lesnik512 in #688
  • fix: wire parameters typed only on the creator's signature by @lesnik512 in #685
  • fix: register and name a provider whose bound type is falsy by @lesnik512 in #687
  • docs: state that close does not wait for other threads and a finalizer runs at most once by @lesnik512 in #689
  • perf: build CacheItem as a plain slotted class by @lesnik512 in #690
  • fix: name the reference-only provider when an alias source is unbound by @lesnik512 in #692
  • fix: draw an unknown step scope as ? in error chains by @lesnik512 in #693
  • docs: document the Litestar, Flask and Celery integration fixes by @lesnik512 in #684
  • ci: list the installed modern-di packages before the integration docs run by @lesnik512 in #695
  • perf: give each factory its own copy of its shape's resolver code by @lesnik512 in #694
  • docs: align README and quickstart with the integration and pytest pages by @lesnik512 in #696
  • docs: document the aiogram, aiohttp, Starlette, gRPC, FastStream, Typer, pytest and FastAPI integration fixes by @lesnik512 in #697
  • docs: call modern-di-pytest an integration and note mounted apps in writing-integrations by @lesnik512 in #698
  • docs: mounted apps with middleware are found since starlette 4.1.1 and fastapi 4.0.2 by @lesnik512 in #699
  • docs: warn that apps sharing a root container close it for each other by @lesnik512 in #700
  • docs: share a container with manage_lifespan=False and one owner by @lesnik512 in #702
  • docs: FastStream start() and stop() skip the app lifespan by @lesnik512 in #703
  • docs: ...
Read more

4.1.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 15:06
4.1.0
56ccced

4.1.0 rejects two declarations that never worked, makes two error messages more useful, and reworks most of the docs. Python 3.11 or newer is required, as in 4.0, and modern-di still has no runtime dependencies.

Three changes can break code that ran on 4.0.0. The first two only affect declarations that already failed at first use; they now fail at import, with a clearer error. Each has a section in the migration guide.

Breaking changes

  • Alias requires bound_type (#646, #650). Alias(X) raises TypeError, and type checkers flag it. Alias(X, bound_type=X) raises the new AliasBoundToSourceError. On 4.0.0 both registered the alias under its own source type, so it resolved to itself: validate() and resolve() reported a CircularDependencyError, or registration raised DuplicateProviderTypeError when a provider for X existed. Pass the type the alias answers for, or bound_type=None to use it by reference only. Migration.
  • Provider subclasses fail when the class is defined (#631). Subclassing Factory, Alias or another provider outside modern-di raises TypeError at import. On 4.0.0 a Factory subclass skipped validation and failed at its first resolve with a bare TypeError. Compose behaviour in a creator function or an Alias instead.
  • FinalizerError has a new message (#631). It is one line naming each kind of error with its count, such as Container.close_sync() found 2 finalizer error(s): ConnectionError (1), ValueError (1). Code that matched the old Errors during sync cleanup: [...] text must change. Each finalizer exception also gets a note naming the type of the cached instance whose finalizer raised it. Migration.

Improvements

  • DuplicateProviderTypeError names both providers, one line each, and exposes them as .first_provider and .second_provider in registration order (#631).
  • AliasBoundToSourceError has its own troubleshooting page (#646).

Docs

  • The quickstart is built around a working app (#647), and the scopes page now covers caching, skipped scopes and the dependency rule (#649).
  • The comparison (#639), performance (#638, #636) and design decisions (#640) pages are rewritten.
  • New: a FastMCP integration page (#637) and a Pydantic AI recipe (#634).
  • Troubleshooting pages quote errors the way Python prints them (#648), and the advice on resolve versus resolve_provider is corrected (#643).
  • Every Python example in the docs runs in the test suite (#644).

4.0.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 17:33
4.0.0
8892421

4.0 is a breaking release. It makes thread safety, closing a container and missing context values behave the same way everywhere, and it shrinks the public surface to what is documented. Read the migration guide before upgrading. Every change below that can break code has a section there with before and after examples.

Python 3.11 or newer is required, as in 3.6. modern-di still has no runtime dependencies.

Highlights

  • Each cached factory has its own lock. Container(use_lock=) is removed and every container locks a cache miss. A cache hit takes no lock. On a miss, the factory's dependencies are built under its lock, so threads that miss together build them once. Creators of different cached factories can now run at the same time (#561, #597).
  • A closed container stays closed. Resolving from it raises ContainerClosedError instead of warning and reopening it. The container counts as closed while its finalizers run, and a finalizer that raises drops its cached instance. Closing a root container keeps its overrides (#555, #584, #586).
  • Context values are required unless you say otherwise. A missing value raises ContextValueNotSetError naming the parameter. ContextProvider(T, default=X) is new, and a nullable or defaulted parameter falls back when no value is set. The container copies the context= dict, so set_context() no longer leaks between containers (#557, #588, #602).
  • Scopes match by enum member. A provider resolves only in a container built at the same member. A custom scope that shares an integer value with Scope.SESSION no longer resolves in a Scope.SESSION container (#596).
  • FinalizerError and ValidationFailedError are ExceptionGroups, so except* catches the errors inside them by type (#583, #607).
  • More creators wire. NewType and type X = ... annotations are bound types of their own, and classes whose signature comes from __new__, such as NamedTuple subclasses, wire their parameters. Type checkers accept a NewType or type alias as bound_type= (#591, #625).

Breaking changes

Each item links to its migration guide section.

Fixes

  • Every modern-di exception can be pickled and copied (#600).
  • Error chains name the right argument and provider (#587).
  • ScopeSkippedError names the root of the chain it was raised in (#607).
  • validate() no longer reports a scope error for a provider that takes the Container itself (#620).
  • The wheel and sdist ship LICENSE (#624).

Performance

Against 3.x on the published benchmarks (Apple M2, CPython 3.14.7), a request lifecycle (C4) went from 2.29 µs to 1.85 µs, and a request with a context value (C6) from 1.45 µs to 806 ns. Warm resolves got 3 to 4% faster. See Performance for the comparison with the other containers.

#626, #628 and #629 landed after that publication and target cold paths. Against the commit before each: building and validating a container 25% faster, a first resolve 12 to 15% faster, and a request cycle with a sync finalizer 8% faster. Warm resolves did not move.

Integrations

The framework integration packages get 4.0-compatible releases after this one (#579).

Full changelog: 3.6.0...4.0.0

3.6.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 20:38
3.6.0
c1cf668

Breaking

  • Python 3.10 is no longer supported. requires-python is now >=3.11.

What's Changed

  • test: report a bare docs/adr/NNNN citation as unresolved by @lesnik512 in #499
  • docs(performance): name the release the snapshot ships in by @lesnik512 in #500
  • docs: restore the agent artifacts to the setup skill's templates by @lesnik512 in #501
  • ci: run the scheduled dependency check daily by @lesnik512 in #502
  • docs: point agents at the humanizer skill for prose by @lesnik512 in #515
  • docs: de-market introduction/comparison.md by @lesnik512 in #516
  • docs: rewrite introduction/about-di.md by @lesnik512 in #517
  • docs: normalize the troubleshooting page template by @lesnik512 in #518
  • docs: editorial pass on providers pages by @lesnik512 in #523
  • docs(introduction): editorial pass on the remaining pages and the quickstart by @lesnik512 in #519
  • docs: editorial pass on recipes/ and dev/ by @lesnik512 in #520
  • docs: editorial pass on the migration guides by @lesnik512 in #522
  • docs: mechanical consistency pass on integration pages by @lesnik512 in #521
  • docs: editorial pass on task, bot, RPC, CLI and testing integrations by @lesnik512 in #524
  • docs: editorial pass on troubleshooting/ prose by @lesnik512 in #525
  • docs: editorial pass on the web integration pages and the spec by @lesnik512 in #526
  • docs: align link text, page titles and nav labels with their targets by @lesnik512 in #527
  • docs: replace emoji contrast markers in the recipes code comments by @lesnik512 in #528
  • docs: record the docs style conventions and drop the last emoji marker by @lesnik512 in #529
  • docs: drop the mid-sentence em dashes from README.md and AGENTS.md by @lesnik512 in #530
  • docs: drop the mid-sentence em dashes from comparison.md by @lesnik512 in #531
  • chore: align with the org standard (MD3, FL2) by @lesnik512 in #532
  • chore: give every coverage pragma a reason (TS6) by @lesnik512 in #533
  • perf(compiler): inline the cross-scope hop into the generated resolver by @lesnik512 in #540
  • ci: fetch the ADR citation check from .github instead of keeping a copy by @lesnik512 in #539
  • perf(container): share the root lock across the container tree by @lesnik512 in #541
  • perf(cache): skip the coroutine for finalizer-less items in close_async by @lesnik512 in #542
  • docs(performance): republish the comparative tables after #540, #541 and #542 by @lesnik512 in #543
  • refactor(graph): move the validation walk into dependency_graph.collect_errors by @lesnik512 in #544
  • refactor(factory): name the parsed creator and the cache coercion by @lesnik512 in #545
  • docs(agents): run just install before just lint by @lesnik512 in #546
  • docs: fix factual errors found in review by @lesnik512 in #547
  • fix(errors): drop skip_creator_parsing advice for positional-only params by @lesnik512 in #548
  • fix(errors): drop skip_creator_parsing advice for parameterized generic params by @lesnik512 in #549
  • docs: plain-prose pass over README and published docs by @lesnik512 in #550
  • docs: match the C3 dependency-injector ratio to the table by @lesnik512 in #551
  • feat!: drop Python 3.10 by @lesnik512 in #552

Full Changelog: 3.5.0...3.6.0

3.5.0

Choose a tag to compare

@github-actions github-actions released this 19 Sep 10:25
3.5.0
524f28d

modern-di 3.5.0 — the resolve hot path is generated source

A Factory resolver is now generated from a source template specialised to the factory's
resolver shape and exec'd with the factory's constants as its globals, replacing the seven
hand-copied closures the compiler used to hold. Overrides are compiled in with it: an override
change drops the compiled resolvers and the next resolve recompiles, so nothing on the resolve
path consults the overrides registry. Warm paths get cheaper across the board, cold first-resolve
pays about 2 µs per provider.

One user-visible fix goes with it: a scope violation found by validate() now draws the chain
that reaches the offending provider instead of naming only the declared dependency. No public
name moved. modern_di.exceptions became a package and re-exports every name it exported before.

Performance

Guard-tier scenarios on CPython 3.14, medians, each measured against main on the same machine
with both halves of the change shipped together.

Scenario
G16/G17 by-type resolve(T) −23%
G12 chain resolved with an unrelated override active −30%
G4 wide fan-out −19%
G9 context parameter −16%
G18 alias hop −12%
G2 cached resolve −11%
G1 transient resolve −10%
G5 cross-scope −7%
G7 request lifecycle −6%
G3 deep chain −3%
G8 cold first-resolve +12%
G8b cold cached +1%
  • One code object per resolver shape, shared by every factory of that shape. Unrolling every
    arity removes the intermediate list and the CALL_FUNCTION_EX unpack, which is what pays for the
    wide fan-out. Container.resolve memoizes type to resolver directly, which is most of the by-type
    number.

  • The override front-guard is gone. has_overrides / fetch_override used to run at the top of
    every resolver on every resolve so that a test could swap a value under a compiled graph. The
    registry already dropped every compiled resolver on mutation, so treating an override as a mutation
    reuses that path and removes the guard from all five resolver kinds. That is the −30% on G12, the
    path every test using override() takes.

  • The duplication could not be removed with functions, and that was measured before this was
    written.
    A folded arity ladder costs +31/+26/+35% on transient/chain/wide, because on CPython
    3.12+ a closure's size is paid on every call including the branches never taken. A shared build()
    helper costs +66/+47/+47%, one frame per node; adding a shared call() makes it +79/+62/+58%.
    One code object per provider is 20-30% faster than per shape on chains, since call sites stay
    monomorphic, but costs about 70 µs of compile() per provider, which a suite building a container
    per test pays per test. It stays available as a later opt-in.

  • Cold first-resolve is about 2 µs per provider slower, once per registry, and an override change
    recompiles lazily at about 2-5 µs per provider actually resolved afterwards.

Reasoning and the full rejected-design table:
docs/adr/0001-resolver-hot-path-generated-source.md.

Fixes

  • InvalidScopeDependencyError draws the redirect chain. It named the declared dependency beside
    the terminal's scope and discarded the hops between them, so a violation reached through an Alias
    never named the provider whose scope was actually at fault, neither in the message nor in the
    attributes. It now renders through the same chain renderer as its three siblings.

    New .dep_chain, running from the declared dependency to the provider that supplies it;
    .dep_provider is still there, now a property returning the first hop, and .dep_terminal the
    last. The two differ only when the dependency is reached through an Alias, which is exactly the
    case where the declared type alone cannot tell a reader who owns the offending scope. The
    constructor takes dep_chain= in place of dep_provider= / dep_scope=, and the message text
    changed, so anything matching on it needs a second look.

    Each redirect hop now draws at the scope it resolves at rather than the Scope.APP default an
    Alias reports, which also removes that fiction from CircularDependencyError.

What to know before upgrading

  • Tracebacks through a creator show generated source. It is registered in linecache per shape,
    so a creator's exception shows the resolver's line rather than an opaque frame, and
    __qualname__ is resolve[<display name>] for profilers.

  • modern_di.exceptions is a package. Each error family gets its own submodule over a shared
    base. from modern_di import exceptions and from modern_di.exceptions import X are unchanged:
    all 33 public classes and SUGGESTION_HEADER are re-exported, now listed in an explicit
    __all__, and the class bodies were a verbatim move with byte-identical ASTs. __module__ now
    names the family submodule, so it reads that way in tracebacks and in anything matching on a
    module path.

  • Container.override is a test-time operation. It is not coordinated with concurrent resolves
    on other threads, which its docstring now states. Overriding drops compiled resolvers; a reset that
    changes nothing drops nothing, so close() on a root does not churn the memo.

What did not change

Every public name, the resolution semantics, sync-only resolution, caching, scope transparency,
validation, and every error type, message and breadcrumb other than InvalidScopeDependencyError
above. All override behaviours (handles, nesting, prior restore, propagation through an alias,
scope bypass) pass unchanged, and the one-resolver-frame-per-node invariant holds by construction,
since generated functions have no free variables at all.

Internals

  • 547 tests, 100% line coverage, Python 3.10-3.14 including free-threaded 3.14t; ruff and ty
    clean. The 100% gate now measures the generator, not the generated resolver, which coverage
    cannot see; test_every_resolver_shape_compiles_and_resolves enumerates every shape so a
    template / namespace mismatch is a test failure rather than a NameError in your application.
  • Three seams moved so data stops leaving its owner: Factory.wiring_plan(registry) replaces
    Factory._plan(container), Factory.can_call_positionally(plan) replaces the compiler-level
    helper, and AbstractProvider.mark_registered() replaces the registry writing _registered
    directly. Alias.find_source and Factory.resolution_step lost their underscore, matching the
    public siblings they sit beside.
  • A benchmark covering concurrent first-resolve in sibling children was added, since the compile
    step is now the thing two threads can race into.

Docs

  • The performance page was republished against the template resolver, measured at #470's merge on
    an Apple M2 / CPython 3.14.7 against dishka 1.10.1, dependency-injector 4.49.1, that-depends
    4.1.0 and wireup 2.12.0. By-reference and by-type columns are now within run-to-run noise of
    each other.
  • The ADR set was compressed and the surviving decisions moved into the docs and the code; the
    integration pages now name the missing-container RuntimeError, the FastStream broker created
    in an on_startup hook, @inject on Starlette HTTPEndpoint / WebSocketEndpoint methods,
    aiohttp web.View methods, and typer's container lookup. Those fixes ship in the integration
    packages, which release separately.

Downstream

No action needed. No public API changed and integrations pick the speedups up on upgrade.
Look only if you construct InvalidScopeDependencyError yourself, match on its message, or read
__module__ off a modern-di exception class.

Full changelog: 3.4.0...3.5.0

3.4.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 18:23
3.4.0
1110d4c

modern-di 3.4.0 — one field per question on the scope path

A provider's scope is now derived from what was actually declared instead of being
collapsed at construction and reconstructed afterwards. That is an internal change with
no effect on which scope any provider resolves at, but two small pieces of surface go
with it — see Breaking changes before upgrading if you touch ContextProvider
internals or assign to provider.scope. Resolving a ContextProvider directly also got
16% faster.

Internal refactors

  • AbstractProvider derives its scope rather than storing it. The constructor used
    to collapse an omitted scope= to Scope.APP on its first line, which erased why a
    provider was APP-scoped — precisely the fact the precedence rule needs (explicit
    scope= beats a Group default beats Scope.APP). A companion flag existed only to
    carry that erased bit back, and had to stay in sync with the field recording which
    group had stamped the provider.

    The sources are kept instead: _explicit_scope holds what scope= gave (None when
    omitted) and _group_claim holds (scope, group name) once a Group stamps it.
    scope is a property whose body is the documented precedence list line for line. Each
    field answers exactly one question, and the effective scope cannot drift from its
    provenance because it is computed from it. Slot count is unchanged.

    Every group-scope precedence, conflict and freeze test passes untouched;
    GroupScopeConflictError and ProviderScopeFrozenError fire on exactly the same
    inputs as before.

  • DependencyPathMixin drops its empty __slots__. The line asserted the class had
    no instance attributes while its own __init__ set two, which ty 0.0.74 began
    reporting. Removing it lets the mixing-in classes' declarations stand on their own and
    leaves __dict__ empty after __init__, at the cost of 16 bytes per instance on the
    nine mixin users, on the error path only. No behavior change.

Performance

Measured on an Apple M2 / CPython 3.14.7, min of 11 timing repeats, averaged across
three separate processes.

Path 3.3.0 3.4.0
direct ContextProvider resolve 194.3 ns 161.6 ns −16.8%
  • The compiled ContextProvider resolver inlines its lookup. It used to delegate to
    ContextProvider.resolve, which called fetch_context_value, which read
    provider.scope on every resolve. The compiled closure now performs the override
    guard, scope hop, reopen check and context read itself, with the scope read once at
    compile time — the same shape the folded context kwargs have used since 3.3.0, and
    consistent with the contract that a registered ContextProvider's scope and
    context_type are fixed. Two delegated frames go with it, which is where most of the
    gain comes from.

    This path is what the Annotated marker injectors hit when a handler asks for a
    context type directly, once per marker per request.

  • The guard tier is unchanged. All 25 scenarios sit inside run-to-run noise; the
    control scenario moved further than any real one. No guard scenario covers a direct
    ContextProvider resolve, which is why the number above is measured separately.

Breaking changes

Both are removals of machinery that was never a designed extension point, in the same
class as the provider-type closure shipped in 2.29.0.

  • ContextProvider.resolve(container) is removed. Its only caller was the compiled
    resolver branch above, and the polymorphic provider.resolve(self) dispatch it
    belonged to was retired in 2.29.0
    (decision).
    Nothing in the docs referenced it. Resolve through the container
    (container.resolve(SomeType) or container.resolve_provider(provider)), which is
    unchanged and raises the same ContextValueNotSetError with the same message.

    ContextProvider.fetch_context_value(container) is unaffected. Public since
    2.18.0, it remains the way to read a context value without raising, returning UNSET
    when nothing is set. It now has direct tests covering the same-scope read, the
    cross-scope hop, the closed-owner reopen, and the absent value.

  • provider.scope is read-only. It is now a property, so assigning to it raises
    AttributeError where it previously silently succeeded. Reading is unchanged
    everywhere, including before any container exists (MyGroup.svc.scope still reflects
    a group default at class-creation time). Mutating a registered provider's scope was
    already unsupported: the group path has raised ProviderScopeFrozenError since 2.x,
    and 3.3.0 stated the contract that a ContextProvider's identity is fixed once
    something resolves through it. Set the scope at construction (scope=) or via the
    Group default; to change it, construct a second provider.

What did not change

Which scope any provider resolves at, the precedence rule, the freeze, every error type,
message and breadcrumb, and the Alias / container_provider opt-out from group
stamping. Resolution stays sync-only. No public name other than the two above moved, and
no documented statement in docs/providers/scopes.md needed editing.

Docs

  • docs/architecture/ is gone. The living truth about behaviour is the code and its
    INVARIANT:-marked tests, and a behaviour change is reviewed with the diff rather than
    promoted to a prose page; the invariant census test now enforces that every citation
    pointing at a test resolves to a real one.
  • docs/introduction/performance.md's comparative table was republished at 3.3.0, which
    it had lagged. It does not yet include this release, whose only measured change is on a
    path that table does not cover.

Internals

  • 514 tests, 100% line coverage, Python 3.10–3.14 including free-threaded 3.14t; ruff
    and ty clean.
  • One invariant test added for the compile-time scope capture, carrying a positive
    control so it cannot pass vacuously, plus two for the group-default opt-outs on Alias
    and container_provider — both written and confirmed passing before the refactor, so
    they characterise existing behaviour rather than the new code.

Downstream

No action needed for any integration that resolves through a Container. Check only
if you call ContextProvider.resolve directly or assign to provider.scope; neither
appears in any sibling integration.