diff --git a/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs b/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs
index 9ba63a4..005b372 100644
--- a/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs
+++ b/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs
@@ -135,6 +135,67 @@ assert mutable.State
});
}
+ ///
+ /// Tests that a module whose annotations name a class that does not exist yet can be loaded.
+ ///
+ ///
+ /// Python before 3.14 evaluates an annotation as the def or the class body runs, so a
+ /// factory answering its own class, a method taking another instance, a field holding one, and a
+ /// parameter naming a class declared further down are each a NameError on import unless the
+ /// annotations are deferred.
+ ///
+ [TestMethod]
+ public void AnnotationsNamingUndeclaredClasses_Import()
+ {
+ string? python = ToolchainHarness.FindOnPath("--version", "python3", "python");
+ if (python is null)
+ {
+ Assert.Inconclusive("No Python interpreter on the path, so nothing was run.");
+ return;
+ }
+
+ SourceFile file = new("shapes");
+ file.HeaderComment.Add("Generated by Coder. Do not edit.");
+
+ ClassDeclaration point = new("Point");
+ point.Members.Add(new FieldDeclaration("x", "int"));
+
+ FunctionDeclaration zero = new("zero") { ReturnType = "Point", IsStatic = true };
+ zero.Body.Add(new ReturnStatement(new ConstructionExpression("Point")));
+ point.Members.Add(zero);
+
+ FunctionDeclaration distance = new("distance") { ReturnType = "int" };
+ distance.Parameters.Add(new Parameter("other", "Point"));
+ distance.Body.Add(new ReturnStatement(0));
+ point.Members.Add(distance);
+
+ FunctionDeclaration attach = new("attach");
+ attach.Parameters.Add(new Parameter("node", "Node"));
+ point.Members.Add(attach);
+
+ ClassDeclaration node = new("Node");
+ node.Members.Add(new FieldDeclaration("next", "Node"));
+
+ file.Members.Add(point);
+ file.Members.Add(node);
+
+ ToolchainHarness.InTemporaryDirectory(directory =>
+ {
+ File.WriteAllText(Path.Combine(directory, "shapes.py"), new PythonGenerator().Generate(file));
+ File.WriteAllText(
+ Path.Combine(directory, "driver.py"),
+ """
+ import shapes
+
+ assert isinstance(shapes.Point.zero(), shapes.Point)
+ assert shapes.Node.__name__ == "Node"
+ """);
+
+ (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.
///
diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs
index e2cd014..430b772 100644
--- a/Coder/Languages/LanguageGeneratorBase.cs
+++ b/Coder/Languages/LanguageGeneratorBase.cs
@@ -574,8 +574,8 @@ protected void WriteTypePromises(ClassDeclaration classDecl, CodeBlocker code, b
/// The writer to emit into.
/// True if anything was written.
///
- /// Empty for every language but C++, which is the only one here where a file can be included
- /// twice and has to say what that means.
+ /// Empty unless a language has something that must precede everything else: C++'s
+ /// #pragma once, Go's package clause, Python's future statement.
///
protected virtual bool WriteFileDirectives(SourceFile file, CodeBlocker code) => false;
diff --git a/Coder/Languages/PythonGenerator.cs b/Coder/Languages/PythonGenerator.cs
index 8c47963..55462d6 100644
--- a/Coder/Languages/PythonGenerator.cs
+++ b/Coder/Languages/PythonGenerator.cs
@@ -167,6 +167,31 @@ private void WriteAccessorBody(Collection body, CodeBlocker code)
}
}
+ ///
+ ///
+ /// from __future__ import annotations, which defers every annotation in the module. Python
+ /// before 3.14 evaluates an annotation when the def or the class body runs, so a factory
+ /// answering its own class, a method taking another instance, or a parameter naming a class
+ /// declared further down is a name that does not exist yet and the module raises
+ /// NameError on import. Deferring them all is the language's own answer and needs nothing
+ /// to know which names are forward; a base list is an expression rather than an annotation, which
+ /// is why still quotes. A future statement has to come
+ /// before any other, so it is written here rather than as an import.
+ ///
+ protected override bool WriteFileDirectives(SourceFile file, CodeBlocker code)
+ {
+ Ensure.NotNull(file);
+ Ensure.NotNull(code);
+
+ if (file.Members.Count == 0)
+ {
+ return false;
+ }
+
+ code.WriteLine("from __future__ import annotations");
+ return true;
+ }
+
///
protected override string? SpellImport(string import)
{