Skip to content

DX polish: repr, keyword-only build_child_container, typed UNSET, error hints#185

Merged
lesnik512 merged 2 commits into
mainfrom
dx-polish-repr-keyword-only-error-messages
May 30, 2026
Merged

DX polish: repr, keyword-only build_child_container, typed UNSET, error hints#185
lesnik512 merged 2 commits into
mainfrom
dx-polish-repr-keyword-only-error-messages

Conversation

@lesnik512
Copy link
Copy Markdown
Member

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.

#4set_context propagation docs

ContextRegistry is per-container. Calling set_context on a parent after build_child_container doesn't propagate to the already-built child. Added a docstring on Container.set_context and a "Context Propagation" !!! warning section in docs/providers/container.md showing the broken pattern plus both fixes (set first, or pass context={...} to build_child_container).

#6override generic typing

Container.override(provider, override_object: object) lost the generic T. Tightened to override_object: types.T so type checkers catch shape mismatches in tests. All existing call sites (tests + docs) pass real subclass instances; no breakage.

#7__repr__ shows parent

Old: 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.name and adds the parent's scope. Useful for nested debugging.

#8build_child_container keyword-only, swap order

Old: (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: ignore

UNSET = object() had no useful type, so bound_type: type | None = types.UNSET needed # ty: ignore[invalid-parameter-default] on three provider constructors. Replaced with a real UnsetType class; three constructors now declare bound_type: type | None | types.UnsetType = types.UNSET and the suppression comments are gone. Body sites use isinstance(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: gained container_scope field 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 passed
  • Manual repr smoke check: root + child reprs match new format
  • Updated tests/test_container.py:repr and :scope_skipped for new strings; tests/test_custom_scope.py:68 substring match still passes; MaxScopeReachedError / InvalidChildScopeError substring assertions in test_container.py still pass since the new messages embed the old phrasing
  • skip_creator_parsing warning test in test_factory.py still passes — guards UNSET-vs-explicit-bound_type semantics in Add LiteStar Example #10

🤖 Generated with Claude Code

… 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>
@lesnik512 lesnik512 self-assigned this May 30, 2026
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>
@lesnik512 lesnik512 merged commit 94925bd into main May 30, 2026
7 checks passed
@lesnik512 lesnik512 deleted the dx-polish-repr-keyword-only-error-messages branch May 30, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant