Skip to content

fix(backend): resolve absolute Python imports from the repository's own roots - #454

Merged
parthrohit22 merged 1 commit into
devfrom
fix/393-resolve-bare-calls-via-import-bindings
Sep 11, 2026
Merged

parthrohit22 merged 1 commit into
devfrom
fix/393-resolve-bare-calls-via-import-bindings

Conversation

@parthrohit22

Copy link
Copy Markdown
Collaborator

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:

def _python_absolute_file_candidates(specifier):
    root = specifier.replace(".", "/")
    return {f"{root}.py", f"{root}/__init__.py"}

So in pallets/click, whose package lives at src/click, from click.core import Command went looking for click/core.py, found nothing, and left the import binding with no target. Every later bare Command() 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_for are 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.py was being sought at app/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__.py while not being inside a package itself:

src layout        ['src/click/__init__.py', 'src/click/core.py', 'docs/conf.py']  ->  ('', 'src')
monorepo          ['apps/backend/app/__init__.py', ...]                           ->  ('', 'apps/backend')
subpackage only   ['pkg/__init__.py', 'pkg/sub/__init__.py']                      ->  ('',)
nothing packaged  ['a.py', 'b/c.py']                                              ->  ('',)

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:

before after
unresolved calls 465 219
resolved edges 4385 4615
total unresolved 992 744

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 binding are 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 to apps/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 --check and mypy clean.

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.

…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
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
partha-frontend Ready Ready Preview Sep 11, 2026 6:12pm UTC

@parthrohit22
parthrohit22 merged commit b6e0cf3 into dev Sep 11, 2026
14 checks passed
@parthrohit22
parthrohit22 deleted the fix/393-resolve-bare-calls-via-import-bindings branch September 11, 2026 18:31
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.

bug(backend): bare calls to imported first-party symbols don't resolve

1 participant