Skip to content
Merged
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
95 changes: 95 additions & 0 deletions planning/releases/2.29.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# modern-di 2.29.0 — the compiled resolver

Resolution was rebuilt onto a single-path compiled-closure resolver: measurably
faster on every hot-path shape, with two correctness fixes to how creator
defaults and nullable parameters are wired. **One breaking change** — the
provider-type set is now closed; subclassing `AbstractProvider` (or `Factory`)
to add a provider type is no longer supported. See below.

## Performance

- **Single-path compiled resolver (#334).** The interpreted resolve recursion
is replaced by one per-provider compiled closure, memoized on
`ProvidersRegistry` alongside the wiring plan. Each closure front-guards its
own override, navigates to its target scope once (same-scope dependencies skip
via an int compare), and inlines the kwargs build and creator call. The shipped
tree has exactly one resolve path.

Measured against the prior path (guard-tier medians): transient resolve
**-37%**, warm-cached singleton **-31%**, deep dependency chain **-61%**, wide
fan-out **-62%**; lifecycle -6.5%, `build_child_container` ~flat (no
regression). modern-di stays the only zero-dependency pure-Python framework in
its comparative tier holding its own against the codegen frameworks.

- **Per-node hot-path tax trimmed (A1–A4).** Redundant per-node framework
overhead removed from the resolve path, feeding the numbers above.

- **Wiring plans memoized on the registry (#326).** A creator's parameter
partition is computed once and shared tree-wide, so child containers reuse the
parent's plan instead of re-parsing signatures per resolve.

## Fixes

- **A `ContextProvider` passed via `kwargs={...}` now honors the creator's
default (#340).** When a context value was unset, a provider wired explicitly
through `kwargs=` bypassed the creator's default and fell through to the direct
`ContextProvider.resolve` path — which returned `None` (with a
`ContextValueNoneWarning`) instead of omitting the parameter so its default
applied. It now routes through the same dependent-parameter path as by-type
wiring: an unset context value with a creator default omits the parameter, a
nullable annotation injects `None`, and a present value is injected. This is
the warning users saw on optional-`Request` creators like
`def choose_engine(*, ..., request: fastapi.Request | None = None)`.

- **`NoneType` parameters keep their default and nullability (#321).** A
parameter annotated `None`/`NoneType` no longer loses its creator default or
its nullable disposition during signature parsing.

- **`validate()` traverses providers supplied via `kwargs=` (#320).** Providers
declared through `kwargs={...}` are now part of the graph `validate()` walks,
so cycle and transitive-scope checks cover them like type-wired dependencies.

## Breaking change

- **The provider-type set is closed; custom providers are no longer supported.**
`Factory`, `Alias`, `ContextProvider`, and the pre-built `container_provider`
are the only provider types. The compiled resolver (#334) dispatches by exact
type identity, so a subclass of `AbstractProvider` — **or of `Factory`** —
raises `TypeError` at its first resolve. Note the failure mode: `validate()`
walks the dependency graph but compilation is lazy, so a container built with
`validate=True` reports clean and then raises `TypeError` on the first resolve
under traffic.

Custom provider support was never a designed capability — it was an emergent
property of the old polymorphic `provider.resolve(self)` dispatch, and a
now-removed docs section described that accident. If you relied on it, move the
behavior into a creator function, or use `Alias`, instead of introducing a
provider type. Rationale and the rejected alternatives (a 2.x deprecation ramp,
holding for 3.0) are recorded in
[`planning/decisions/2026-07-17-custom-providers-retracted.md`](https://github.com/modern-python/modern-di/blob/main/planning/decisions/2026-07-17-custom-providers-retracted.md).

## Docs & internals

- The "Subclassing `AbstractProvider`" extension point is retracted from the
advanced-API docs, replaced by a note stating the closed set and the failure
mode above (#341, #338).
- Curated core-terms glossary (#329); corrected `architecture/resolution.md`
after the edge-set unification (#322); `CLAUDE.md` message-ownership claim
corrected (#323).
- Internal refactors with no behavior change: exceptions own every rendering
glyph, raise sites carry only facts (#324); `Scope` gets its own algebra
(#325); `CacheItem` owns the singleton get-or-create invariant (#314); the
override short-circuit folds into `OverridesRegistry` (#315); the suggester
owns finding a suggestion, the registry is storage (#327);
`ProvidersRegistry` owns its validation-freshness check (#339).
- 100% line coverage maintained across Python 3.10–3.14; `ruff` and `ty` clean;
docs built under `mkdocs --strict` in CI.

## Downstream

- **Existing integrations:** no action needed — none of the sibling
`modern-di-*` adapters subclass a provider type; the breaking change does not
reach them.
- **If you subclass `AbstractProvider` or `Factory`** in your own code: this
release breaks that on first resolve. Replace the custom provider with a
creator function or an `Alias` before upgrading.
Loading