Skip to content

Quote a Python base that names the class being declared [patch] - #67

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/nice-davinci-0qaook
Sep 14, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/nice-davinci-0qaook

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #64

What was wrong

PythonGenerator wrote a class's bases verbatim. Python evaluates a base list as the class statement runs, so a base written over the class being declared names something that does not exist yet:

class Length(IVector0[Length[T], T]):
    Value = None
NameError: name 'Length' is not defined

The module cannot be imported at all. This is the self-type idiom — the interface that hands a method the implementing type, TSelf Create(T value) rather than IVector0 Create(T value) — which is how every one of the 212 generated ktsu.Semantics quantities is declared, and it needs no generics to reach: class Node(Visitor[Node]) is the same knot.

The fix

The declaration is not what is wrong. The AST says the class implements an interface named over itself, six targets write that correctly, and Python is the one that reads a base eagerly. So the fix is Python's:

class Length(IVector0["Length[T]", T]):

A string is what typing takes as a forward reference and resolves once something asks, by which time the class exists — the spelling a typing.Generic consumer writes for exactly this case. (from __future__ import annotations is the other half of the idea and does not reach here: it defers annotations, and a base is an expression.)

Two limits, both load-bearing:

  • Only the arguments are quoted, never the base itself. A class may inherit from a subscripted generic holding a forward reference and may not inherit from a string.
  • Only at the outermost argument. One quoted argument already carries every name inside it, and a second pair of quotes within the first would end the string rather than nest — so IVector0<Wrapper<Length<T>>, T> comes out as IVector0["Wrapper[Length[T]]", T]. The search for the name is through the arguments rather than at the top alone, since Python evaluates all of it at once either way.

Everything else about the base list is untouched: an argument naming anything else is left exactly as it was, which Python_LeavesABaseArgumentNamingSomethingElseUnquoted pins.

Tests

The issue's other half is that nothing ran what this generator wrote — there was no Python equivalent of the four compile tests, and no Python test with a self-referential interface at all.

PythonGeneratedSourceImportsTests closes that. Python has no compiler and the equivalent question is whether the module loads: a Python file builds its own declarations as it is read, so a class statement the language refuses raises then. The driver is written the way a consumer would be — it supplies the interface and the type variable the generated module expects to find, and runs the module against them through runpy, which is how a module is run with names already in its namespace. It asks for both declarations afterwards, so a module that loads but declares nothing fails rather than passing quietly. Inconclusive rather than failing where no interpreter is on the path, matching the existing toolchain tests.

Verified by reverting the generator change and re-running:

failed GeneratedSource_Imports
  NameError: name 'Length' is not defined
failed Python_QuotesABaseArgumentNamingTheClassBeingDeclared
failed Python_QuotesABaseArgumentThatIsTheClassItself

Python_QuotesTheWholeArgumentWhenTheClassIsNestedInIt was added after that run and fails on a revert for the same reason; the three above are what the revert actually printed.

With the fix: 857/857 passing, and dotnet build -c Release over the solution is clean — 0 warnings, 0 errors.

CLAUDE.md gains the decision, next to what the other targets do with Interfaces, and ToolchainHarness says why Python is asked by an interpreter rather than a compiler.

The second commit takes the assertion form Sonar named for the six new assertions (Assert.Contains rather than StringAssert.Contains, which is what this file already uses for the negative it asserts); the pre-existing calls are left alone.

Not in this change

#63 is the Go half of the same survey and is a different bug in a different generator; it is left alone.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MSQCijNKMyTWktjbys6Yu3

…[patch]

Python evaluates a base list as the class statement runs, so an interface
written over its own implementer -- IVector0<TSelf, T>, which is how every
generated ktsu.Semantics quantity is declared -- names something that does not
exist yet and raises NameError on import. The module cannot be loaded at all.

The declaration is not what is wrong: six targets write it correctly, and
Python is the one that reads a base eagerly. The argument is now quoted, which
is what typing takes as a forward reference and resolves once something asks,
and is what a typing.Generic consumer writes for this case. Only the arguments
are quoted, and only at the outermost level: a class may inherit from a
subscripted generic holding a forward reference and may not inherit from a
string, and one quoted argument already carries every name inside it.

PythonGeneratedSourceImportsTests runs what the generator writes through a real
interpreter, which is the Python equivalent of the four compile tests and the
only way to catch the class of error where the text looks right and the
interpreter disagrees. Reverting the fix fails it with the NameError above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSQCijNKMyTWktjbys6Yu3
MSTEST0046 on each of the six new assertions: Assert.Contains rather than
StringAssert.Contains, which is the form this file already uses for the
negative it asserts. The pre-existing StringAssert calls are left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSQCijNKMyTWktjbys6Yu3
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 53eb2c0 into main Sep 14, 2026
13 checks passed
@matt-edmondson
matt-edmondson deleted the claude/nice-davinci-0qaook branch September 14, 2026 10:05
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.

Python writes a base list naming the class being declared, which raises NameError on import

2 participants