Files
codegraph/__tests__/python-local-binding.test.ts
Colby MchenryandClaude Opus 5.5 71202c38df fix(python): a bare call to a locally bound name is that value (#2198)
A bare Python call whose name the enclosing function binds - an assignment
(`view = SomeView.as_view()`, tuple and annotated forms), a parameter, a
`for` / `with ... as` / `except ... as` target - or the module binds at top
level, no longer resolves to a same-named function in another file. DRF's
tests write `view = X.as_view()` then `view(request)`: 118 such calls went
to one test file's `def view`. Read from code only (docstrings and comments
blanked), and an imported name is left to the import. A pytest fixture
(`@pytest.fixture`, or anything in a conftest.py) is still what a test's
parameter of that name calls.

django-rest-framework -122, pytest -25 (`func` / `xfail` params and
aliases), allauth -12 (`serialize_instance = get_adapter().serialize_instance`),
netbox -6 (migrations' `apps.get_model(...)`), mealie -3, flask -2;
healthchecks, flaskbb, httpx unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:07:30 +00:00

88 lines
2.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* A bare Python call to a name its function (or module) binds itself —
* DRF's `view = SomeView.as_view()` then `view(request)` — is that value,
* not another file's `def view` (118 such calls went to one test file's
* `view`). A pytest fixture the test takes as a parameter is still the
* fixture, and an imported name is the import's, whatever a docstring says.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-py-local-'));
const files: Record<string, string> = {
'tests/test_testing.py': `def view(request):
return request
`,
'tests/test_versioning.py': `class TestVersion:
def test_version(self, request):
view = MyView.as_view()
response = view(request)
return response
`,
'tests/conftest.py': `import pytest
@pytest.fixture
def user_factory():
def make():
return 1
return make
`,
'tests/test_users.py': `def test_user(user_factory):
user = user_factory()
return user
`,
'app/utils.py': `def user_display(user):
return str(user)
`,
'app/tags.py': `from app.utils import user_display
def user_display_tag(user):
"""
{% user_display user as user_display %}
"""
return user_display(user)
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
const callsFrom = (file: string) => {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'calls').map((e) => {
const t = cg.getNode(e.target)!;
return `${t.filePath}: ${t.qualifiedName}`;
}).sort();
};
describe('bare Python calls to locally bound names', () => {
it('are the local value, not another file’s function of that name', () => {
expect(callsFrom('tests/test_versioning.py')).not.toContain('tests/test_testing.py: view');
});
it('still reach a pytest fixture taken as a parameter', () => {
expect(callsFrom('tests/test_users.py')).toContain('tests/conftest.py: user_factory');
});
it('still reach an imported function a docstring happens to mention', () => {
expect(callsFrom('app/tags.py')).toContain('app/utils.py: user_display');
});
});