Skip to content

feat(topology): Map modern Python projects - #162

Merged
JordanCoin merged 2 commits into
JordanCoin:mainfrom
reneleonhardt:feat/python-project-topology
Sep 1, 2026
Merged

JordanCoin merged 2 commits into
JordanCoin:mainfrom
reneleonhardt:feat/python-project-topology

Conversation

@reneleonhardt

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds Python project topology discovery for common repository manifests:

  • Reads pyproject.toml without executing Python code.
  • Maps declared Python project/module roots into the typed topology graph.
  • Handles workspace members and package layouts conservatively.

Why it matters

Python repositories often describe project boundaries in packaging metadata rather than directory layout. The topology graph should reflect those boundaries without running project code.

CLI / MCP surface

No new commands, arguments, or MCP tools. Existing topology output includes Python project and module nodes when manifests are present.

Security

pyproject.toml is read as data; no Python code is executed.

Verification

  • codemap . and codemap --deps .

The repository linter reports the existing baseline findings and one non-functional simplification suggestion in this branch.

Co-Authored-By: GPT-5.6 Sol codex@openai.com

Copy link
Copy Markdown
Owner

Reviewed in depth against the #160 provider contract, with fixtures run through --topology --json. The manifest parsing itself is solid — malformed TOML, duplicate keys, non-UTF-8, and a 2MB/200k-entry manifest all fail closed to malformed-manifest / CoverageUnavailable rather than a false "complete"; nested-project ownership resolves to the deepest project correctly; resolvePythonPath's symlink-escape and ../absolute rejection for local source dependencies is genuinely tight. Two blocking items before this lands, both in the "confident but wrong" class.

1. BLOCKING — source roots are guessed from directory naming, not read from the declared package layout

pythonNode (topology/python_provider.go:114-138) only ever considers two candidates for SourceRoots: {root}/src and {root}/{project_name_with_underscores}, adding either if any file happens to live there. Nothing in python_manifest.go reads [tool.setuptools] package-dir / py-modules, [tool.setuptools.packages.find], [tool.hatch.build.targets.wheel] packages, or [tool.pdm.build]. Both directions of harm reproduce:

False ownership. A flat-layout project declaring py-modules = ["standalone_module"] that also happens to have an unrelated src/ directory:

[project]
name = "myproj"
[tool.setuptools]
py-modules = ["standalone_module"]

with standalone_module.py at root and src/unrelated_scratch/scratch.py →

"source_roots": ["src"],
"members": {"python:pyproject.toml:myproj": ["src/unrelated_scratch/scratch.py"]},
"coverage": {"status": "complete"}

The actually-declared module is absent from members, an unrelated directory is claimed as this project's source, and coverage says complete.

Silent drop. A hatch project with packages = ["lib/actualpkg"], no src/, package name ≠ project name → "coverage":"complete" with zero members: the entire real source tree vanishes and the tool reports success.

Minimum fix consistent with the contract: detect the presence of [tool.setuptools] / packages.find / [tool.hatch.build.targets.wheel] / [tool.pdm.build] and emit CoveragePartial plus an unsupported-package-layout Issue rather than guessing. Better: parse the literal package-dir/packages/py-modules lists the way you already parse [tool.uv.sources] and [tool.poetry.dependencies] — those are literal string lists, same shape as what's already handled. Either way the guess must not survive as complete. No test currently covers setuptools/hatch/pdm layouts, so this needs fixtures too.

2. BLOCKING — a pyproject.toml symlinked outside the repo is read and trusted

parsePythonManifest (topology/python_manifest.go:60) does os.ReadFile(filepath.Join(root, path)), which follows symlinks. discoverManifests walks with WalkDir (which doesn't follow symlinks itself), so a file named pyproject.toml that is a symlink out of the tree still gets enumerated and its external content parsed — producing a node with "manifest":"pyproject.toml", "root":".", "coverage":"complete" fabricated entirely from data outside the repository, no Issue raised. Your own resolvePythonPath already applies exactly the right discipline (EvalSymlinks both sides, reject escapes) for dependency targets; the primary manifest read just didn't get it. Fix: Lstat/EvalSymlinks the manifest before reading and treat an out-of-repo target as a manifest issue instead of trusting it.

Non-blocking

  • dynamic handling only special-cases "dependencies" (python_manifest.go:92-98). Correct today since no other dynamic field feeds the graph, but worth a comment so the next reader doesn't assume full coverage.

Sequencing

No conflicts: git merge-tree against #163 and #165 shows zero conflict markers — each provider self-registers via init() in its own new files and none of the three touches topology/provider.go or graph.go, so the trio is a pure mechanical union with distinct provider names (no duplicate-registration panic). go vet clean; full suite matches the main baseline exactly.


Generated by Claude Code

reneleonhardt and others added 2 commits September 1, 2026 20:16
Co-Authored-By: GPT-5.6 Sol <codex@openai.com>
Reject external manifests and mark declared layouts partial.
@reneleonhardt
reneleonhardt force-pushed the feat/python-project-topology branch from d2190a3 to 7d53ba9 Compare September 1, 2026 18:30
@reneleonhardt

Copy link
Copy Markdown
Contributor Author

Complied

  • Declared Setuptools, Hatch, and PDM layouts no longer trigger guessed source roots; supported literal roots are used and unsupported layouts report partial coverage.
  • External pyproject.toml symlinks are rejected.
  • Added regressions for nested layouts, Setuptools roots, unrelated files, and external symlinks.

Additional changes

  • Anchored declared package roots to each manifest’s directory.
  • Added the concise dynamic-field coverage comment.

@JordanCoin
JordanCoin merged commit 413e25f into JordanCoin:main Sep 1, 2026
11 of 12 checks passed

Copy link
Copy Markdown
Owner

Verified and merged. Re-ran both original reproductions against 7d53ba9:

  • setuptools py-modules + unrelated src/ — was claiming src/ as a source root with src/unrelated_scratch/scratch.py as a member under complete. Now: no source roots, no members, partial + unsupported-package-layout. Honest about what it doesn't understand.
  • hatch packages = ["lib/actualpkg"] — was 0 members under complete. Now actually resolves lib/actualpkg/__init__.py as a member, with partial coverage. Better than the minimum I asked for.
  • pyproject.toml symlinked outside the repo — was read and trusted, fabricating a node. Now: no nodes, unavailable + invalid-manifest-path.

Anchoring declared package roots to each manifest's own directory is a good call — that's the generalization that makes nested layouts behave.

One note for the record: watch/TestLineDelta failed in my full-suite run on the merge, but passes 5/5 standalone on both this branch and unmodified main, and this PR doesn't touch watch/ — the known wall-clock flake in that package, not yours. go vet clean, everything else at baseline.


Generated by Claude Code

@reneleonhardt
reneleonhardt deleted the feat/python-project-topology branch September 1, 2026 18:44
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.

2 participants