Skip to content

fix(python): a base's type argument is no supertype, so Repo.save() is not User.save - #3000

Closed
JosefAschauer wants to merge 1 commit into
colbymchenry:mainfrom
JosefAschauer:fix/python-supertype-type-args
Closed

JosefAschauer wants to merge 1 commit into
colbymchenry:mainfrom
JosefAschauer:fix/python-supertype-type-args

Conversation

@JosefAschauer

Copy link
Copy Markdown
Contributor

Problem

declaredSupertypes' Python branch reads every word of a class's base list, so class Repo(Base[User]) declares both Base and User. A call on the class, or on a receiver named after it, walks those supertypes for the method (classStrategyMethod → inheritedClassMethod). When Base declares no save, it finds User.save:

Call Before
Repo.save() User::save (qualified-name, 0.85)
repo.save() User::save (instance-method, 0.8)

User is only a type argument, so both edges are confidently wrong. Found while working on #2998.

Fix

The Python branch drops bracketed type arguments, nested ones too, before reading the names. Base is still read and searched. Other languages are untouched.

Validation

  • New __tests__/python-supertype-type-arguments.test.ts:

    • Repo.save(), repo.save(), a nested Pair[str, dict[str, User]] and a metaclass= head no longer reach User::save (a decoy Other.save exists too);
    • Repo.load() / Store.load() still reach Base::load / Pair::load.

    Mutation-checked: with the stripping disabled, the first test fails.

  • npx tsc --noEmit: clean.

  • Full suite on Linux: 8762 passed, 4 failed. The failures are environmental and also fail on main:

    • bundle-launcher ×2: no zip binary.
    • cli-sync ×2: Node 26 prints the unsupported-Node banner.
  • Independent review (Grok): SHIP.

Not addressed

  • The head window is the class line plus two, so a base list wrapped over more lines is still read only in part. That limit predates this change.

Independent of #2076, #2998 and #2999.

🤖 Generated with Claude Code

https://claude.ai/code/session_018hJjBUPFiLSFBssshFwHzq

…s not User.save

declaredSupertypes' Python branch took every word of the base list, so
`class Repo(Base[User])` declared `Base` and `User`. A call on the class or
on a receiver named after it (classStrategyMethod -> inheritedClassMethod),
`Repo.save()` / `repo.save()`, then reached `User.save` at 0.85 / 0.8 when
`Base` declares no `save`. Bracketed type arguments, nested too, are now
dropped before the names are read.

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
…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

Copy link
Copy Markdown
Owner

Thanks @JosefAschauer! This landed as #3083 with you as the commit author, and your fix went in unchanged. We added two test cases, a type argument ahead of a real base and a quoted forward reference, and moved the CHANGELOG bullet next to 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