Skip to content

fix: resolve absolute Python imports under src/ layout#607

Open
jepzone wants to merge 1 commit into
peteromallet:mainfrom
jepzone:fix/python-src-layout-import-resolution
Open

fix: resolve absolute Python imports under src/ layout#607
jepzone wants to merge 1 commit into
peteromallet:mainfrom
jepzone:fix/python-src-layout-import-resolution

Conversation

@jepzone
Copy link
Copy Markdown

@jepzone jepzone commented May 14, 2026

Problem

For a project using the standard PEP 621 src/ layout — sources under
<project_root>/src/<pkg>/... — scanning with desloppify scan --path .
from the project root never resolves any of the project's own absolute
imports.

resolve_absolute_import in
desloppify/languages/python/detectors/deps_resolution.py currently
probes only:

  1. <scan_root>/<dotted/path>
  2. <project_root>/<dotted/path>

For a file like src/getaccept_worker/entities/activity.py imported as
from getaccept_worker.entities.activity import Activity, both candidates
miss (the file lives under src/getaccept_worker/..., not under the root
or scan root directly). The resolver returns None, so every file under
src/<pkg>/ ends up with importer_count == 0 and the orphan-file
detector emits false-positive findings for entire production packages.

Repro

In a src-layout project (e.g. one with
[tool.hatch.build.targets.wheel] packages = ["src/getaccept_worker", "tools"]
in pyproject.toml):

src/getaccept_worker/entities/activity.py  # imported by 21 files
$ desloppify scan --path .
$ desloppify show orphaned --status open
# activity.py and friends flagged as orphans

Direct check on this project:

from desloppify.languages.python.detectors.deps_resolution import resolve_absolute_import
from pathlib import Path
# old behavior:
resolve_absolute_import("getaccept_worker.entities.activity", Path("."))
# -> None  (bug)

Fix

Extend resolve_absolute_import to additionally probe src/-prefixed
paths under both the scan root and project root. New probe order:

  1. <scan_root>/<dotted/path>
  2. <project_root>/<dotted/path>
  3. <scan_root>/src/<dotted/path> (only when src/ exists)
  4. <project_root>/src/<dotted/path> (only when src/ exists)

src/ candidates are appended only when those directories actually exist,
so flat-layout behavior is identical. The existing scan_root /
project_root probes still run first, so flat layouts win when both
exist.

This is the conservative fix described as "the simple src-fallback" and
matches the layout declared by both Hatchling (packages = ["src/<pkg>"])
and setuptools ([tool.setuptools.packages.find] where = ["src"]).
A follow-up could parse those declarations explicitly, but the simple
fallback is enough to fix the common case.

After the fix on the repro project, activity.py correctly reports
importer_count == 21 and the false-positive orphan findings are gone.

Tests

New file: desloppify/languages/python/tests/test_py_deps_src_layout.py

Covers:

  • module resolution under src/pkg/mod.py
  • package __init__.py resolution under src/pkg/
  • deeply nested resolution under src/pkg/a/b/c.py
  • flat layout still resolves (no spurious /src/ in result)
  • unresolvable modules still return None
  • flat layout is preferred when both <root>/pkg and <root>/src/pkg exist
  • build_dep_graph end-to-end: shared module under src/pkg/ correctly
    records importer_count >= 2 (the original bug scenario)

7 new tests; all 287 tests in desloppify/languages/python/tests/ and
all 5810 tests in desloppify/tests/ still pass.

Extend resolve_absolute_import (the helper backing the Python
dependency-graph builder and dynamic-import finder) to also probe
src/-prefixed paths when the scan root or project root holds a src/
directory. Without these probes a project using the standard PEP 621
src layout — sources under <project>/src/<pkg>/... and scanned with
--path . from the project root — could not resolve any of its own
absolute imports. Every file under src/<pkg>/ then ended up with
importer_count == 0 and the orphan-file detector emitted
false-positive findings for entire production packages.

Probe order is now:
  1. <scan_root>/<dotted/path>
  2. <project_root>/<dotted/path>
  3. <scan_root>/src/<dotted/path>      (only if src/ exists)
  4. <project_root>/src/<dotted/path>   (only if src/ exists)

src/ candidates are appended only when those directories exist,
keeping flat-layout behavior identical. The existing scan_root /
project_root probes still run first, so flat layouts win when both
exist.

Adds tests in
desloppify/languages/python/tests/test_py_deps_src_layout.py covering:
- module, package __init__, and nested module resolution under src/
- flat layout still resolves correctly (no /src/ in result)
- unresolvable modules return None
- flat layout is preferred when both <root>/pkg and <root>/src/pkg exist
- build_dep_graph end-to-end: shared module under src/pkg/ correctly
  records importer_count >= 2 (the original bug scenario)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant