fix(backend): resolve absolute Python imports from the repository's own roots - #454
Merged
Merged
Conversation
…wn roots A Python import is written against the interpreter's path, not against the repository root. The resolver only ever looked below the repository root, so `from click.core import Command` went looking for `click/core.py` in a project whose package lives at `src/click`, found nothing, and left the binding with no target -- and every later bare `Command()` was then reported as a call with no resolvable target. This is why #393's samples all looked wrong in the same way: `get_settings`, `AiProviderTestResponse`, `NotFoundError`, `capability_for` are all real, in-repo, extractable definitions. The binding was recorded correctly; the module behind it was being looked for two directories away from where it is. Import roots are now read off the observed file list: a directory is a root when it holds a top-level package -- it contains `<name>/__init__.py` while not being inside a package itself. That is the same fact the interpreter uses, and it finds `src` for a src-layout library and `apps/backend` for this monorepo without anything being configured or guessed. The repository root is always included, so a flat layout is untouched. Widening the candidate set does not widen what gets claimed. A specifier that now matches files under two roots stays ambiguous and emits a diagnostic naming both, which is the existing contract for every other multi-candidate reference -- a test pins that it is ambiguous rather than merely unresolved. Measured on pallets/click's real checkout, resolving it twice through the full extraction and resolution path with the roots on and off: unresolved calls 465 -> 219 resolved edges 4385 -> 4615 total unresolved 992 -> 744 Unresolved *imports* barely moved (360 -> 358), which is the expected result: what remains is os, configparser and third-party packages, correctly diagnosed. That was the open question in the issue, and the answer is that those are a different case and should stay as they are. Closes #393
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
parthrohit22
deleted the
fix/393-resolve-bare-calls-via-import-bindings
branch
September 11, 2026 18:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #393. Off
dev, independent of the other open PRs.The cause
A Python import is written against the interpreter's path, not against the repository root. The resolver only ever looked below the repository root:
So in
pallets/click, whose package lives atsrc/click,from click.core import Commandwent looking forclick/core.py, found nothing, and left the import binding with no target. Every later bareCommand()was then a call with no resolvable target.That is why every sample in the issue looked wrong in the same way —
get_settings,AiProviderTestResponse,NotFoundError,capability_forare all real, in-repo, extractable definitions. The binding was recorded correctly all along; the module behind it was being looked for two directories from where it lives.apps/backend/app/core/config.pywas being sought atapp/core/config.py.This affects essentially every real Python repository:
src/layout is the modern packaging standard, and a monorepo is the other common case.The fix
Import roots are read off the observed file list. A directory is a root when it holds a top-level package — it contains
<name>/__init__.pywhile not being inside a package itself:Same fact the interpreter uses, read from the snapshot rather than configured or guessed. The repository root is always included, so a flat layout is untouched.
Widening the candidate set does not widen what gets claimed. A specifier matching files under two roots stays ambiguous and emits a diagnostic naming both — the existing contract for every other multi-candidate reference. A test pins that it is ambiguous rather than merely unresolved, which is the distinction that proves both files were found and neither was chosen.
Measured
pallets/click's real checkout (204 files), resolved twice through the full extraction and resolution path with root detection on and off:Unresolved imports barely moved: 360 → 358. That is the expected result and it answers the open question in the issue. What remains is
os,configparser,importlib.metadata,pallets_sphinx_themes— genuine third-party and stdlib, correctly diagnosed. Those are a different case and should stay as they are.The 167
calls target is shadowed by a local bindingare also unchanged, correctly: that path is lexical proof that the name is local, and it should not be affected by where modules resolve from.Tests
Three behaviour tests, each confirmed failing without the fix: a src-layout bare call resolves to
src/click/core.py::Command; a monorepo bare call resolves toapps/backend/app/core/config.py::get_settings; the same package under two roots stays ambiguous with both candidates named. Plus a unit test for root detection across all four layouts above.1168 passed / 14 skipped,
ruff check,ruff format --checkandmypyclean.Not in scope
The TypeScript side of the same question (a bare-identifier call resolving through a named import) is untouched — TS module resolution is a different mechanism with its own config surface, and I would not want to fold a guess about it into a change this measurable. Worth its own issue if the numbers warrant it.