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
75 changes: 75 additions & 0 deletions planning/releases/2.27.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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:

```python
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`:

```python
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](https://modern-di.modern-python.org/troubleshooting/group-scope-conflict-error/).
- 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.