Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,67 @@ assert mutable.State
});
}

/// <summary>
/// Tests that a module whose annotations name a class that does not exist yet can be loaded.
/// </summary>
/// <remarks>
/// Python before 3.14 evaluates an annotation as the <c>def</c> 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 <c>NameError</c> on import unless the
/// annotations are deferred.
/// </remarks>
[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}");
});
}

/// <summary>
/// Builds a file whose declarations are each written over themselves.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions Coder/Languages/LanguageGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@
}
}

foreach (object item in expected)

Check warning on line 372 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 372 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (!emittedPreambleItems!.Contains(item))
{
Expand All @@ -387,7 +387,7 @@
/// Read off the nodes' public properties rather than a list of node types, so a node added to the
/// AST is walked without anybody remembering to add it here.
/// </remarks>
protected static IEnumerable<AstNode> SelfAndDescendants(AstNode root)

Check warning on line 390 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.

Check warning on line 390 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.
{
HashSet<AstNode> visited = new(ReferenceEqualityComparer.Instance);
Stack<AstNode> pending = new();
Expand Down Expand Up @@ -574,8 +574,8 @@
/// <param name="code">The writer to emit into.</param>
/// <returns>True if anything was written.</returns>
/// <remarks>
/// 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
/// <c>#pragma once</c>, Go's package clause, Python's future statement.
/// </remarks>
protected virtual bool WriteFileDirectives(SourceFile file, CodeBlocker code) => false;

Expand Down Expand Up @@ -1128,7 +1128,7 @@
/// the generator adds through <see cref="RequiredImports"/>.
/// </remarks>
protected virtual string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity";

Check warning on line 1131 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 1131 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <summary>
/// Formats a single-precision value so that every target reads it as floating-point.
Expand Down Expand Up @@ -1193,7 +1193,7 @@
string? origin = null;
long offset = 0;

foreach (EnumMember member in enumDecl.Members)

Check warning on line 1196 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)

Check warning on line 1196 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)
{
if (member.Value is not null)
{
Expand Down
25 changes: 25 additions & 0 deletions Coder/Languages/PythonGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,31 @@
}
}

/// <inheritdoc/>
/// <remarks>
/// <c>from __future__ import annotations</c>, which defers every annotation in the module. Python
/// before 3.14 evaluates an annotation when the <c>def</c> 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
/// <c>NameError</c> 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 <see cref="PythonBaseFromGenericType"/> still quotes. A future statement has to come
/// before any other, so it is written here rather than as an import.
/// </remarks>
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;
}

/// <inheritdoc/>
protected override string? SpellImport(string import)
{
Expand All @@ -182,7 +207,7 @@
/// <c>float("nan")</c> does not.
/// </remarks>
protected override string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "float(\"nan\")" : value > 0 ? "float(\"inf\")" : "float(\"-inf\")";

Check warning on line 210 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 210 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
protected override IEnumerable<string> RequiredImports(SourceFile file)
Expand Down
Loading