diff --git a/CLAUDE.md b/CLAUDE.md
index d6d59e1..d165d46 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -139,6 +139,14 @@ source in seven target languages. The solution uses:
whole a pointer to the member — to exactly one of them, and Go satisfies an interface structurally
and so writes `var _ Contract = (*Type)(nil)`, an assertion the compiler checks rather than a
declaration. A generator handed one list would be guessing which entry was the class.
+ Python folds the two back into the one list it has, and is the one target that has to do more than
+ spell them: a base list is an expression evaluated as the `class` statement runs, so a base naming
+ the class being declared — the self-type idiom, an interface written over its own implementer —
+ names something that does not exist yet and raises `NameError` on import. The argument is quoted,
+ which is what `typing` takes as a forward reference and resolves once something asks;
+ `from __future__ import annotations` is the other half of the idea and does not reach a base, which
+ is an expression rather than an annotation. `PythonGeneratedSourceImportsTests` loads what the
+ generator writes, because that is a class of error no test pinning the text can see.
The three modifiers split along a line worth stating once: `IsRecord` and `IsReadOnly` are claims
about the type — it compares by value, no member of it modifies it — so a target with no word for
one writes it down, the same as `CompileTimeAssertion`, while Rust's `#[derive(Clone, Debug,
diff --git a/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs b/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs
new file mode 100644
index 0000000..aa56fbe
--- /dev/null
+++ b/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs
@@ -0,0 +1,122 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Coder.Test.Languages;
+
+using ktsu.Coder.Ast;
+using ktsu.Coder.Languages;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+///
+/// Runs what writes, with a real interpreter.
+///
+///
+/// The other four targets that are checked by a toolchain are checked by compiling. Python has no
+/// compiler, and the equivalent question is whether the module can be imported at all: a Python file
+/// is a program that builds its own declarations, so a class statement runs when the module is loaded
+/// and anything wrong with it is raised then. That is the whole class of error a test pinning the
+/// text cannot see — the spelling looks right and the interpreter disagrees — and a base list naming
+/// the class being declared is exactly one of them.
+///
+/// The driver is written the way a consumer would be: it supplies the names the generated module
+/// expects to find — the interface it implements, and the type variable it is written over — and then
+/// runs the module against them. runpy is how a module is run with names already in its
+/// namespace; an import cannot, and the AST has no way to declare a TypeVar or a
+/// Generic for the file to carry its own.
+///
+///
+/// The test is inconclusive rather than failing where no interpreter is on the path, which is the
+/// honest result: nothing was run.
+///
+///
+[TestClass]
+public class PythonGeneratedSourceImportsTests
+{
+ ///
+ /// The consumer of the generated module, written the way a person would write one.
+ ///
+ ///
+ /// Every declaration the module makes is asked for afterwards, so a module that loads but declares
+ /// nothing fails here rather than passing quietly. Length is the self-type idiom and
+ /// Node the mutually-referential pair, which are the two shapes a base list can hold a name
+ /// that does not exist yet.
+ ///
+ private const string Driver = """
+ import runpy
+ from typing import Generic, TypeVar
+
+ T = TypeVar("T")
+ TSelf = TypeVar("TSelf")
+
+
+ class IVector0(Generic[TSelf, T]):
+ pass
+
+
+ class Visitor(Generic[TSelf]):
+ pass
+
+
+ module = runpy.run_path(
+ "exemplar.py",
+ init_globals={"IVector0": IVector0, "Visitor": Visitor, "T": T},
+ )
+
+ assert module["Length"].__name__ == "Length"
+ assert module["Node"].__name__ == "Node"
+
+ """;
+
+ ///
+ /// Tests that a module whose classes are written over themselves can be loaded.
+ ///
+ [TestMethod]
+ public void GeneratedSource_Imports()
+ {
+ string? python = ToolchainHarness.FindOnPath("--version", "python3", "python");
+ if (python is null)
+ {
+ Assert.Inconclusive("No Python interpreter on the path, so nothing was run.");
+ return;
+ }
+
+ ToolchainHarness.InTemporaryDirectory(directory =>
+ {
+ File.WriteAllText(Path.Combine(directory, "driver.py"), Driver);
+ File.WriteAllText(
+ Path.Combine(directory, "exemplar.py"),
+ new PythonGenerator().Generate(Exemplar()));
+
+ (int exitCode, string output) = ToolchainHarness.Run(python, "driver.py", directory);
+ Assert.AreEqual(
+ 0,
+ exitCode,
+ $"Python could not load the generated module:{Environment.NewLine}{output}");
+ });
+ }
+
+ ///
+ /// Builds a file whose declarations are each written over themselves.
+ ///
+ /// The file to generate.
+ private static SourceFile Exemplar()
+ {
+ SourceFile file = new("exemplar");
+ file.HeaderComment.Add("Generated by Coder. Do not edit.");
+
+ // The shape every generated ktsu.Semantics quantity has: an interface written over the type
+ // implementing it, so that the method it declares answers that type rather than the interface.
+ ClassDeclaration length = new("Length");
+ length.Interfaces.Add(TypeReference.Parse("IVector0, T>"));
+ length.Members.Add(new VariableDeclaration("Value", "T"));
+
+ // The same knot without generics of its own: a type and the thing that visits it, each named
+ // in the other's declaration.
+ ClassDeclaration node = new("Node");
+ node.Interfaces.Add(TypeReference.Parse("Visitor"));
+
+ file.Members.Add(length);
+ file.Members.Add(node);
+
+ return file;
+ }
+}
diff --git a/Coder.Test/Languages/ToolchainHarness.cs b/Coder.Test/Languages/ToolchainHarness.cs
index a9af2d2..48dd26c 100644
--- a/Coder.Test/Languages/ToolchainHarness.cs
+++ b/Coder.Test/Languages/ToolchainHarness.cs
@@ -5,7 +5,7 @@ namespace ktsu.Coder.Test.Languages;
using System.Diagnostics;
///
-/// Runs a real compiler over generated source.
+/// Runs a real toolchain over generated source.
///
///
/// Three of the generators here are checked by compiling what they write, because the rules they
@@ -13,6 +13,11 @@ namespace ktsu.Coder.Test.Languages;
/// about receivers, associated items and what a trait implementation owes its trait, and Go's about
/// unused names, which it refuses rather than warns about. Everything those tests share about
/// *running* a compiler is here, so that what is left in each of them is the language.
+///
+/// Python is asked the same question by an interpreter rather than a compiler, having no compiler to
+/// ask: a Python file builds its own declarations as it is loaded, so a declaration the language
+/// refuses raises on import rather than failing to build.
+///
///
internal static class ToolchainHarness
{
diff --git a/Coder.Test/Languages/TypeDeclarationShapeTests.cs b/Coder.Test/Languages/TypeDeclarationShapeTests.cs
index 24840bd..3e4ac5d 100644
--- a/Coder.Test/Languages/TypeDeclarationShapeTests.cs
+++ b/Coder.Test/Languages/TypeDeclarationShapeTests.cs
@@ -55,6 +55,19 @@ private static ClassDeclaration Money() =>
IsReadOnly = true,
};
+ ///
+ /// A type implementing an interface written over the type itself, which is how an interface gives
+ /// a method the implementing type as its result — TSelf Create(T value) rather than
+ /// IVector0 Create(T value).
+ ///
+ /// The declaration.
+ private static ClassDeclaration SelfTyped()
+ {
+ ClassDeclaration length = new("Length");
+ length.Interfaces.Add(TypeReference.Parse("IVector0, T>"));
+ return length;
+ }
+
private static string Generate(ILanguageGenerator generator, AstNode node) => generator.Generate(node);
// ------------------------------------------------------------------ Interfaces
@@ -138,6 +151,82 @@ public void Python_TakesThemAllAsBases()
StringAssert.Contains(code, "class Widget(Control, Drawable, Clickable):");
}
+ ///
+ /// Python quotes the argument of a base that names the class being declared, because a base list
+ /// is an expression Python evaluates before the name exists.
+ ///
+ ///
+ /// The self-type idiom, which is how an interface hands a method the implementing type — every one
+ /// of the 212 generated quantities in ktsu.Semantics is declared this way. Unquoted, the
+ /// module raises NameError on import rather than misbehaving later, so this is the
+ /// difference between a file that can be loaded and one that cannot.
+ ///
+ [TestMethod]
+ public void Python_QuotesABaseArgumentNamingTheClassBeingDeclared()
+ {
+ string code = Generate(new PythonGenerator(), SelfTyped());
+
+ Assert.Contains("class Length(IVector0[\"Length[T]\", T]):", code);
+ }
+
+ ///
+ /// The same for a pair that refer to each other, which needs no generics of its own.
+ ///
+ [TestMethod]
+ public void Python_QuotesABaseArgumentThatIsTheClassItself()
+ {
+ ClassDeclaration node = new("Node");
+ node.Interfaces.Add(TypeReference.Parse("Visitor"));
+
+ string code = Generate(new PythonGenerator(), node);
+
+ Assert.Contains("class Node(Visitor[\"Node\"]):", code);
+ }
+
+ ///
+ /// An argument holding the class further down is quoted whole, which is the one place the quote
+ /// can go: a second pair inside the first would end the string rather than nest.
+ ///
+ [TestMethod]
+ public void Python_QuotesTheWholeArgumentWhenTheClassIsNestedInIt()
+ {
+ ClassDeclaration length = new("Length");
+ length.Interfaces.Add(TypeReference.Parse("IVector0>, T>"));
+
+ string code = Generate(new PythonGenerator(), length);
+
+ Assert.Contains("class Length(IVector0[\"Wrapper[Length[T]]\", T]):", code);
+ }
+
+ ///
+ /// An argument naming anything else is left alone: a forward reference is what a name that does
+ /// not exist yet needs, and every other name is one Python can already resolve.
+ ///
+ [TestMethod]
+ public void Python_LeavesABaseArgumentNamingSomethingElseUnquoted()
+ {
+ ClassDeclaration button = new("Button");
+ button.Interfaces.Add(TypeReference.Parse("Handler"));
+
+ string code = Generate(new PythonGenerator(), button);
+
+ Assert.Contains("class Button(Handler[Event]):", code);
+ Assert.DoesNotContain("\"", code, "Python quoted a base that names nothing being declared.");
+ }
+
+ ///
+ /// The other targets write the same declaration verbatim, because the declaration is not what is
+ /// wrong with it: Python is the one target that reads a base list eagerly.
+ ///
+ [TestMethod]
+ public void OtherTargets_WriteASelfTypedInterfaceVerbatim()
+ {
+ Assert.Contains(
+ "class Length : IVector0, T>", Generate(new CSharpGenerator(), SelfTyped()));
+ Assert.Contains(
+ "class Length : public IVector0, T>", Generate(new CppGenerator(), SelfTyped()));
+ }
+
///
/// JavaScript extends one thing and has no interfaces, so it writes down what it cannot say.
///
diff --git a/Coder/Languages/PythonGenerator.cs b/Coder/Languages/PythonGenerator.cs
index 74d505e..2e60f6e 100644
--- a/Coder/Languages/PythonGenerator.cs
+++ b/Coder/Languages/PythonGenerator.cs
@@ -408,10 +408,11 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod
// Python inherits from as many things as it is given and has no separate notion of an
// interface, so the base and the interfaces are one list of bases -- which is what the
// abstract base classes in the standard library already are.
+ string? declaring = classDecl.Name;
string[] bases =
[
- .. classDecl.BaseType is TypeReference baseType ? (string[])[PythonTypeFromGenericType(baseType)] : [],
- .. classDecl.Interfaces.Select(PythonTypeFromGenericType),
+ .. classDecl.BaseType is TypeReference baseType ? (string[])[PythonBaseFromGenericType(baseType, declaring)] : [],
+ .. classDecl.Interfaces.Select(implemented => PythonBaseFromGenericType(implemented, declaring)),
];
if (bases.Length > 0)
@@ -678,16 +679,7 @@ protected override void GenerateParameter(Parameter parameter, CodeBlocker code,
///
private static string PythonTypeFromGenericType(TypeReference type)
{
- string name = type.Name.ToLowerInvariant() switch
- {
- "int" => "int",
- "string" => "str",
- "bool" => "bool",
- "float" => "float",
- "double" => "float",
- "void" => "None",
- _ => type.Name
- };
+ string name = PythonTypeName(type);
string spelled = type.TypeArguments.Count == 0
? name
@@ -698,6 +690,88 @@ private static string PythonTypeFromGenericType(TypeReference type)
return type.IsArray ? $"list[{spelled}]" : spelled;
}
+ ///
+ /// Maps a type's name to the Python spelling of it.
+ ///
+ /// The type whose name to map.
+ /// The name Python knows it by.
+ private static string PythonTypeName(TypeReference type) => type.Name.ToLowerInvariant() switch
+ {
+ "int" => "int",
+ "string" => "str",
+ "bool" => "bool",
+ "float" => "float",
+ "double" => "float",
+ "void" => "None",
+ _ => type.Name
+ };
+
+ ///
+ /// Spells a base of a class, quoting any argument that names the class being declared.
+ ///
+ /// The base to spell.
+ /// The name of the class the base list belongs to.
+ /// The Python source for it.
+ ///
+ /// A base list is an expression and Python evaluates it as the class statement is run, so a
+ /// base naming the class being declared is a name that does not exist yet: the self-type idiom —
+ /// IVector0<TSelf, T>, the interface that hands a method the implementing type — makes
+ /// the module raise NameError on import rather than misbehave later. Nothing about the
+ /// declaration is wrong; Python is the one target that reads a base eagerly.
+ ///
+ /// A quoted argument is the language's own answer, and the one typing consumers write for
+ /// exactly this case: typing takes a string as a forward reference and resolves it when
+ /// something asks, by which time the class exists. 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.
+ ///
+ ///
+ /// 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, and quoting is at the
+ /// outermost argument alone for the same reason: one quoted argument carries every name inside it,
+ /// and a second pair of quotes within the first would end the string rather than nest.
+ ///
+ ///
+ private static string PythonBaseFromGenericType(TypeReference type, string? declaring)
+ {
+ if (declaring is null || type.TypeArguments.Count == 0 || !NamesType(type.TypeArguments, declaring))
+ {
+ return PythonTypeFromGenericType(type);
+ }
+
+ IEnumerable arguments = type.TypeArguments.Select(argument =>
+ NamesType(argument, declaring)
+ ? $"\"{PythonTypeFromGenericType(argument)}\""
+ : PythonTypeFromGenericType(argument));
+
+ string spelled = $"{PythonTypeName(type)}[{string.Join(", ", arguments)}]";
+
+ return type.IsArray ? $"list[{spelled}]" : spelled;
+ }
+
+ ///
+ /// Asks whether a type names another anywhere in it.
+ ///
+ /// The type to look through.
+ /// The name being looked for.
+ /// True when the name is the type's own or any of its arguments'.
+ ///
+ /// Through the arguments rather than at the top alone, because a base is as readily written over a
+ /// type holding the class — IVector0<Wrapper<Length<T>>, T> — as over the
+ /// class itself, and Python evaluates all of it at once either way.
+ ///
+ private static bool NamesType(TypeReference type, string name) =>
+ string.Equals(type.Name, name, StringComparison.Ordinal) || NamesType(type.TypeArguments, name);
+
+ ///
+ /// Asks whether any of several types names another anywhere in it.
+ ///
+ /// The types to look through.
+ /// The name being looked for.
+ /// True when any of them names it.
+ private static bool NamesType(IEnumerable types, string name) =>
+ types.Any(type => NamesType(type, name));
+
///
///
/// A constant is emitted as an ordinary assignment: Python has no constant declaration, and the