You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Unify provider-graph traversal behind a DependencyGraph module (#308)
* docs(planning): spec + decision for DependencyGraph traversal unification
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* test: characterize validate() and runtime-guard behavior before refactor
Locks today's behavior of Container.validate() (cycle detection shape,
collecting multiple error kinds in one pass) and the runtime resolve
cycle guard, against the public API only, so a later DependencyGraph
refactor can be checked for parity.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* feat: track validated_version on ProvidersRegistry
Adds a `validated_version` marker (int | None, default None) reset to
None on every mutation (register, add_providers, _remove_providers).
Later work will let validate() set it on success and short-circuit
re-validation when the registry hasn't changed since.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* feat: add redirect_target node hook for transparent redirects
Lets a later DependencyGraph module follow alias chains without
knowing about the concrete Alias type. Alias overrides it to return
its source, or None when the source is unregistered (dangling).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* feat: DependencyGraph.walk emits an iterative event stream
Add modern_di/dependency_graph.py with the NodeEntered/Edge/Cycle/
DependenciesError event NamedTuples, the Event union, and
DependencyGraph.walk: a single explicit-stack (non-recursive) pre-order DFS
of the static provider graph. Event order mirrors Container.validate's
recursive _visit exactly (Edge before a possible Cycle; one NodeEntered per
node; visiting/visited shared across roots), so a later task can reproduce
validate() output byte-for-byte and reuse the walk inside a RecursionError
handler near the stack limit. Imports only AbstractProvider and exceptions
at runtime; Container stays behind TYPE_CHECKING to avoid an import cycle.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* feat: add DependencyGraph.find_cycle_from and terminal_scope
find_cycle_from reuses the iterative walk to locate the first cycle
reachable from a provider, staying safe near the stack limit for a
later RecursionError handler. terminal_scope follows redirect_target
hops to the terminal provider's scope, guarding against alias cycles
via a seen-id set. Both will replace the hand-rolled chain walk in
Alias.effective_scope in a later task.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* refactor: validate() folds over DependencyGraph; drop effective_scope
Rewrite Container.validate() as a fold over the shared DependencyGraph.walk
event stream instead of a bespoke recursive DFS, and short-circuit when the
providers registry has already been validated at its current version. Remove
AbstractProvider.effective_scope and Alias.effective_scope; the scope-ordering
check now uses DependencyGraph.terminal_scope on both sides of each edge.
Event emission order (NodeEntered -> DependenciesError -> Edge -> Cycle in
registry order) reproduces the old _visit output byte-for-byte, so the
characterization/parity suites stay green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* refactor: route runtime cycle guard through DependencyGraph; delete _find_reachable_cycle
Rewire resolve_provider's RecursionError handler to reuse the shared
DependencyGraph.find_cycle_from walker instead of a second explicit-stack
DFS, and short-circuit (re-raise untouched) when the registry is already
validated. Add dependency_graph.build_cycle_error so validate() and the
guard construct CircularDependencyError from one place, removing the last
verbatim duplication. Delete the now-redundant _find_reachable_cycle and
_convert_recursion_error.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* test: exclude never-run _explode guards from coverage
Both `_explode` monkeypatch helpers intentionally never execute their
body (they exist to fail the test loudly if the patched method IS
called). Mark them `# pragma: no cover` per the convention already
used in test_runtime_cycle_guard.py, so line coverage doesn't count
dead-by-design guard bodies against the 100% gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* refactor: restore RecursionError call boundary for coverage tracer
CPython suspends the coverage tracer for a few frames while unwinding a
RecursionError, so a re-raise written inline in the except block is never
recorded as covered. Route the handling back through a separate module-level
function so the tracer gets a fresh call boundary to re-arm on, restoring the
100% coverage gate without changing any resolve behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* docs: promote DependencyGraph model into architecture and docs
Replace the recursive-DFS/effective_scope narration with the shipped
DependencyGraph model: validate() as a fold over one iterative walk, the
runtime guard sharing that walk via find_cycle_from, terminal_scope following
the redirect_target node hook, and the registry-level validated_version
short-circuit. effective_scope no longer exists in code and is now absent from
all architecture and docs prose.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* test: rename stale effective_scope test; note match exhaustiveness
Rename test_effective_scope_handles_mutual_alias_cycle to
test_terminal_scope_handles_mutual_alias_cycle so it's grep-discoverable
under the current terminal_scope API, and add a one-line comment noting
the Event match in Container.validate() is exhaustive by construction.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
0 commit comments