Summary
When a project contains both a top-level module X.py and a package directory X/, graphify treats them as the same node. Imports into the package are attributed to the script, which fabricates import cycles that do not exist in the code.
This is a common Python layout — an entry-point script named after the package it launches.
Reproduction
OWASP/Nettacker at current HEAD has exactly this shape:
nettacker.py <- 123-byte entry point script
nettacker/ <- the package
nettacker/logger.py
nettacker/main.py
nettacker/core/app.py
The actual imports are acyclic:
# nettacker.py
from nettacker.main import run
# nettacker/main.py
from nettacker.core.app import Nettacker
# nettacker/core/app.py
from nettacker import logger # -> nettacker/logger.py, i.e. the PACKAGE
from nettacker import logger resolves to nettacker/logger.py. It has nothing to do with the top-level nettacker.py, which never mentions logger at all (grep -c logger nettacker.py → 0).
Actual
GRAPH_REPORT.md reports three cycles, all rooted in the same conflation:
- 3-file cycle: nettacker.py -> nettacker/main.py -> nettacker/core/app.py -> nettacker.py
- 4-file cycle: nettacker.py -> nettacker/main.py -> nettacker/core/app.py -> nettacker/core/die.py -> nettacker.py
- 4-file cycle: nettacker.py -> nettacker/main.py -> nettacker/core/app.py -> nettacker/core/module.py -> nettacker.py
Expected
nettacker.py and the package nettacker/ should be distinct nodes. An import of nettacker.<submodule> should resolve to the package, and no cycle should be reported for this project.
Why it matters
"Import Cycles" is presented as a finding worth acting on, and a circular import is normally a genuine design problem — so a false one sends a reader looking for a defect that isn't there. Worse, anything consuming graph.json for reachability inherits the phantom edge silently: the script appears to participate in call paths it has no part in.
Reproduced on a 1,786-node graph of the project above; the same shape will occur in any repo using the foo.py + foo/ entry-point layout.
Environment
- graphify
0.9.65 (PyPI graphifyy)
- Python 3.14, Windows 10
- Command:
graphify <path> --code-only then graphify cluster-only <path> --no-label
Summary
When a project contains both a top-level module
X.pyand a package directoryX/, graphify treats them as the same node. Imports into the package are attributed to the script, which fabricates import cycles that do not exist in the code.This is a common Python layout — an entry-point script named after the package it launches.
Reproduction
OWASP/Nettackerat current HEAD has exactly this shape:The actual imports are acyclic:
from nettacker import loggerresolves tonettacker/logger.py. It has nothing to do with the top-levelnettacker.py, which never mentionsloggerat all (grep -c logger nettacker.py→ 0).Actual
GRAPH_REPORT.mdreports three cycles, all rooted in the same conflation:Expected
nettacker.pyand the packagenettacker/should be distinct nodes. An import ofnettacker.<submodule>should resolve to the package, and no cycle should be reported for this project.Why it matters
"Import Cycles" is presented as a finding worth acting on, and a circular import is normally a genuine design problem — so a false one sends a reader looking for a defect that isn't there. Worse, anything consuming
graph.jsonfor reachability inherits the phantom edge silently: the script appears to participate in call paths it has no part in.Reproduced on a 1,786-node graph of the project above; the same shape will occur in any repo using the
foo.py+foo/entry-point layout.Environment
0.9.65(PyPIgraphifyy)graphify <path> --code-onlythengraphify cluster-only <path> --no-label