DX polish: repr, keyword-only build_child_container, typed UNSET, error hints#185
Merged
Merged
Conversation
… typed UNSET sentinel, error message hints - Container.__repr__ now includes the parent container's scope and uses bare scope.name instead of the verbose enum repr, so nested debugging output is easier to read. - build_child_container is now keyword-only with (scope, context) order (was positional (context, scope)). All callers across the modern-di ecosystem use keyword args; no internal breakage. External users that passed positionally will need a one-line change. - Container.override(provider, override_object) now binds T from the provider, so test mocks that don't match the provider's type surface as type errors rather than silent shape mismatches. - Container.set_context grew a docstring explaining the per-container scope; docs/providers/container.md grew a "Context Propagation" section warning that set_context after build_child_container does not propagate. - modern_di.types.UNSET is now an instance of a real UnsetType class instead of object(); the three provider constructors annotate bound_type with the union including UnsetType and drop the # ty: ignore[invalid-parameter-default] comments. - Error messages for InvalidChildScopeError, MaxScopeReachedError, and ScopeSkippedError now explain the underlying rule or the escape hatch. ScopeSkippedError additionally reports the local container's scope, so users know where the chain actually starts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The defensive __bool__ wasn't called from anywhere in the codebase and left a coverage gap. The two real interaction patterns (`isinstance(x, UnsetType)` and `x is UNSET`) don't depend on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Six Tier-1/2 items from
plan.md(#4, #6, #7, #8, #10, #15–#17). Each is small and topically diverse; bundled into one PR because they share verification (lint + full test suite) and individually wouldn't warrant their own review cycle.#4 —
set_contextpropagation docsContextRegistryis per-container. Callingset_contexton a parent afterbuild_child_containerdoesn't propagate to the already-built child. Added a docstring onContainer.set_contextand a "Context Propagation"!!! warningsection indocs/providers/container.mdshowing the broken pattern plus both fixes (set first, or passcontext={...}tobuild_child_container).#6 —
overridegeneric typingContainer.override(provider, override_object: object)lost the genericT. Tightened tooverride_object: types.Tso type checkers catch shape mismatches in tests. All existing call sites (tests + docs) pass real subclass instances; no breakage.#7 —
__repr__shows parentOld:
Container(scope=<Scope.APP: 1>, providers=1, cached=0)New:
Container(scope=APP, parent=APP, providers=1, cached=0)(child container example).Replaces the verbose enum repr with
scope.nameand adds the parent's scope. Useful for nested debugging.#8 —
build_child_containerkeyword-only, swap orderOld:
(self, context=None, scope=None)— positional, with the inconvenient order.New:
(self, *, scope=None, context=None)— keyword-only, scope-first.Audited every caller across the ecosystem: 38 sites in this repo + all 5 sibling integration repos (
modern-di-fastapi,modern-di-faststream,modern-di-litestar,modern-di-pytest,modern-di-typer). Zero positional callers. Safe across the modern-python ecosystem.External breaking change: downstream users passing positionally will need to update.
#10 — typed UNSET sentinel removes
# ty: ignoreUNSET = object()had no useful type, sobound_type: type | None = types.UNSETneeded# ty: ignore[invalid-parameter-default]on three provider constructors. Replaced with a realUnsetTypeclass; three constructors now declarebound_type: type | None | types.UnsetType = types.UNSETand the suppression comments are gone. Body sites useisinstance(bound_type, types.UnsetType)for proper narrowing.#15-17 — error message polish
InvalidChildScopeError: appends "(child scope value must be strictly greater than parent scope value)".MaxScopeReachedError: appends "To go deeper, build a child container with a custom IntEnum scope whose value is higher." (custom scopes are documented but invisible in this error).ScopeSkippedError: gainedcontainer_scopefield and a much clearer message: "No APP-scope container exists in this chain; this chain starts at {container_scope}. Build an APP-scope container as the root."Test plan
just lint— clean (ruff format + check, ty)just test— 122 passedtests/test_container.py:reprand:scope_skippedfor new strings;tests/test_custom_scope.py:68substring match still passes;MaxScopeReachedError/InvalidChildScopeErrorsubstring assertions intest_container.pystill pass since the new messages embed the old phrasingskip_creator_parsingwarning test intest_factory.pystill passes — guards UNSET-vs-explicit-bound_type semantics in Add LiteStar Example #10🤖 Generated with Claude Code