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) |
calls → gate.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
Summary
For Python,
from pkg import mod as aliasrecords the file-levelimports_fromedge, but the alias binding is not tracked — so every downstreamalias.func()call fails to resolve and produces nocallsedge. The unaliased formfrom pkg import modfollowed bymod.func()resolves correctly.Net effect: the callee keeps only its
containsedge from its own file and looks like dead code toaffected, 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/gate.py:pkg/c_alias.py— module-level, aliased:pkg/c_tryalias.py— try-block, aliased:pkg/c_tryplain.py— try-block, not aliased (control):Run from the package parent:
graphify extract . --code-only --out ./outResult
imports_from(file level)calls→gate.validatec_alias.pyfrom pkg import gate as m_gatec_tryalias.pytry: … as t_gatec_tryplain.pytry: from pkg import gateFull edge list (9 nodes, 8 edges):
The control produces the
callsedge; the two aliased variants do not. Swappingas t_gateout ofc_tryalias.pymakes its edge appear, which isolates the alias as the cause rather than thetry: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:
affectedongate.validatelists 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_fromedge is present, so the module looks connected in the graph and ingraph.html; only the symbol-level reverse query comes back empty.Suggested fix
Track the local binding introduced by
ImportFromwith anasname, and resolve attribute calls against it the same way the unaliased module name is resolved today.import pkg.mod as aliasis presumably the same code path and worth covering in the same change.Related
callsedges are never resolved for member/dotted calls (obj.method(),Type.method()) by deliberate design — only bare (undotted) calls resolve, via a fragile label-text match that isn't scoped to real definitions #2041 — dotted/member calls not resolved by design. Adjacent, but the unaliasedmod.func()case does resolve today, so aliasing specifically regresses a working path.Environment
uv tool install)