Repository navigation
fix(sync): a Python import of a name a package re-exports links when the package appears later - #2507
Merged
Merged
Conversation
…the package appears later `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 an unaliased Python import of a name a package re-exports when the package's__init__.pywas added after the importing files were indexed, or gained the re-export in a later edit. A fresh index links it.from fastapi import FastAPIreachesfastapi/applications.pythroughfastapi/__init__.py(from .applications import FastAPI as FastAPI). With__init__.pymissing, the calls through the name (FastAPI(),APIRouter(),HTTPException(...)) fail and stay failed after it comes back. In a hold-out run on fastapi (every 7th source file held out, restored and synced), that leftFastAPI735,APIRouter166 andHTTPException75 calls unlinked. netbox had the same for itsutilities/forms/widgetsclasses, and Django for everything imported fromdjango.urlsanddjango.test. Follow-up to #2501; residue noted there.Cause
Three things, each of which alone kept the importers unlinked:
FastAPI). Sync's Incremental sync misses cross-file relations after a new export is added #1240 retry looks failed refs up by the changed files' node names and module keys, and a re-exporting__init__.pydeclares no node of that name (a re-export only makes an import node). fix(sync): a Python import alias links its module when the module appears later #2501 parked aliased Python bindings under their module and left unaliased ones on their names.module:fastapi: 1,069 of them, over the 500-per-key ceilinggetRetryableFailedReferencesapplies, so they were skipped anyway. Django: 2,257 undermodule:urls, 1,867 undermodule:test.resurrectStaleResolutionEdges), whose per-name ceiling counts every edge whose target has the name. Relinking everyfrom django.urls import pathpushedpathover 500, so the rebind skippedpathand left thepathedges this sync's new definitions moved unrebound. That was traced with an instrumented sync: on main all three refs are re-opened by the rebind and re-resolved in the sweep; on the first fix arm none was.Fix
pythonImportBindingTail): an unaliasedfrom X import Yis parked undermodule:<last segment of X>, as an alias is, when X is in the project's own folders. That means a relative import, or an absolute one whose dotted path, or the package holding it, is a project folder at any depth (isProjectPythonModule, a per-pass memo of folder-path suffixes). It's a folder, not a module, becausefastapi/__init__.pyis what goes missing whilefastapi/keeps its other files. An outside package's names keep their tails:from django.db.models import Q,from django.urls import pathin netbox. Parking those would crowd netbox'smodule:urls(1,575),module:models(1,140),module:exceptions(876) past the ceiling, and the project's own refs under the same keys with them. An unaliasedimport xkeeps its tail, as before. fix(sync): a reference through an import binding links its module when the module appears later #2450's by-name lookup still finds every parked row by its own name.get,map). A module key's population is every importer of one module, which the file that just appeared resolves at once. The ceiling now only bounds the work. This also covers JS/TS modules parked by fix(sync): a reference through an import binding links its module when the module appears later #2450/fix(sync): a route links the module it lazily loads when the module appears later #2422 with more than 500 waiting refs. fix(sync): a reference through an import binding links its module when the module appears later #2450 measured one on vscode,module:sessionStatewith 546 refs that fail in a fresh index too; it would now be retried when such a file changes, which is wasted work but no change in the graph. Not re-measured here.getResolutionEdgesByTargetName(…, countedUpToEdgeId),getMaxEdgeId;edges.idis AUTOINCREMENT). The retry's own edges were resolved against this sync's graph a moment earlier. They are still re-opened under a name that stays below the ceiling, exactly as on main, so the rebind's choice of names equals main's whenever the retry adds no edges.No schema change, no extraction change (no kernel change).
Validation
Tests:
__tests__/sync-python-import-binding-retry.test.ts, now 10 tests:from shop import Order), one from the package above (from .. import Order), and a renamed re-export, so nothing namedOrderexists for a name lookup.Kit as Kit), compared link for link.from django.db.models import Qand stdlib imports keep their tails.getMaxEdgeIdmissing. The 4 alias tests from fix(sync): a Python import alias links its module when the module appears later #2501 pass on both.CODEGRAPH_KERNEL=0.sync*andpython*suite, 238 tests) pass. Full suite: 6,932 passed, 0 failed (82 skipped). tsc clean.A "restored after delete" scenario isn't included. Deleting a re-export-only
__init__.pydoesn't unlink anything: the edges that resolved through it target nodes in files that still exist. That's a separate convergence gap in the removal direction, the same on main, and filed as a follow-up.A/B: main = 5171a6a, fix = this branch, same kernel.
Python repos: django, netbox, pydantic, fastapi, flask, requests, httpx, microblog, fastapi-template, django-realworld.
Non-Python controls: ts-express, takenote, vue3-element-admin, gin, serilog, kong.
Fresh index: the six controls, httpx and django-realworld are byte-identical (nodes, edges, refs, files). Everywhere else, nodes, edges and files are byte-identical, and only the
name_tailof refs that already failed changes: requests 116, microblog 1, flask 34, fastapi-template 12, netbox 237, django 1,421, fastapi 2,172, pydantic 3,046.Late scenarios: every 7th source file held out and then restored ("hold"), or every 7th Python file emptied and then restored ("gut"), then synced. Each arm's synced graph is compared with its own fresh index; the numbers are differing dump lines, main → fix.
Every residue bucket that changed went down: django 80 (hold) and 12 (gut), netbox 25, fastapi 8, pydantic 2. None went up, and the 107 + 2 rebind mismatches the first fix arm had are gone.
Cost:
module:testclient(1,484, re-exported from starlette), pydanticmodule:pydantic_core(2,432, compiled-extension names) and netboxmodule:translation(8,612,gettext_lazy as _, from fix(sync): a Python import alias links its module when the module appears later #2501). Each sits over the 5,000 module ceiling or is never resolvable, so it costs nothing. A key over the ceiling also drops the module trigger for anything else parked under it; on pydantic that is 5 aliased refs into the same unresolvable package.Resolver pool: the decision also runs in pool workers. fastapi and django indexed with the pool forced on (
CODEGRAPH_PARALLEL_RESOLVE_MIN=0) and off (CODEGRAPH_NO_PARALLEL_RESOLVE=1) park the samemodule:tails (2,209/2,209 and 1,593/1,593). fastapi is byte-identical. django differs by one pre-existing nested-functionfunction_ref(async_view), which is the same on main, like pydantic'sfoonoted in #2501.Residues (pre-existing, the same on main)
exact-matchwhere a fresh index saysimport(fastapi 961, django 3,065, netbox 261). This includes fastapi'sfrom fastapi import FastAPIimports themselves; their calls are what this PR relinks.django.testmodule-path imports wait under the tailtest, which is over 500.self.assertHttpStatus801,add_permissions576, andextends TestCase417, which imports Django'sTestCasethat a fresh index links by name to netbox's own.__init__.pyis deleted (see Validation); filed separately.Overlap
pythonImportBindingTail; the alias behavior is unchanged.import-resolver.ts,queries.tsand sync, but none of these functions. Whichever lands second may need a small rebase.🤖 Generated with Claude Code