mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 03:49:59 +08:00
In Python, `from loader import X` with loader.py at a package root left the module reference `loader` unresolved, so the importing file had no imports edge to loader.py. `import loader` and `from pkg.loader import X` both link their module's file. The from-import's names (X) linked, but `codegraph affected` and impact could miss the file's dependency on the module. v1.6.2 behaves the same. Root cause: resolvePythonAbsoluteModule resolves only dotted module refs. It leaves bare names to resolveModuleImportToFile, because a bare imported name like `authentication` in `from app.api.routes import authentication` is ambiguous on its own. But resolveModuleImportToFile matches an import by its local name, which for `from loader import X` is X, never `loader`. So the module of an undotted from-import was never looked up by its path. When the file also bound the module's name some other way, the generic import loop resolved it through that binding: next to `from django.core import mail`, the module of `from mail import ...` became django/core/mail/__init__.py. Django's tests/mail/__init__.py is the module meant, and the statement's own names already resolve there. Fix: resolvePythonAbsoluteModule also takes the module a from-import names (isPythonFromImportModule), dotted or not. It resolves it from the package roots, as `import loader` is resolved. Imported names keep their path, and a module from outside the project still links nothing. Validation: - New __tests__/python-from-import-module-file.test.ts, 7 tests: a root-level module, a top-level package, a src layout, a module named on a continuation line, a submodule imported by name, sync after the module file is added (matching a fresh index), and an outside module with a same-named nested file (links nothing). With main's import-resolver.ts the 6 positive tests fail and the guard passes. Seven assertions in python-module-resolution.test.ts listed a file's import edges exactly while the module edge was missing; they now include the module's file. What they test is unchanged: where the imported name binds, and no cycle. - A/B against maind5fc1fbb. Both arms were built from their own sources with the kernel. Rebased onto628f69e2(PR #2822, Kotlin only), the fix arm's graphs are byte-identical to before the rebase: - requests: +2 edges; - sqlalchemy: +3,189; - django's 2,938 Python files: +103 edges and 2 retargeted; - every change is a from-import's module linking its file, and nothing else changed; - express and gin (controls): byte-identical. Triage of all 3,296 added edges: - 3,284 point at the file, or the package folder, where the same statement's imported names already resolve; - the other 12 were read by hand: multi-line statements, re-exported constants the name resolver doesn't follow, and a deliberately broken name in a Django test fixture, each on the right module file; - the 2 django retargets are the mail case above. - Cost: index CPU medians of 3 interleaved runs, main vs fix: sqlalchemy 100.7 s vs 100.3 s, django 98.4 s vs 97.8 s. Found while verifying another Python issue in the sweep; no issue was filed for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
5.1 KiB
TypeScript
130 lines
5.1 KiB
TypeScript
/**
|
|
* The module a Python `from X import y` names is a file of the project when X
|
|
* is one, dotted or not: `from loader import X` with `loader.py` at a package
|
|
* root links the importing file to it, as `import loader` and `from
|
|
* pkg.loader import X` already did. Before, an undotted module of a
|
|
* from-import was left unresolved, so the file lost its dependency on the
|
|
* module and `codegraph affected` missed it. A module from outside the
|
|
* project still links nothing.
|
|
*/
|
|
|
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import CodeGraph from '../src/index';
|
|
|
|
let tempDir: string;
|
|
let cg: CodeGraph | null = null;
|
|
|
|
function write(files: Record<string, string>): void {
|
|
for (const [rel, source] of Object.entries(files)) {
|
|
fs.mkdirSync(path.dirname(path.join(tempDir, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(tempDir, rel), source);
|
|
}
|
|
}
|
|
|
|
/** `kind:name@file` of every target of an `imports` edge from `fromFile`'s file node, sorted. */
|
|
function importEdges(fromFile: string): string[] {
|
|
const file = cg!.getNodesByKind('file').find((n) => n.filePath === fromFile)!;
|
|
expect(file).toBeDefined();
|
|
return cg!
|
|
.getOutgoingEdges(file.id)
|
|
.filter((e) => e.kind === 'imports')
|
|
.map((e) => cg!.getNode(e.target))
|
|
.filter((n): n is NonNullable<typeof n> => !!n)
|
|
.map((n) => `${n.kind}:${n.name}@${n.filePath}`)
|
|
.sort();
|
|
}
|
|
|
|
async function indexed(files: Record<string, string>): Promise<void> {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-from-module-'));
|
|
write(files);
|
|
cg = await CodeGraph.init(tempDir, { index: true });
|
|
}
|
|
|
|
afterEach(() => {
|
|
cg?.close();
|
|
cg = null;
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
const LOADER = 'class Registry:\n def get(self, k):\n return k\n\n\ndef load_registry():\n return Registry()\n';
|
|
|
|
describe('the module of a Python from-import links its file', () => {
|
|
it('links a top-level module at the project root, as `import loader` does', async () => {
|
|
await indexed({
|
|
'loader.py': LOADER,
|
|
'packs.py': 'from loader import Registry, load_registry\n\n\ndef use():\n return load_registry()\n',
|
|
'plain.py': 'import loader\n\n\ndef use():\n return loader.load_registry()\n',
|
|
});
|
|
expect(importEdges('packs.py')).toEqual([
|
|
'class:Registry@loader.py',
|
|
'file:loader.py@loader.py',
|
|
'function:load_registry@loader.py',
|
|
]);
|
|
expect(importEdges('plain.py')).toEqual(['file:loader.py@loader.py']);
|
|
});
|
|
|
|
it('links a top-level package to its __init__.py, as a dotted from-import links its package', async () => {
|
|
await indexed({
|
|
'pkg/__init__.py': 'def helper():\n return 1\n',
|
|
'pkg/sub/__init__.py': 'def deep():\n return 2\n',
|
|
'app.py': 'from pkg import helper\nfrom pkg.sub import deep\n',
|
|
});
|
|
expect(importEdges('app.py')).toEqual([
|
|
'file:__init__.py@pkg/__init__.py',
|
|
'file:__init__.py@pkg/sub/__init__.py',
|
|
'function:deep@pkg/sub/__init__.py',
|
|
'function:helper@pkg/__init__.py',
|
|
]);
|
|
});
|
|
|
|
it('finds the module from the package roots, as in a src layout', async () => {
|
|
await indexed({
|
|
'pyproject.toml': '[project]\nname = "app"\n',
|
|
'src/app/__init__.py': 'def helper():\n return 1\n',
|
|
'tests/test_app.py': 'from app import helper\n\n\ndef test_helper():\n assert helper() == 1\n',
|
|
});
|
|
expect(importEdges('tests/test_app.py')).toEqual([
|
|
'file:__init__.py@src/app/__init__.py',
|
|
'function:helper@src/app/__init__.py',
|
|
]);
|
|
});
|
|
|
|
it('links a module named on a continuation line of the statement', async () => {
|
|
await indexed({ 'loader.py': LOADER, 'packs.py': 'from \\\n loader import Registry\n' });
|
|
expect(importEdges('packs.py')).toEqual(['class:Registry@loader.py', 'file:loader.py@loader.py']);
|
|
});
|
|
|
|
it('links nothing for a module from outside the project, even one a nested file is named like', async () => {
|
|
await indexed({
|
|
'tests/fixtures/requests.py': 'class Session:\n pass\n',
|
|
'client.py': 'from requests import Session\nimport requests\n',
|
|
});
|
|
expect(importEdges('client.py')).toEqual([]);
|
|
});
|
|
|
|
it('keeps a submodule the statement imports by name', async () => {
|
|
await indexed({
|
|
'pkg/__init__.py': '',
|
|
'pkg/loader.py': LOADER,
|
|
'app.py': 'from pkg import loader\n',
|
|
});
|
|
expect(importEdges('app.py')).toEqual(['file:__init__.py@pkg/__init__.py', 'file:loader.py@pkg/loader.py']);
|
|
});
|
|
|
|
it('links the module once it is added, on sync as on a fresh index', async () => {
|
|
await indexed({ 'packs.py': 'from loader import Registry\n' });
|
|
expect(importEdges('packs.py')).toEqual([]);
|
|
write({ 'loader.py': LOADER });
|
|
await cg!.sync();
|
|
const synced = importEdges('packs.py');
|
|
cg!.close();
|
|
fs.rmSync(path.join(tempDir, '.codegraph'), { recursive: true, force: true });
|
|
cg = await CodeGraph.init(tempDir, { index: true });
|
|
expect(synced).toEqual(importEdges('packs.py'));
|
|
expect(synced).toEqual(['class:Registry@loader.py', 'file:loader.py@loader.py']);
|
|
});
|
|
});
|