Skip to content

fix(python): a class-name call doesn't follow a base imported from outside the project - #3115

Merged
colbymchenry merged 1 commit into
mainfrom
claude/python-class-call-outside-base
Oct 30, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/python-class-call-outside-base

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

In Python, a call on a class whose base comes from outside the project no longer links to a project class that only shares the base's name. With class Color(Enum) after from enum import Enum, or class Color(enum.Enum), Color.name_of() no longer reaches a project Enum.name_of.

Follow-up to #3109, which fixed the extends edge and named this gap.

Root cause

The reader behind class-name calls doesn't use the extends edge. declaredSupertypes gives the names in the base list, and findInheritedClassMethod looks each one up by name. So the import never counted.

On sqlalchemy this produced a chain worth seeing. Query(…, Generic[_T]) has Generic from typing, the walk landed on a test's local class Generic, then on that test's Base, and on to FunctionElement, so query.select() linked to FunctionElement.select.

Fix

The walk now skips a project class whose name the class head binds outside the project. It uses the check from #3109, moved to name-matcher.ts so both callers share it: the import resolver already imports from there, not the reverse. The check reads the module through context.resolveModuleFile, the seam name matching already has.

A dotted base is bound through its first word: enum.Enum through import enum, e.Enum through import enum as e, and collections.abc.Mapping through import collections.abc, which the check now counts as binding collections.

Python only. TS, JS and the rest of the walk are unchanged.

Validation

Tests: python-outside-bases.test.ts gains 4. A named import, a receiver named after the class, and dotted, aliased and deep bases reach nothing. A project base and a class in a file the import path names still reach their methods. Without the gate, 2 of the 4 fail. Taking out the dotted word or the import a.b binding fails the dotted one. 11 pass here.

A/B against main, kernel on: 13 repos, 496,746 edges, 10 calls removed, 0 added, 0 moved, no extends touched.

Repo Edges Removed
sqlalchemy/sqlalchemy @ 2d8e649 229,518 10
python/mypy, litestar, polar server/polar, django django/, flask, requests, and the four small PEP 695 apps from #3105 256,859 0
expressjs/express, gin-gonic/gin (controls) 10,369 0

All 10 were read and all were wrong, each through typing's Generic into a test's own class Generic:

Removed Call Wrong target
4 value.self_group() FunctionElement.self_group
4 query.select() FunctionElement.select
1 query.alias() FunctionElement.alias
1 query.compile() DQLDMLClauseElement.compile

Cost: sqlalchemy, median of 3 interleaved runs: 27.8 s on main, 26.4 s here. That gap is noise, so nothing measurable.

Suite: 841 files (835 passed, 6 skipped), 9,890 tests passed, 2 expected failures, 0 failed, 87 skipped, on this exact commit, kernel on, 4 workers, Vitest 4.

🤖 Generated with Claude Code

…tside the project

PR #3109 stopped a base imported from a package linking to a project class of
its name, but only for the extends edge. The reader behind class-name calls
follows the base list as written: declaredSupertypes gives the names and
findInheritedClassMethod looks each up by name. So with class Color(Enum) after
from enum import Enum, Color.name_of() still reached a project Enum.name_of, as
did color.name_of() on a receiver named after the class. On sqlalchemy,
Query(..., Generic[_T]) with Generic from typing walked to a test's local
class Generic, then to that test's Base and on to FunctionElement, so
query.select() linked to FunctionElement.select.

The walk now skips a project class whose name the head binds outside the
project, with the check PR #3109 added, moved to name-matcher.ts so both can
call it (the import resolver already imports from there, not the reverse). A
dotted base is bound through its first word: enum.Enum through import enum,
e.Enum through import enum as e, collections.abc.Mapping through import
collections.abc, which the check now counts as binding collections.

Validation:
- python-outside-bases.test.ts gains the calls: named import, receiver, dotted,
  aliased and deep bases reach nothing, and a project base and a class in a file
  the import path names still reach their methods. Without the gate 2 of the 4
  new tests fail, and taking out the dotted word or the import a.b binding fails
  the dotted one. 11 pass here.
- A/B on 11 Python repos plus express and gin, 496,746 edges: 10 calls removed,
  0 added, 0 moved, 0 extends touched. All 10 are in sqlalchemy and all were
  wrong: 6 query.select(), query.alias() and query.compile() and 4
  value.self_group(), each through typing's Generic into a test's own class
  Generic and its Base.
- Controls express and gin unchanged.
- Index cost on sqlalchemy, median of 3 interleaved: 27.8 s on main, 26.4 s here,
  which is noise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit c0b4906 into main Oct 30, 2026
@colbymchenry
colbymchenry deleted the claude/python-class-call-outside-base branch October 30, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant