Skip to content

Releases: modern-python/modern-di

2.15.0

05 Jun 20:27
e34ff01

Choose a tag to compare

modern-di 2.15.0 — Audit-driven correctness pass

2.15.0 is mostly additive. One behavior change in Container.validate() is called out in Behavior changes. Code that does not catch CircularDependencyError from validate() directly continues to work unchanged.

This release ships ten focused PRs (#188#197) driven by a single bug-hunt audit of the codebase. Eighteen audit findings — 2 must-fix, 3 should-fix, 13 nice-to-have — are addressed. The remaining audit items (22 intentional-behavior entries) are recorded in planning/audits/2026-06-05-bug-hunt-audit-report.md and not actioned.

New features

  • Container.validate() now works across scopes. Previously, validate() (and validate=True at construction time) raised ScopeNotInitializedError for any provider whose scope was deeper than the root container — making the feature unusable for any realistic web app with REQUEST-scoped providers. Factory.get_dependencies was refactored into a pure type→provider lookup that no longer requires find_container. (#189)
  • validate() now catches two new wiring-bug classes at startup. Inverted-scope dependencies (an APP-scoped provider depending on a REQUEST-scoped one) raise a new InvalidScopeDependencyError. Missing required dependencies (parameter with no provider, no default, no static kwarg) raise the same ArgumentResolutionError you'd see at resolve time, just earlier. All issues are accumulated and reported together. (#189)
  • Group inheritance is now supported. Subclassing a Group to add providers ("class TestDeps(AppDeps):") now correctly registers both the parent's and the child's providers. Group.get_providers walks cls.__mro__ and tracks attribute names so subclass overrides and non-provider masks work the way Python's normal attribute lookup does. (#197)
  • Factory validates static kwargs against the creator signature at construction time. A typo like kwargs={'connetion_string': ...} previously surfaced at instantiation as a raw TypeError with no provider identity. Now Factory.__init__ raises UnknownFactoryKwargError(RegistrationError) immediately, naming the offending keys and using difflib.get_close_matches to suggest the closest valid parameter. Validation is skipped when the creator accepts **kwargs or when its signature is uninspectable. (#194)
  • Container(use_lock=False) is honored across child containers. build_child_container now propagates the parent's use_lock setting; the documented single-threaded opt-out no longer silently re-acquires locking at REQUEST scope. (#193)
  • container_provider works on Container subclasses. Auto-injection of a Container parameter (the documented pattern in docs/providers/container.md) now resolves correctly even when the user subclasses Container — the provider is now registered under the base Container class instead of type(self). (#193)
  • Container.__init__ rejects non-IntEnum scope values up front. A new InvalidScopeTypeError(ContainerError) raises at construction with a clear message instead of exploding later from an unrelated __repr__ call with a bare AttributeError. (#193)

Correctness fixes

  • Singleton resolution is now re-entrant. Container.lock switched from threading.Lock to threading.RLock. The documented container_provider auto-injection pattern (where a creator's __init__ accepts container: Container and calls container.resolve(other_singleton)) no longer deadlocks the calling thread. (#188)
  • build_child_container(scope=ZERO_VALUED_INTENUM) raises instead of silently auto-incrementing. Custom IntEnum members with value 0 are falsy in Python; the previous truthiness check misclassified them as "scope omitted" and fell through to the auto-increment branch. Now uses is None. (#190)
  • Self-reference in union-typed parameters falls through to the default instead of deadlocking. A creator typed def make(x: int | SelfType = 1) previously triggered RecursionError at resolve time because the union-lookup branch did not check provider is self. Now mirrors the single-type guard. (#191)
  • Default parameter is used when its ContextProvider's value is unset. Previously, _compile_kwargs raised ArgumentResolutionError immediately on finding an unset ContextProvider — without checking whether the parameter had a default. The check is now symmetric with the no-provider branch directly below it. (#192)
  • Factory correctly handles default values whose __eq__ returns True for all comparands (e.g., unittest.mock.ANY). The UNSET sentinel comparison is now is instead of ==, matching the rest of the codebase. (#194)
  • CacheItem.close_sync/close_async no longer re-run the finalizer when clear_cache=False. Repeated container shutdowns — or the canonical Resource-replacement pattern from docs/migration/to-2.x.md — no longer double-close DB connections. A new finalized flag guards the finalizer; _clear() resets it when actually clearing the cache so re-resolved values can be finalized again on the next close. (#195)

Behavior changes

Container.validate() now raises ValidationFailedError (aggregate) instead of CircularDependencyError directly. (#189)

The new exception carries an .errors: list[Exception] attribute containing every issue found during the walk — cycles, inverted-scope deps, and missing required deps. Single-error runs still go through ValidationFailedError; the inner exceptions are typed (CircularDependencyError, InvalidScopeDependencyError, ArgumentResolutionError). Callers who previously caught CircularDependencyError directly need to update to either catch the aggregate or unwrap .errors:

# Before
try:
    container.validate()
except CircularDependencyError as e:
    log.error("cycle: %s", e.cycle_path)

# After
try:
    container.validate()
except ValidationFailedError as e:
    for issue in e.errors:
        if isinstance(issue, CircularDependencyError):
            log.error("cycle: %s", issue.cycle_path)
        elif isinstance(issue, InvalidScopeDependencyError):
            log.error("inverted scope: %s -> %s", issue.provider, issue.dep_provider)
        elif isinstance(issue, ArgumentResolutionError):
            log.error("missing dep: %s", issue.arg_name)

If you only ever called validate() and let exceptions propagate at startup, no change is needed.

New exceptions

  • ValidationFailedError(ContainerError) — aggregate raised by Container.validate() when any issues are found.
  • InvalidScopeDependencyError(RegistrationError) — a provider depends on another with a strictly deeper scope.
  • InvalidScopeTypeError(ContainerError)Container.__init__ received a non-IntEnum scope value.
  • UnknownFactoryKwargError(RegistrationError)Factory(kwargs=...) contains a key not present in the creator's signature.

Internals

  • AbstractProvider gained an iter_validation_issues(container) -> Iterable[Exception] method with a default no-op implementation. Factory overrides it to yield missing-dep ArgumentResolutionErrors; Container.validate() collects them. Custom AbstractProvider subclasses (if any) continue to work unchanged.
  • Factory gained an internal _find_dep_provider(container, v) helper shared between get_dependencies and _compile_kwargs.
  • CacheItem gained a finalized: bool field.
  • Test suite grew from 125 to 150 tests over this release; 100% coverage maintained throughout.

References

2.14.0

30 May 15:19
d9b7bcd

Choose a tag to compare

What's Changed

  • Argument suggestions, context falsy/None fix, docs lead with with by @lesnik512 in #184
  • DX polish: repr, keyword-only build_child_container, typed UNSET, error hints by @lesnik512 in #185
  • Raise on async finalizer in close_sync; drop Group cache; slot exceptions by @lesnik512 in #186

Full Changelog: 2.13.0...2.14.0

2.13.0

02 May 17:08
0e8f059

Choose a tag to compare

What's Changed

  • Replace RuntimeError raises with custom exception hierarchy by @lesnik512 in #178
  • Include dependency path in resolution error messages by @lesnik512 in #179
  • Add smart suggestions to resolution error messages by @lesnik512 in #180
  • Add Alias provider for interface-to-implementation binding by @lesnik512 in #181
  • Unify validation and remove Container.validate_provider by @lesnik512 in #182
  • Support user-defined custom scopes via any IntEnum by @lesnik512 in #183

Full Changelog: 2.12.0...2.13.0

2.12.0

01 May 13:45
80419ae

Choose a tag to compare

What's Changed

  • add info about typer integration by @lesnik512 in #168
  • Optimize singleton resolution with early cache return by @lesnik512 in #169
  • Add Container.validate() for cycle detection

Full Changelog: 2.11.0...2.12.0

2.11.0

23 Apr 16:40
b970f20

Choose a tag to compare

What's Changed

Full Changelog: 2.10.0...2.11.0

2.10.0

15 Apr 19:45
5f466fe

Choose a tag to compare

What's Changed

  • Add context manager support, repr, and skip_creator_parsing warning by @lesnik512 in #165

Full Changelog: 2.9.0...2.10.0

2.9.0

07 Apr 14:20
f4e8007

Choose a tag to compare

What's Changed

Code fixes:

  • Replaced all private typing._* internals with public get_origin() / get_args() / hasattr(metadata) API — future-proof across Python versions
  • Silent NameError on unresolvable forward references now emits a UserWarning naming the creator and the missing annotation
  • Removed misleading frozen=True from CacheRegistry, ContextRegistry, OverridesRegistry — all three mutate their dict fields in place
  • ContainerProvider no longer requires post-construction mutation by the container — added ProvidersRegistry.register(type, provider) to map the container type directly, keeping bound_type=None permanently on the singleton
  • Finalizer exceptions no longer abort remaining cleanup — both close_async() and close_sync() now run all finalizers, collect failures, and raise a combined RuntimeError at the end

Tooling:

  • Migrated all 8 # type: ignore[...] suppressions to # ty: ignore[rule] after mypy was replaced by ty

Documentation:

  • use_lock=False documented under Cached Factories
  • Union resolution order documented in factories and resolving pages
  • Override global scope behavior documented in testing fixtures
  • Async finalizers vs sync-only resolution distinction added to migration guide

Tests:

  • Added test_sync_finalizer_exception_does_not_abort_remaining_cleanup
  • Added test_async_finalizer_exception_does_not_abort_remaining_cleanu

Full Changelog: 2.8.4...2.9.0

2.8.4

22 Mar 08:52
b7a9a7c

Choose a tag to compare

What's Changed

Full Changelog: 2.8.3...2.8.4

2.8.3

20 Mar 12:35
2b61158

Choose a tag to compare

What's Changed

Full Changelog: 2.8.2...2.8.3

2.8.2

20 Mar 11:03
4aa72b7

Choose a tag to compare

What's Changed

Full Changelog: 2.8.1...2.8.2