Skip to content

fix(python): a base list wrapped over several lines is read whole - #3103

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

colbymchenry merged 1 commit into
mainfrom
claude/pr-3001-python-wrapped-bases

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

In Python, a class whose base list is wrapped over several lines, as black and ruff write a long one, now reaches the methods its bases declare, through a module global like current = Wrapped() then current.run() and through the class name like Wrapped.run(x), as a one-line list already did.

Lands #3001 by @JosefAschauer.

Root cause

Both of the resolver's readers of a base list stopped early. pythonBases, behind module-global and self. dispatch, read only the class line. declaredSupertypes, behind class-name calls, read three lines and needed the closing parenthesis on them. Extraction already read the whole list, so the class had its extends edges and only these readers missed them.

What came from the contributor, and what we changed

From #3001: pythonBaseList, both readers using it, and the test file.

Ours:

Validation

Stage A: every changed line read, on 17e2f5b and its commit 5d50558. Scanner clean: the only hits are RegExp.exec, the test's temp-dir writes and its ../src import.

The gap from the review on #3001 is closed: Wrapped.run(None) on a wrapped base list reaches Base::run, with the kernel and with wasm, and so do a list over five lines, a decorated class and a wrapped Base[Optional[User]]. On main none of these has an edge.

Tests: 7 of 9 fail without this change (the two one-line controls pass), 9 pass here. Taking the type-argument drop out of the merged case 'python' fails the new composition case and two of #3083's tests.

A/B on top of #2998, kernel on:

Repo Edges Added Removed Target moved Same target, new confidence
psf/requests @ 3c7baaa 2,976 0 0 0 0
pallets/flask @ 49943b5 5,292 0 0 0 0
litestar-org/litestar @ 716f8b0 42,397 0 0 0 0
python/mypy @ d8a43fb 70,811 0 0 0 0
sqlalchemy/sqlalchemy @ 2d8e649 229,431 → 229,549 118 0 17 113
polarsource/polar server/polar @ 8dd9956 62,305 0 0 942 0
django/django django/ @ a6a5d1e 46,228 0 0 0 0
expressjs/express @ 1c6f1b0 (control) 2,296 0 0 0 0
gin-gonic/gin @ 0f09c3a (control) 8,073 0 0 0 0

Both controls have byte-identical full dumps. polar and django are their package folders.

Every edge class, 459,440 edges before:

Change Edges Class Read as
Moved 943 calls 0.65 → 0.85 942 in polar: XRepository.from_session(session) moves from a sibling repository's from_session to RepositoryBase.from_session. One in sqlalchemy: ColumnElement._gen_cache_key
Moved 16 calls on a receiver sqlalchemy: 15 land on the typing-only SQLCoreOperations overload of the same ColumnOperators method, which ColumnElement reaches first now that its wrapped list is read. One table.alias() moves to FromClause.alias
Added 102 calls sqlalchemy's wrapped heads, class Table( and class ColumnElement(, now reach what they inherit: t.select() reaches FromClause.select
Added 16 references The same walk
Same target 113 calls, higher confidence Right
Removed 0

We read all 17 moved sqlalchemy sites, 12 of polar's, and the added list. No kernel change, so no parity run beyond #2998's: this PR reads the same kernel output.

Cost, with #2998: sqlalchemy, median of 3 interleaved runs: 23.7 s on main, 25.2 s here.

Suite: 835 files (829 passed, 6 skipped), 9,830 tests passed, 0 failed, 87 skipped, on this exact commit, kernel on, 4 workers.

Not in this PR

  • A class written with PEP 695 parameters, class Repo[T](Base[T]):, still has no base list for these readers, as before.
  • A # or ) inside a string in a base list is read as code, so such a rare list is read in part or not at all, as before.

🤖 Generated with Claude Code

Both of the resolver's readers of a Python base list stopped early. pythonBases,
behind module-global and self. dispatch, read only the class line.
declaredSupertypes, behind class-name calls, read three lines and needed the
closing parenthesis on them. black and ruff wrap a long list one base per line,
so class Wrapped(Mixin, Base) written that way seemed to have no bases:
current = Wrapped() then current.run(), and Wrapped.run(x), reached nothing a
base declares. Extraction already read the whole list.

A new pythonBaseList reads from the class line to the list's closing
parenthesis, counting nested parentheses, within the class's extent, and leaves
out comments, so a ) in one doesn't end the list. Both readers use it.
declaredSupertypes still drops type arguments first, the composition the author
posted on PR #3001, and keeps its metaclass= and factory rewrites.

Validation:
- The test file: 7 of 9 fail without this change (the two one-line controls
  pass), 9 pass here. Putting the type-argument drop out of the merged
  case 'python' fails the new composition case and the two of PR #3083.
- Wrapped.run(None) on a wrapped base list reaches Base::run with the kernel
  and with wasm, as do a list over five lines, a decorated class, and a wrapped
  Base[Optional[User]].
- A/B on top of PR #2998, 7 Python repos, 459,440 edges: 118 added (102 calls,
  16 references), 0 removed, 959 call sites moved, 113 kept their target with
  higher confidence. requests, flask, litestar, mypy and django do not change.
  polar: 942 XRepository.from_session(session) calls move from a sibling
  repository's from_session at 0.65 to RepositoryBase.from_session at 0.85.
  sqlalchemy: wrapped heads like class Table( and class ColumnElement( now
  reach what they inherit: t.select() reaches FromClause.select. Its 17 moved
  sites read right; 15 of them land on the typing-only SQLCoreOperations
  overload of the same ColumnOperators method.
- Controls express and gin: full dumps byte-identical.
- No kernel change. Index cost with PR #2998, sqlalchemy, median of 3
  interleaved: 23.7 s on main, 25.2 s here.

Original PR #3001 by @JosefAschauer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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