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) {