Repository navigation
fix(sync): a Python import alias links its module when the module appears later - #2501
Merged
Merged
Conversation
…ears later `from .models import User as UserModel` and `from . import helpers as h` bind a name their module declares under another: `User`, `helpers`. Every reference through the alias (the import, a call, a base class, the class or function passed as a value) was parked under the alias, which no file a sync adds or changes carries. So a module added after its importers were indexed, restored after a delete, or given the name in a later edit never relinked them, while a fresh index did. #2450 fixed the same gap for ES modules. Such a reference is now parked under its module's key. A Python import names its module by a dotted path, counted from the importing file's own package when it starts with dots: `.models` in app/main.py is 'module:models', `.` is 'module:app', `..pkg` the package above's 'module:pkg'. #2450's lookup still finds it by its own name, which is how a fresh index links an alias its module never declares. A package's `__init__.py` also carries the key of the package above it, as a module file beside it does through its folder, so `from app.pkg import sub as s` relinks when the subpackage appears. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
added a commit
that referenced
this pull request
Oct 11, 2026
…the package appears later (#2507) `from fastapi import FastAPI` reaches `fastapi/applications.py` through `fastapi/__init__.py`, which re-exports it and declares no node named `FastAPI`. While that `__init__.py` was missing, or before it gained the re-export, the calls through the name failed and were parked under the name, which the package file that then appears or changes never carries. A fresh index linked them; a sync never did. An unaliased from-import of a module in the project's own folders is now parked under the module's key, as an aliased one is (#2501): a relative import, or an absolute one whose dotted path, or the package holding it, is a project folder at any depth. An outside package's names (`from django.db.models import Q`) keep their tails, since on netbox they would crowd 'module:models', 'module:urls' and others past the retry ceiling. #2450's by-name lookup still finds every parked ref by its own name. Two limits made the restored package's importers wait anyway: - A module key gets a retry ceiling of its own, 5,000 rather than the 500 a name tail has. A large population under a module key is every importer of a package that was missing, not noise: fastapi's 1,069 `from fastapi import ...` refs, Django's 2,257 under 'module:urls'. - The CG-33 rebind's per-name ceiling counts only the edges that existed before the retry. Relinking every `from django.urls import path` pushed `path` over it, so the `path` edges the sync's new definitions moved were never rebound; the retry's own edges are still rebound under a name that stays below it, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Problem
codegraph sync(and the watcher) never relinked a reference through a Python import alias, a binding whose local name differs from the name its module declares, when the module was added after the importing file was indexed, came back after a delete, or gained the name in a later edit. A fresh index links it.from .models import User as UserModel,from app.models import Group as GroupModelfrom . import helpers as h(a submodule),from .pkg import sub as subpkg(a subpackage),from .. import helpers as hhfrom settings import Config as Cfg(a new top-level module)Every reference through the alias stayed failed: the import itself, a call (
UserModel(),mk()), a base class, and the class or function passed as a value (isinstance(u, UserModel),return mk).Repro (from #2450's residue list):
app/__init__.pyempty,app/main.py=from .models import User as UserModel/from . import helpers as h/def run(): return UserModel(), h; index, addapp/models.pyandapp/helpers.py, sync. A fresh index hasapp/main.py -imports-> class User(refNameUserModel),-imports-> file app/helpers.py(refNameh) andrun -instantiates-> User; the synced graph kept those three refs failed with tailsUserModel/h.Cause
Sync's #1240 retry finds a parked failed ref by
name_tail, looked up by the changed files' node names and theirmoduleReferenceKeys. Python's binding refs carry the local alias, which no changed file declares. #2450 fixed this for ES modules (importBindingTailparks such a ref undermodule:<stem>and the retry also finds module-parked rows by whole name); its first line returned early for every non-ESM language.Fix
importBindingTailgets a Python branch. A whole-name ref through a binding whose local name differs from the module's name for it (from X import A as B,import X.Y as Z) is parked undermodule:<last segment of the module>. The module comes from Python's dotted path, counted from the importing file's own package when it starts with dots:.modelsinapp/main.py→module:models,.→module:app,..pkgfromapp/sub/→module:pkg,app.models→module:models; dots that climb above the project park nothing new. An unaliased import keeps its tail (the module, or the one it re-exports from, declares that name), as does a standard-library import (isExternalImport's list).moduleReferenceKeysgives a package's__init__.pythe key of the package above it, as a module file beside it already has through its folder:app/pkg/sub/__init__.pyis whatfrom app.pkg import sub as s(parked asmodule:pkg) imports.from .lib import FormSchemaBase as FormSchema→ another file'sFormSchema), and that relinks as before.An installed package's import can't be told apart from a module the project doesn't have yet (
from settings import Config as Cfgmust relink whensettings.pyappears), so it is parked under its module too. That is netbox'sfrom django.utils.translation import gettext_lazy as _: 8,611_()calls now wait undermodule:translationinstead of_. Both keys are over the 500 ceiling, so nothing retries them, as before.No extraction change (so no kernel change) and no schema change. Rows parked by an older version keep their alias tail (still found by name); a re-index parks them under their modules, hence the CHANGELOG's re-index note.
Validation
Tests:
__tests__/sync-python-import-binding-retry.test.ts(5). Added / restored / edited-module scenarios over relative, package-relative (.,..), absolute and top-level imports, a submodule file, a subpackage, a re-export through a package__init__.py, and a function alias; each is compared edge for edge with a full index of the final tree in a second folder. Plus the by-name case and the parking keys. On main (fc0da38) 4 of the 5 fail: 0 of 19 links after add, 0 of 19 after restore, 5 of 19 after the edit, and the parking test. The by-name case passes on main by design and fails when #2450's by-name lookup is removed (0 of 2 edges), which is the path it guards. All 5 pass with the kernel and withCODEGRAPH_KERNEL=0. Related suites (allsync*retry suites,sync.test.ts, convergence, the Python resolution suites; 235 tests) pass, as does tsc. Full suite: 6,927 passed, 0 failed (82 skipped).A/B: main = fc0da38 and fix = this branch, with the same kernel. Python repos: django, netbox (Django), pydantic, fastapi, flask, requests, httpx, microblog (Flask), fastapi-template and django-realworld. Non-Python controls: ts-express, takenote, vue3-element-admin, gin, serilog and kong.
refsdiffer only in thename_tailof refs that already failed (nowmodule:*): requests 32, microblog 26, flask 22, fastapi 37, pydantic 55, django 171, netbox 8,699 (8,611 of them netbox's_calls).Every residue bucket that changed went down: requests 10 (hold) and 9 (gut), django 20 and 17, pydantic 5, netbox 1. Examples are
DjangoTestCase,MakeMigrationsCommand,PythonSerializer,DjangoWarningandRequestsSSLError. None went up. The one apparent rise, pydantic's module-parked bucket going from 0 to 1, is a ref that fails in both arms and only changed tail (see Residues). For the controls, the synced graphs themselves are byte-identical between arms.Resolver pool: the parking decision also runs in pool workers. requests and pydantic indexed with the pool forced on (
CODEGRAPH_PARALLEL_RESOLVE_MIN=0) and off (CODEGRAPH_NO_PARALLEL_RESOLVE=1) give the samemodule:tails. pydantic differs by one pre-existingfunction_ref, the same on main:foo = validate_call(foo)in tests/test_validate_call.py links to the nesteddef fooonly when pooled.Cost: the decision runs once per failed ref, with the file's mappings already cached by the attempt: ~0.1 s per full index on django (137K failed Python refs) and netbox (104K), ~15 ms on pydantic. That's about 0.1–0.3% of an index. The workspace map
isExternalImportreads is already loaded byresolveImportPathbefore any ref is parked. Sync adds one lookup key per nested__init__.py.Residues (pre-existing, unchanged)
__init__.pythat appears later:from fastapi import FastAPI, wherefastapi/__init__.pyre-exportsfrom .applications import FastAPI as FastAPI. The ref waits underFastAPI, and the restored__init__.pydeclares no such node. fastapi holdsFastAPI731,APIRouter165 andHTTPException75 failed calls; netbox has the same for itsutilities/forms/widgetsclasses. This is the Python side of the barrel case. Parking these under their module could crowd keys likemodule:modelspast the 500 ceiling in Django apps, so it needs its own measurement; filed as a follow-up.httpx.MockTransport61, pydanticm.model_dump216.from ._internal._schema_generation_shared import GetJsonSchemaHandler as _GetJsonSchemaHandlerwaits undermodule:_schema_generation_shared. The class lives inannotated_handlers.py, whose keys don't name that module. It fails on main too, under its alias.v.validate_python) are the same in both arms.Overlap
importBindingTailand relies on its by-name lookup (schema v15); no schema change here.import-resolver.ts,queries.tsand sync, but notimportBindingTailormoduleReferenceKeys. Whichever lands second may need a small rebase.🤖 Generated with Claude Code