Skip to content

Top-level module X.py and package X/ are conflated, producing phantom import cycles #3777

Description

@BloodyBeard

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

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions