Skip to content

fix(python): a class with PEP 695 type parameters reads its base list - #3111

Merged
colbymchenry merged 1 commit into
mainfrom
claude/pr-3105-python-pep695-bases
Oct 30, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/pr-3105-python-pep695-bases

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

In Python, a class written with PEP 695 type parameters, like class Repo[T](Base[T]):, now reaches the methods its bases declare, through a module global and through the class name, as a class without type parameters already did.

Lands #3105 by @JosefAschauer.

Root cause

pythonBaseList, the resolver's reader behind module-global, self. and class-name dispatch, expected the ( of the base list right after the class name. Here it found the [ of the type parameters, so the class seemed to have no bases. Extraction already gave these classes their extends edge.

What came from the contributor, and what we changed

From #3105, unchanged: the reader change in name-matcher.ts, which skips a balanced type parameter list, and the test file.

Ours: the CHANGELOG bullet placed after the newest Python bullet, with the credit added.

Validation

Stage A: every changed line of 34250df read. Scanner clean: the only hits are RegExp.exec, the test's temp-dir writes and its ../src import.

Reproduced on main with a fixture of ours, kernel on: Repo.run(None), a wrapped list, a constraint tuple and a module global reach nothing. With this change they reach Base::run, class NoBases[T] reaches neither run, and Typed[T](Base[User]) doesn't reach User.save.

Tests: 4 of 7 fail on main (the 3 controls pass), 7 pass here. The multiline, generic-base and type-argument suites still pass.

A/B against main, kernel on: no change.

Repo Edges Change
psf/requests @ 3c7baaa 2,976 0
pallets/flask @ 49943b5 5,292 0
litestar-org/litestar @ 716f8b0 42,376 0
python/mypy @ d8a43fb 70,811 0
sqlalchemy/sqlalchemy @ 2d8e649 229,518 0
polarsource/polar server/polar @ 8dd9956 62,291 0
django/django django/ @ a6a5d1e 46,227 0
wxxb789/vprism @ 5b2efb1 4,043 0
mxpadidar/fastup @ 8a38647 1,767 0
jenreh/runic @ 0b334b5 20,548 0
kuzmenkous/relikt-arte-api @ 9842822 528 0
expressjs/express @ 1c6f1b0 (control) 2,296 0
gin-gonic/gin @ 0f09c3a (control) 8,073 0

PEP 695 bases are still rare, so this mostly closes a gap rather than moving real graphs. polar has 39 such classes, and the four small apps were picked because they use them, but no call in these repos is decided by this reader on such a class. Controls have byte-identical full dumps.

Cost: sqlalchemy, median of 3 interleaved runs: 23.4 s on main and here.

Suite: 840 files (834 passed, 6 skipped), 9,887 tests passed, 0 failed, 87 skipped, on 303a881d, kernel on, 2 workers, Vitest 2. This head is that commit rebased onto main, which then moved the tests to Vitest 4 (#3110) and shares no source or test file with it. After npm ci, its test file and the 4 other Python base suites pass on Vitest 4 (42 tests).

🤖 Generated with Claude Code

pythonBaseList, the resolver's reader of a Python base list behind module-global,
self. and class-name dispatch, expected the ( of the list right after the class
name. In class Repo[T](Base[T]): it found the [ of the type parameters, so the
class seemed to have no bases there, though extraction gave it its extends edge:
current = Repo() then current.run(), and Repo.run(x), reached nothing.

The reader now skips a type parameter list between the name and the bases, with
brackets balanced, so bounds like T: dict[str, int], *Ts and **P go whole, and a
constraint tuple like [T: (int, str)] isn't taken for the base list. Either list
can wrap over several lines. Anything else after the name and its parameters,
such as the : of class NoBases[T]:, still means no base list.

Validation:
- The test file: 4 of 7 fail on main (the controls pass), 7 pass here, and the
  multiline, generic-base and type-argument suites still pass.
- A fixture of ours on the built CLI, kernel on: Repo.run(None), a wrapped
  list, a constraint tuple and a module global reach no method on main and
  Base::run here. class NoBases[T] reaches neither run, and
  Typed[T](Base[User]) does not reach User.save.
- A/B against main on 11 Python repos, 486,377 edges (requests, flask,
  litestar, mypy, sqlalchemy, polar's and django's packages, and four small
  apps that use PEP 695 bases): nothing changes. Neither do express and gin,
  whose full dumps are byte-identical. polar has 39 such classes, and none is
  reached by a call this reader decides.
- Index cost on sqlalchemy, median of 3 interleaved: 23.4 s on main and here.

Original PR #3105 by @JosefAschauer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit bea5e20 into main Oct 30, 2026
@colbymchenry
colbymchenry deleted the claude/pr-3105-python-pep695-bases branch October 30, 2026 22:48
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.

2 participants