Skip to content

Python: from pkg import mod as alias drops every downstream alias.func() call edge — unaliased form resolves fine #2082

Description

@Kyo14363

Summary

For Python, from pkg import mod as alias records the file-level imports_from edge, but the alias binding is not tracked — so every downstream alias.func() call fails to resolve and produces no calls edge. The unaliased form from pkg import mod followed by mod.func() resolves correctly.

Net effect: the callee keeps only its contains edge from its own file and looks like dead code to affected, while the file-level import edge is present and makes the graph look complete.

This is independent of #2072 (scan root) — it reproduces with the scan root at the package root — and independent of try: nesting.

Reproduction

pkg/
  __init__.py      (empty)
  gate.py
  c_alias.py
  c_tryalias.py
  c_tryplain.py

pkg/gate.py:

def validate(rows):
    return bool(rows)

pkg/c_alias.py — module-level, aliased:

from pkg import gate as m_gate


def run_plain_alias():
    return m_gate.validate([1])

pkg/c_tryalias.py — try-block, aliased:

try:
    from pkg import gate as t_gate
except Exception:
    t_gate = None


def run_try_alias():
    return t_gate.validate([1])

pkg/c_tryplain.py — try-block, not aliased (control):

try:
    from pkg import gate
except Exception:
    gate = None


def run_try_plain():
    return gate.validate([1])

Run from the package parent:

graphify extract . --code-only --out ./out

Result

file import form imports_from (file level) callsgate.validate
c_alias.py from pkg import gate as m_gate present missing
c_tryalias.py try: … as t_gate present missing
c_tryplain.py try: from pkg import gate present present

Full edge list (9 nodes, 8 edges):

pkg_c_alias           --contains     --> pkg_c_alias_run_plain_alias      [EXTRACTED]
pkg_c_alias           --imports_from --> pkg_gate                         [EXTRACTED]
pkg_c_tryalias        --contains     --> pkg_c_tryalias_run_try_alias     [EXTRACTED]
pkg_c_tryalias        --imports_from --> pkg_gate                         [EXTRACTED]
pkg_c_tryplain        --contains     --> pkg_c_tryplain_run_try_plain     [EXTRACTED]
pkg_c_tryplain        --imports_from --> pkg_gate                         [EXTRACTED]
pkg_c_tryplain_run_try_plain --calls --> pkg_gate_validate                [EXTRACTED]
pkg_gate              --contains     --> pkg_gate_validate                [EXTRACTED]

The control produces the calls edge; the two aliased variants do not. Swapping as t_gate out of c_tryalias.py makes its edge appear, which isolates the alias as the cause rather than the try: block.

Why it matters

Hit this on a real repo where verification scripts use the aliased form deliberately, to keep the imported module distinct from a local name:

try:
    from etf_capture import schema as m_schema
    from etf_capture import gate as m_gate
    from etf_capture import capture as m_capture
except Exception as exc:
    ...
m_gate.validate(BASELINE_ROWS, None)

affected on gate.validate lists the package's own caller and the three pytest tests that exercise it, but silently omits this script — which is the one that gates acceptance. For an impact-analysis workflow ("what do I re-run if I change this?") the omitted caller is the most important one, and there is no signal that anything was dropped.

The asymmetry also makes it hard to notice by eye: the file-level imports_from edge is present, so the module looks connected in the graph and in graph.html; only the symbol-level reverse query comes back empty.

Suggested fix

Track the local binding introduced by ImportFrom with an asname, and resolve attribute calls against it the same way the unaliased module name is resolved today. import pkg.mod as alias is presumably the same code path and worth covering in the same change.

Related

Environment

  • graphifyy 0.9.22 (uv tool install)
  • Python 3.13.7
  • Windows 11 Pro 26100

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions