fix(resolver): resolve calls through absolute Python imports - #825
Merged
Merged
Conversation
A call through an absolute import of a first-party module, e.g. `from shop.pricing import apply_discount; apply_discount()`, reaches resolveExtern as `shop.pricing.apply_discount::apply_discount`. The extern match there is Go-shaped (the candidate's directory must end in the import path's last `/` component), and a dotted module path never matches, so the call fell through to a `dep::` stub. get_callers, get_test_targets and `analyze kind=untested` all missed it; since pytest files import the code under test this way, most tested Python functions were reported untested. resolvePythonModuleExtern runs first for .py/.pyi callers with a dotted import path. It tries the symbol defined in the module the path names (`import shop.pricing as p; p.apply_discount()`), then the path's last segment defined in its parent module (`from shop.pricing import apply_discount [as alias]`, which also covers aliases). A module `a.b` matches `a/b.py`, `a/b.pyi` or `a/b/__init__.py`, at the root or under a source root such as `src/`. Only module-level functions and classes are candidates. It resolves only on a unique match, preferring the caller's repo, and otherwise falls through to the existing logic, so non-Python callers and third-party imports are unchanged. The cache warm-up also warms the imported name, so the step adds no store round-trip per edge. On psf/requests, first-party calls landing on `dep::` stubs drop from 171 to 33 and functions with a direct test edge rise from 32 to 63 of 268. Re-exports through a package __init__.py are not chased; that is left for a follow-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzet
approved these changes
Sep 23, 2026
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.
Summary
Resolves calls made through absolute Python imports of first-party modules (
from shop.pricing import apply_discount; apply_discount()) to the function or class they name, instead of adep::stub. Until now these calls were lost from the graph, soget_callers,get_test_targetsandanalyze kind=untestedmissed them. Pytest files import the code under test this way, so on Python repos most tested functions were reported untested.Fixes #824.
Changes
internal/resolver/python_absolute_imports.go:resolvePythonModuleExtern, called at the top ofresolveExtern. It runs only for.py/.pyicallers whose extern import path is dotted (no/), and tries the two readings the Python extractor produces:shop.pricing::apply_discount, fromimport shop.pricing as p; p.apply_discount()orfrom shop import pricing; pricing.apply_discount();shop.pricing.apply_discount::apply_discount, fromfrom shop.pricing import apply_discount. The local name is ignored, soimport ... as aliasresolves too.A module
a.bmatchesa/b.py,a/b.pyiora/b/__init__.py, at the root or under a source root / repo prefix (src/a/b.py). OnlyKindFunction/KindTypecandidates count, because an import binds module attributes and never class members. The step resolves only on a unique match: when the module exists in more than one repo, it prefers the caller's repo; two same-repo matches (asrc/and abuild/lib/copy, say) are left unresolved rather than guessed. Anything it doesn't resolve falls through to the existing directory match unchanged.internal/resolver/resolver.go: the hook, pluscallerRepohoisted above it so the Python step and Pass 1 share one lookup.internal/resolver/repo_language_lookup.go:warmRepoLanguageNameCachealso warms the imported name (reading 2) for Python extern edges, so the step doesn't add a store round-trip per edge.internal/resolver/python_absolute_imports_test.go: a table test for the module/file matcher, plusresolveExterntests for from-import, alias, class, module-attribute call, package__init__.py, methods ignored, wrong module, third-party, ambiguous layout, caller-repo preference, and a Go caller left untouched.internal/indexer/python_absolute_import_test.go:TestIndex_PythonAbsoluteImportCallsResolve, end to end through the real extractor: asrc/-layout package and a pytest file using a plain import, an alias, a module attribute and a class. Each test function must get bothcallsandtestsedges to the first-party definition.Effect
On the two public
src/-layout projects from #824 (Python extractor only, in-process indexing):callsedges landing ondep::stubssrc/functions + methods with anEdgeTestsedgeWhat remains is mostly re-exports through a package
__init__.py(click's tests callclick.echo, whichclick/__init__.pyre-imports fromclick.utils). As noted in the issue, that case is out of scope here.Testing
go test -race ./...), with the scope noted belowgo test -racepasses on./internal/resolver/,./internal/parser/languages/,./internal/analysis/and./internal/entrypoints/. I ran the affected packages rather than the full suite. In./internal/indexer/, everything passes exceptTestBuildCommitLayerNeverFetchesPromisedRenameBlobs, the promisor-fixture test that also fails on unmodifiedmainon this machine (local git 2.43.0), as noted in #796.golangci-lintv2.13.1 reports 0 issues on./internal/resolver/and./internal/indexer/. With the hook removed, the six positive resolver tests and the indexer test fail; the negative cases (methods, wrong module, third-party, ambiguous, Go caller) pass either way, as they should.Checklist
relative_imports.go, and extractor-backed indexer tests like the Pydantic and Alembic onesMeta["methods"]for interfaces (if applicable): n/aEdgeMemberOfedges to their containing type (if applicable): n/a🤖 Generated with Claude Code