Skip to content

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

Closed
JosefAschauer wants to merge 1 commit into
colbymchenry:mainfrom
JosefAschauer:fix/python-pep695-bases
Closed

JosefAschauer wants to merge 1 commit into
colbymchenry:mainfrom
JosefAschauer:fix/python-pep695-bases

Conversation

@JosefAschauer

Copy link
Copy Markdown
Contributor

What

This is a follow-up to #3103, covering the first gap its notes list. A Python class written with PEP 695 type parameters, class Repo[T](Base[T]):, had no base list for the resolver's reader pythonBaseList. That reader sits behind module-global, self. and class-name dispatch, and since #3103 it also backs declaredSupertypes. It expected the ( of the base list right after the class name, and found the [ of the type parameters instead.

pythonBaseList now skips a type parameter list between the name and the bases:

  • Brackets are balanced, so bounds such as T: dict[str, int], *Ts and **P are skipped whole.
  • A constraint tuple inside the parameters, [T: (int, str)], is not taken for the base list.
  • A parameter list and a base list can each wrap over several lines.
  • Anything other than whitespace after the name and its parameters (:) still means the class has no base list, so class NoBases[T]: gets none.

Extraction already gave these classes their extends edge. The test keeps that as a control; the gap was only in dispatch.

Tests

__tests__/python-pep695-bases.test.ts. Base and Other both declare run, so only the class's own bases can pick one. Each case is checked through a module global and through the class name:

  • a classic base list (control);
  • Repo[T](Base[T]);
  • bounds with brackets plus *Ts and **P;
  • a constraint tuple;
  • a wrapped parameter list and a wrapped base list;
  • the extends edges (control);
  • a parameterized class without bases, which must reach neither run.

Without the change, the 4 PEP 695 dispatch cases fail; with it, all 7 pass. python-multiline-bases and python-generic-bases still pass.

Full suite on main 27c904b plus this change: 9336 passed, 8 failed. The 8 are bundle-launcher x2, cli-sync x2 and gitignore-pattern-parity x4, and they fail the same way on plain main here. tsc --noEmit is clean.

The second gap from those notes, a # or ) inside a string in a base list, is left as it is: it would take a tokenizer for close to no real code.

🤖 Generated with Claude Code

https://claude.ai/code/session_018hJjBUPFiLSFBssshFwHzq

pythonBaseList, behind module-global, self. and class-name dispatch,
expected the base list's "(" right after the class name, so a class
written `class Repo[T](Base[T]):` had no bases for those readers. It
now skips a type parameter list between the name and the bases,
bracket-balanced and wrapped or not, so a constraint tuple inside it
(`[T: (int, str)]`) is not taken for the base list. A parameterized
class without bases still has none. Extraction already gave these
classes their extends edge; the test keeps that as a control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hJjBUPFiLSFBssshFwHzq
colbymchenry pushed a commit that referenced this pull request Oct 30, 2026
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

Copy link
Copy Markdown
Owner

Thanks @JosefAschauer! This landed as #3111 with you as the commit author, with your fix and test unchanged. On main, a class like Repo[T](Base[T]) reached nothing through a module global or the class name, and now it reaches Base. We only added the credit to the CHANGELOG bullet and placed it beside the other Python entries.

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