Skip to content

Python output raises NameError on import when a method or field annotation names its own class (or a class declared later in the file) #102

Description

@matt-edmondson

What's wrong

PythonGenerator writes parameter, return and class-attribute annotations with the bare type name (PythonTypeFromGenericType, Coder/Languages/PythonGenerator.cs:677). The call sites are GenerateFieldDeclaration at :310, the parameter emitter near :661 and the return hint in the function and method signatures. Python before 3.14 evaluates these annotations eagerly: when the def statement runs, and when a class-body variable annotation runs. Suppose an annotation names the class whose body is still executing, or a class declared further down the file. That name does not exist yet, so the module fails to import.

This is the same failure #64 fixed for base lists. PythonBaseFromGenericType (:732) quotes a base argument that names the declaring class. The fix did not reach annotations, and the remarks at :720 say so explicitly. They point to from __future__ import annotations as "the other half of the idea", but the generator never emits it. SpellImport (:169) only produces import X, so a caller cannot add it through SourceFile.Imports either.

The shape is extremely common. Examples: a factory returning its own type (the Point.zero() in CompiledExemplar.Point()), a method taking another instance (distance(self, other: Point), any operator-like method), and a linked node (next: Node).

Reproduction

var point = new ClassDeclaration("Point");
point.Members.Add(new FieldDeclaration("x", "int"));
var zero = new FunctionDeclaration("zero") { ReturnType = "Point", IsStatic = true };
zero.Body.Add(new ReturnStatement(new ConstructionExpression(new TypeReference("Point"))));
point.Members.Add(zero);
var dist = new FunctionDeclaration("distance") { ReturnType = "int" };
dist.Parameters.Add(new Parameter("other", "Point"));
dist.Body.Add(new ReturnStatement(new LiteralExpression<int>(0)));
point.Members.Add(dist);
var node = new ClassDeclaration("Node");
node.Members.Add(new FieldDeclaration("next", "Node"));
Console.WriteLine(new PythonGenerator().Generate(new SourceFile("shapes") { Members = { point, node } }));

Generated:

class Point:
    x: int
    
    @staticmethod
    def zero() -> Point:
        return Point()
    
    def distance(self, other: Point) -> int:
        return 0
    

class Node:
    next: Node

Actual result under Python 3.11 (python3 -c "import shapes"):

  File "shapes.py", line 5, in Point
    def zero() -> Point:
                  ^^^^^
NameError: name 'Point' is not defined. Did you mean: 'print'?

Generating Node on its own fails the same way: next: Node gives NameError: name 'Node' is not defined.

Expected: the module imports. I prepended from __future__ import annotations to the same output, and it imported and ran (fixed.Point.zero() returned a Point).

Why it matters

Any generated Python class that mentions its own type in a signature or field cannot be imported at all. PythonGeneratedSourceImportsTests exists to catch exactly this class of error, but its exemplar has no annotation naming the class, so the test passes.

Suggested fix / acceptance criteria

  • Pick one of these two fixes:
    • Emit from __future__ import annotations as the first statement of a generated Python SourceFile. For example, override WriteFileDirectives, which already exists for C++ and Go. It must come before any import; the header comment can stay above it.
    • Quote any annotation that names the enclosing class or a class declared later, the same way PythonBaseFromGenericType quotes base arguments. This also covers a class generated on its own, outside a SourceFile.
  • Extend PythonGeneratedSourceImportsTests so its exemplar includes the following, and the module still loads:
    • a static method returning the declaring class
    • a parameter typed as the declaring class
    • a field typed as the declaring class
    • a parameter typed as a class declared later in the file

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingreadyFully specified; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions