Repository: tefra/xsdata

Issue:
Forward referencing when generating a recursive schema
If generating a recursively defined schema with postponed annotations and compound fields both enabled, the library does not correctly forward reference the recursive types, leading to generated code that cannot be used or imported.

Consider this schema where two types reference each other.

```xml
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:complexType name="type1">
        <xs:sequence>
            <xs:choice>
                <xs:element name="str" type="xs:string"/>
                <xs:element name="data" type="type2"/>
            </xs:choice>
        </xs:sequence>
    </xs:complexType>

    <xs:complexType name="type2">
        <xs:sequence>
            <xs:choice>
                <xs:element name="str" type="xs:string"/>
                <xs:element name="data" type="type1"/>
            </xs:choice>
        </xs:sequence>
    </xs:complexType>
</xs:schema>
```

If generating this schema with `xsdata generate schema.xsd --compound-fields --postponed-annotations`, then the following dataclass is produced:

```python
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional, Union


@dataclass
class Type2:
    class Meta:
        name = "type2"

    str_or_data: Optional[Union[str, Type1]] = field(
        default=None,
        metadata={
            "type": "Elements",
            "choices": (
                {
                    "name": "str",
                    "type": str,
                    "namespace": "",
                },
                {
                    "name": "data",
                    "type": Type1,
                    "namespace": "",
                },
            ),
        }
    )


@dataclass
class Type1:
    class Meta:
        name = "type1"

    str_or_data: Optional[Union[str, Type2]] = field(
        default=None,
        metadata={
            "type": "Elements",
            "choices": (
                {
                    "name": "str",
                    "type": str,
                    "namespace": "",
                },
                {
                    "name": "data",
                    "type": Type2,
                    "namespace": "",
                },
            ),
        }
    )
```

The reference to `Type1` in the `choices` dict of `Type2`'s metadata is invalid, as it is a forward reference that is yet to be evaluated by the Python interpreter. This leads to invalid Python code being generated and the dataclasses are unable to be imported:

```bash
>>> import generated
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/test/generated/__init__.py", line 1, in <module>
    from generated.schema import (
  File "/test/generated/schema.py", line 7, in <module>
    class Type2:
  File "/test/generated/schema.py", line 23, in Type2
    "type": Type1,
NameError: name 'Type1' is not defined
```

Discussion:

Patch:
diff --git a/xsdata/formats/dataclass/filters.py b/xsdata/formats/dataclass/filters.py
index 926d871d..f3cc4d15 100644
--- a/xsdata/formats/dataclass/filters.py
+++ b/xsdata/formats/dataclass/filters.py
@@ -720,7 +720,7 @@ class Filters:
         is also ignored.
         """
         type_names = collections.unique_sequence(
-            self.field_type_name(x, parents) for x in choice.types
+            self.field_type_name(x, parents, choice=True) for x in choice.types
         )
 
         if self.union_type:
@@ -742,7 +742,9 @@ class Filters:
 
         return f"Type[{result}]"
 
-    def field_type_name(self, attr_type: AttrType, parents: List[str]) -> str:
+    def field_type_name(
+        self, attr_type: AttrType, parents: List[str], choice: bool = False
+    ) -> str:
         name = self.type_name(attr_type)
 
         if attr_type.forward and attr_type.circular:
@@ -754,7 +756,7 @@ class Filters:
         elif attr_type.circular:
             name = f'"{name}"'
 
-        if self.postponed_annotations:
+        if self.postponed_annotations and not choice:
             name = name.strip('"')
 
         return name


Test patch:
diff --git a/tests/formats/dataclass/test_filters.py b/tests/formats/dataclass/test_filters.py
index 449611cb..3800b2b7 100644
--- a/tests/formats/dataclass/test_filters.py
+++ b/tests/formats/dataclass/test_filters.py
@@ -741,7 +741,7 @@ class FiltersTests(FactoryTestCase):
 
         self.filters.postponed_annotations = True
         actual = self.filters.choice_type(choice, ["a", "b"])
-        self.assertEqual("Type[Foobar]", actual)
+        self.assertEqual('Type["Foobar"]', actual)
 
     def test_choice_type_with_multiple_types(self):
         choice = AttrFactory.create(types=[type_str, type_bool])