Skip to content

python/parser: an imported module-level value resolves to MODULE with no hash #1140

Description

@swapnilpaliwal-sd

Symptom

An import of a module-level VALUE carries no pointer to what it names. The parser
resolves it to resolvedTargetKind=MODULE with an EMPTY resolvedTargetHash, so nothing
downstream can reach the declaration.

# signals.py
class Signal: ...
order_placed = Signal()

# publisher.py
from signals import order_placed        # -> MODULE, hash ""

A def or a class imported the same way resolves correctly, with a real hash.

Root cause

The member lookup that runs for a from X import Y statement resolves Y against the
functions and classes declared in X. A module-level assignment is neither, so the lookup
falls through to the module itself and the target hash is left empty. The kind reported is
then MODULE, which is accurate for what was found and wrong for what was written: the
import names a member, not a module.

Census

Counted over one mid-sized public project's parse, by resolvedTargetKind:

kind rows with an empty hash
UNRESOLVED 1859 1859
MODULE 1225 1225
TYPE 817 0
FUNCTION 708 0

Every MODULE-kind row, without exception. Roughly 940 of the 1225 are value-shaped: a
lowercase member imported from a client module, such as from pkg._state import current_app. TYPE and FUNCTION never have an empty hash, so the gap is specific to the
value case rather than general to imports.

Why it matters

Any engine rule that joins two declarations through an imported module-level object stops
at the boundary. That covers a signal object, a settings singleton, a module-level
registry dict and a configured client instance. The rule cannot tell that two modules
importing the same name are talking about the same object, because neither import carries
its identity.

A concrete case is already shipped and pinned. In
graph/python/engine/framework-behavior/dispatch.dl, signal_dispatch joins a publisher
to a receiver by requiring both ends to resolve to the same binding for the signal object.
Across modules each end has its own import binding and the join produces nothing, so the
mechanism is same-module only. graph/test/python/cases/21-url-and-signal-dispatch asserts
that limitation in its golden: three modules sharing one signal produce no edge, and a fix
here will visibly move that file.

The sibling mechanisms are unaffected, which isolates the gap: task_dispatch joins on an
imported def and works across modules, because FUNCTION imports carry a hash.

What would resolve it

A member lookup that also resolves a module-level assignment target, so a value import
reports the binding it names rather than the module it came from. Failing that, any
durable identity for the member would let the engine join by it: the qualified name of the
declaring module plus the member name is already enough, and is how library linking joins
today.

Activity

  1. added
    bugSomething isn't working
    parserthe parser (parser/), transferred from AxiomCodeAI/parser
    on Sep 20, 2026
  2. swapnilpaliwal-sd commented on Oct 6, 2026

    @swapnilpaliwal-sd
    ContributorAuthor

    The residual is fixed on fix/1140-imported-instance-receiver (PR #1856, into apps/integration-0.1.9): the singleton idiom (value named after its own module) was suffix-matching back to its own module and skipping the VARIABLE branch from #1143, and neither the parser's call-site pass nor the engine ever consumed a VARIABLE import's binding to type a receiver. Parse-level A/B on three corpus projects: 621 call sites gain a resolved callee, zero lose one. Keep this open until it ships in a release; the external report that re-measured it (22.4% of in-repo call sites) is testing npm latest.

  3. added a commit that references this issue on Oct 7, 2026
  4. swapnilpaliwal-sd commented on Oct 8, 2026

    @swapnilpaliwal-sd
    ContributorAuthor

    Fixed by #1856 (commit 9395005), merged into apps/integration-0.1.9; ships with 0.1.9.

    Re-checked 2026-10-08 on a synthetic repo of the reported shape (from svc.order_service import order_service then order_service.cancel(oid)): on apps/integration-0.1.9 impact OrderService.cancel lists cancel_endpoint as [resolved] (4 resolved, 1 by name — the by-name one is a genuinely untyped parameter); the 0.1.9 release branch still reports it [by name].

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

    bugSomething isn't workingparserthe parser (parser/), transferred from AxiomCodeAI/parserpythonPython

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions