Repository navigation
fix(python): a base's type argument is no supertype, so Repo.save() is not User.save - #3083
Merged
Merged
Conversation
…s not User.save The Python branch of declaredSupertypes read every word of a class's base list, so class Repo(Base[User]) declared both Base and User. A call on the class or on a receiver named after it, Repo.save() or repo.save(), walks those supertypes for the method (classStrategyMethod to inheritedClassMethod), and when Base declares no save it reached User.save at 0.85 or 0.8. In class Mixed(Base[Optional[User]], Mixin) the type argument also came ahead of Mixin, so Mixed.save() reached User.save instead of Mixin.save. Bracketed type arguments, nested ones and quoted forward references too, are now dropped before the names are read. The base itself is still read and searched. Other languages are untouched. Validation: - The new test file: red on main (the type-argument and later-base cases fail, the case that a base's own method is still reached passes), green here. We added the Mixed and quoted forward-reference cases to the contributor's test. - A/B against main on 7 Python repos, 670,226 edges on main: requests, flask, litestar, mypy, polar's server, django unchanged, sqlalchemy 1 edge removed, 0 added, 0 retargeted. That edge is a.references(b) in relationships.py. Its local a named a test's class a(TypedColumns), and TypedColumns' type argument "Column[Any]" reached Column.references: the right method by chance, by the path this fixes. Its twin b.references(a) two lines down has no edge in either arm. - Controls express and gin: full dumps byte-identical. - Kernel parity on flask and requests: 43/43 in both arms (the kernel is not touched). Index cost on sqlalchemy, median of 3 interleaved: 29.5 s on main, 25.7 s here, so nothing measurable. Original PR #3000 by @JosefAschauer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
pushed a commit
that referenced
this pull request
Oct 30, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In Python, a type argument in a class's base list no longer counts as a supertype. With
class Repo(Base[User]),Repo.save()andrepo.save()no longer link toUser.save, and inclass Mixed(Base[Optional[User]], Mixin),Mixed.save()now reachesMixin.save.Lands #3000 by @JosefAschauer.
Root cause
The Python branch of
declaredSupertypesread every word of the base list, soclass Repo(Base[User])declared bothBaseandUser. A call on the class, or on a receiver named after it, walks those supertypes for the method (classStrategyMethod→inheritedClassMethod). WhenBasehas nosave, it foundUser.save, at 0.85 forRepo.save()and 0.8 forrepo.save().What came from the contributor, and what we changed
From #3000, unchanged: the fix in
name-matcher.ts, which drops bracketed type arguments, nested ones too, before reading the names; the new test file; the CHANGELOG text.Ours:
class Mixed(Base[Optional[User]], Mixin), where a type argument came ahead of a real base, and a quoted forward reference,class Forward(Base["Pair[str, User]"]), the shape sqlalchemy uses.Validation
Stage A: every changed line read, scanner clean. The only hits are
RegExp.execand the test's../srcimport.Reproduced on main with a fixture of our own:
Repo.save(),repo.save(),Store.save()andKeyed.save()reachUser::save, andMixed.save()reachesUser::saveinstead ofMixin::save.Tests: red on main's
name-matcher.ts(2 failed, 1 passed), green here (3 passed).A/B against main, same build otherwise, kernel on:
server/@ 8dd9956Controls and the two small Python repos have byte-identical full dumps.
The change is small on real code: 1 edge of 670,226. That edge is
a.references(b)in sqlalchemy'srelationships.py. Its localamatched a test'sclass a(TypedColumns), andclass TypedColumns(ReadOnlyColumnCollection[str, "Column[Any]"])then reachedColumn.referencesthrough the type argument. The target happens to be right, because the code narrowsawithisinstance, but the path is the bug this fixes. Its twinb.references(a)two lines down has no edge in either arm. The call is now unresolved, which is better than a guess that happened to land.Kernel parity: 43/43 on flask and requests in both arms. This doesn't touch the kernel.
Cost: sqlalchemy, median of 3 interleaved runs: 29.5 s on main, 25.7 s here. That gap is noise, so nothing measurable.
Suite: 828 files (822 passed, 6 skipped), 9,721 tests passed, 0 failed, 87 skipped, on this exact commit, kernel on, 2 workers.
Not in this PR
declaredSupertypes, so it conflicts with this PR and gets rebased onto it. fix(python): a parameterized base links its class, like class Leaf(Base[str]) #2998 touchespythonBasesonly and merges clean. This PR depends on neither.Repo.touch()withtouchon only one project class still links at 0.7. That's the existing unique-name fallback, and a plainclass Repo(Base)does the same.🤖 Generated with Claude Code