Skip to content

validate() scope violations omit the chain that reached the offending provider #444

Description

@lesnik512

Split out of the original bundled roadmap line (see #438). The binding half is now #468 and
is closed; this issue is the diagnostics half only, and the report its needs-info label was
waiting on is below.

The report

validate()'s scope check follows redirects to decide whether an edge is inverted
(terminal_scope in dependency_graph.py), then builds a message naming the declared dependency
beside the terminal's scope, and discards everything in between. Through an Alias — which is how
an abstract base or Protocol is bound to an implementation — those are two different providers, and
the one you have to change is named nowhere.

Handler at APP depending on Impl at REQUEST through a three-hop Protocol chain:

Container.validate() found 1 issue(s): InvalidScopeDependencyError

InvalidScopeDependencyError (1):
  - Provider Handler (scope APP) declares parameter 'svc' typed as a provider of IA at deeper scope REQUEST. A provider cannot depend on a deeper-scoped provider.

IA is the Protocol and declares no scope at all. REQUEST belongs to Impl. Impl does not
appear, and it is not recoverable in code either: .dep_provider is the alias and its source type is
private, so the fact is absent from the message and from the attribute surface.

Resolving the same graph, rather than validating it, prints this:

Cannot resolve dependency chain:
  APP      Handler (app:8)
  APP      └─> IA
  APP          └─> IB
  APP              └─> IC
  REQUEST              └─> Impl (app:7)
  caused by: Provider of scope REQUEST cannot be resolved in container of scope APP.

Why this is not "the wording could be better"

Per ADR 0009,
a message that reads awkwardly is not a defect. Three things here are more than that:

  1. A sibling error out of the same walk already does it right. A cycle through the same alias
    renders the full arrow tree with declaration sites, because build_cycle_error keeps the
    providers it walked. Two errors from one validate() call, one drawing the chain and one printing
    a line, is the drift the shared _render_chain exists to prevent.
  2. The fact is missing from the attributes, not just the text. That is ADR 0009's own revisit
    trigger, one step earlier: there is nothing to parse out of str(exc) because the terminal is not
    in the string.
  3. validate(), whose only job is diagnosis, prints strictly less than the crash it exists to
    prevent.
    Even with no alias involved this error carries no module:line for either provider,
    while every other chain-shaped error does.

Also note the runtime tree above draws APP for IA, IB and IC. Alias sets
_takes_group_scope = False and passes scope=UNSET, so AbstractProvider.scope returns the
Scope.APP default for every alias — a band nothing in the graph chose. CircularDependencyError
prints the same fiction today.

Resolution

Implemented on fix/scope-violation-chain, recorded as ADR 0029:

  • the redirect walk keeps its chain (terminal_chain()), rather than discarding it to return a scope;
  • InvalidScopeDependencyError renders through _render_chain like its three siblings, and gains
    .dep_chain with .dep_provider / .dep_terminal as its ends;
  • each redirect hop draws at the scope it resolves at, so the APP/REQUEST boundary lands on the
    offending edge. Alias.scope still returns APP; scope ordering depends on it, so this is a
    rendering rule, not a change to the provider.

The same graph now renders identically from both detectors:

InvalidScopeDependencyError (1):
  - Provider at a deeper scope reached through this chain:
      APP      Handler (app:8)
      REQUEST  └─> IA
      REQUEST      └─> IB
      REQUEST          └─> IC
      REQUEST              └─> Impl (app:7)
      caused by: Handler (scope APP) declares parameter 'svc' typed as a provider of Impl at deeper scope REQUEST. A provider cannot depend on a deeper-scoped provider.

Guarded by an invariant test that validate() and a runtime resolve name the same chain, an
attribute test that the terminal is recoverable without parsing the message, and a rendering test
that the alias hop draws REQUEST while its own .scope is APP.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions