Skip to content

Commit 42d6852

Browse files
authored
Fix recursive TypeAlias generation for Python 3.11 (covers mutual alias refs) (#2570)
* fix: enhance recursive type alias handling for Python 3.11 * fix: improve handling of recursive type aliases and render order * fix: skip tests for mutual recursive type aliases on unsupported black versions * fix: enhance type alias handling for recursive and cross-module scenarios * fix: simplify type hint rendering and improve type alias handling * fix: remove redundant test for TypeAlias with no fields * fix: refine type alias handling in recursive type resolution * fix: update type alias definitions for improved compatibility with Python 3.11 * fix: update type alias handling to properly manage forward references per PEP 484
1 parent 000b177 commit 42d6852

13 files changed

Lines changed: 320 additions & 2 deletions

src/datamodel_code_generator/model/type_alias.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ class TypeAliasTypeBackport(TypeAliasBase):
5959

6060

6161
class TypeStatement(TypeAliasBase):
62-
"""Type statement for Python 3.12+ (type Name = type)."""
62+
"""Type statement for Python 3.12+ (type Name = type).
63+
64+
Note: Python 3.12+ type statements use deferred evaluation,
65+
so forward references don't need to be quoted.
66+
"""
6367

6468
TEMPLATE_FILE_PATH: ClassVar[str] = "TypeStatement.jinja2"
6569
BASE_CLASS: ClassVar[str] = ""

src/datamodel_code_generator/parser/base.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
DataModelFieldBase,
4848
)
4949
from datamodel_code_generator.model.enum import Enum, Member
50-
from datamodel_code_generator.model.type_alias import TypeAliasBase
50+
from datamodel_code_generator.model.type_alias import TypeAliasBase, TypeStatement
5151
from datamodel_code_generator.parser import DefaultPutDict, LiteralType
5252
from datamodel_code_generator.reference import ModelResolver, Reference
5353
from datamodel_code_generator.types import DataType, DataTypeManager, StrictTypes
@@ -1298,6 +1298,34 @@ def __set_one_literal_on_default(self, models: list[DataModel]) -> None:
12981298
if model_field.nullable is not True: # pragma: no cover
12991299
model_field.nullable = False
13001300

1301+
@classmethod
1302+
def __update_type_aliases(cls, models: list[DataModel]) -> None:
1303+
"""Update type aliases to properly handle forward references per PEP 484."""
1304+
rendered_aliases: set[str] = set()
1305+
1306+
for model in models:
1307+
if not isinstance(model, TypeAliasBase):
1308+
continue
1309+
1310+
if isinstance(model, TypeStatement):
1311+
rendered_aliases.add(model.class_name)
1312+
continue
1313+
1314+
for field in model.fields:
1315+
for data_type in field.data_type.all_data_types:
1316+
if not data_type.reference:
1317+
continue
1318+
source = data_type.reference.source
1319+
if not isinstance(source, TypeAliasBase):
1320+
continue
1321+
if isinstance(source, TypeStatement): # pragma: no cover
1322+
continue
1323+
name = data_type.reference.short_name
1324+
if name not in rendered_aliases:
1325+
data_type.alias = f'"{name}"'
1326+
1327+
rendered_aliases.add(model.class_name)
1328+
13011329
@classmethod
13021330
def __postprocess_result_modules(cls, results: dict[tuple[str, ...], Result]) -> dict[tuple[str, ...], Result]:
13031331
def process(input_tuple: tuple[str, ...]) -> tuple[str, ...]:
@@ -1547,6 +1575,7 @@ class Processed(NamedTuple):
15471575
if with_import:
15481576
result += [str(self.imports), str(imports), "\n"]
15491577

1578+
self.__update_type_aliases(models)
15501579
code = dump_templates(models)
15511580
result += [code]
15521581

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# generated by datamodel-codegen:
2+
# filename: type_alias_mutual_recursive.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import TypeAlias, Union
8+
9+
NodeA: TypeAlias = Union[int, "NodeB"]
10+
11+
12+
NodeB: TypeAlias = Union[str, NodeA]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# generated by datamodel-codegen:
2+
# filename: a.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import TypeAlias
8+
9+
Item: TypeAlias = str
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# generated by datamodel-codegen:
2+
# filename: b.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import List, TypeAlias
8+
9+
Item: TypeAlias = List["Item"]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# generated by datamodel-codegen:
2+
# filename: type_alias_forward_ref_multiple.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional, TypeAlias, Union
8+
9+
from pydantic import BaseModel
10+
11+
12+
class RegularModel(BaseModel):
13+
name: Optional[str] = None
14+
15+
16+
Third: TypeAlias = str
17+
18+
19+
Second: TypeAlias = Union["First", Third, RegularModel]
20+
21+
22+
First: TypeAlias = Second
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# generated by datamodel-codegen:
2+
# filename: type_alias_mutual_recursive.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import TypeAlias, Union
8+
9+
NodeA: TypeAlias = Union[int, "NodeB"]
10+
11+
12+
NodeB: TypeAlias = Union[str, NodeA]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# generated by datamodel-codegen:
2+
# filename: type_alias_recursive.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Dict, List, Optional, TypeAlias, Union
8+
9+
from pydantic import BaseModel
10+
11+
12+
class File(BaseModel):
13+
path: str
14+
15+
16+
class Folder(BaseModel):
17+
address: Optional[str] = None
18+
files: List[File]
19+
subfolders: Optional[List[Folder]] = None
20+
21+
22+
ElementaryType: TypeAlias = Optional[Union[bool, str, int, float]]
23+
24+
25+
JsonType: TypeAlias = Union[ElementaryType, List["JsonType"], Dict[str, "JsonType"]]
26+
27+
28+
class Space(BaseModel):
29+
label: Optional[str] = None
30+
data: Optional[JsonType] = None
31+
dual: Optional[DualSpace] = None
32+
33+
34+
class DualSpace(BaseModel):
35+
label: Optional[str] = None
36+
data: Optional[JsonType] = None
37+
predual: Optional[Space] = None
38+
39+
40+
Folder.update_forward_refs()
41+
Space.update_forward_refs()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
openapi: "3.0.0"
2+
info:
3+
version: 1.0.0
4+
title: Module A
5+
components:
6+
schemas:
7+
Item:
8+
type: string
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
openapi: "3.0.0"
2+
info:
3+
version: 1.0.0
4+
title: Module B
5+
components:
6+
schemas:
7+
Item:
8+
oneOf:
9+
- type: array
10+
items:
11+
$ref: "#/components/schemas/Item"

0 commit comments

Comments
 (0)