Skip to content

Commit 95e5143

Browse files
Fix TypeAlias with circular reference to class generates NameError (#2591)
* Fix type alias circular reference handling for classes * Refactor type alias circular reference handling in __update_type_aliases method * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent b84474f commit 95e5143

4 files changed

Lines changed: 84 additions & 10 deletions

File tree

src/datamodel_code_generator/parser/base.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,31 +1454,30 @@ def __set_one_literal_on_default(self, models: list[DataModel]) -> None:
14541454
@classmethod
14551455
def __update_type_aliases(cls, models: list[DataModel]) -> None:
14561456
"""Update type aliases to properly handle forward references per PEP 484."""
1457-
rendered_aliases: set[str] = set()
1457+
model_index: dict[str, int] = {m.class_name: i for i, m in enumerate(models)}
14581458

1459-
for model in models:
1459+
for i, model in enumerate(models):
14601460
if not isinstance(model, TypeAliasBase):
14611461
continue
1462-
14631462
if isinstance(model, TypeStatement):
1464-
rendered_aliases.add(model.class_name)
14651463
continue
14661464

14671465
for field in model.fields:
14681466
for data_type in field.data_type.all_data_types:
14691467
if not data_type.reference:
14701468
continue
14711469
source = data_type.reference.source
1472-
if not isinstance(source, TypeAliasBase):
1473-
continue
1474-
if isinstance(source, TypeStatement): # pragma: no cover
1470+
if not isinstance(source, DataModel):
1471+
continue # pragma: no cover
1472+
if isinstance(source, TypeStatement):
1473+
continue # pragma: no cover
1474+
if source.module_path != model.module_path:
14751475
continue
14761476
name = data_type.reference.short_name
1477-
if name not in rendered_aliases:
1477+
source_index = model_index.get(name)
1478+
if source_index is not None and source_index >= i:
14781479
data_type.alias = f'"{name}"'
14791480

1480-
rendered_aliases.add(model.class_name)
1481-
14821481
@classmethod
14831482
def __postprocess_result_modules(cls, results: dict[tuple[str, ...], Result]) -> dict[tuple[str, ...], Result]:
14841483
def process(input_tuple: tuple[str, ...]) -> tuple[str, ...]:
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# generated by datamodel-codegen:
2+
# filename: type_alias_with_circular_ref_to_class.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import List, Union
8+
9+
from msgspec import Struct
10+
from typing_extensions import TypeAlias
11+
12+
13+
class Defaults(Struct):
14+
a: List[Span]
15+
16+
17+
class SpanB(Struct):
18+
recur: List[Span]
19+
20+
21+
Either: TypeAlias = Union[SpanB, "Span"]
22+
23+
24+
class Span(Struct):
25+
recur: List[Either]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$defs": {
4+
"SpanB": {
5+
"type": "object",
6+
"properties": {
7+
"recur": { "type": "array", "items": [{ "$ref": "#/$defs/Span" }] }
8+
},
9+
"required": ["recur"]
10+
},
11+
"Either": { "oneOf": [{ "$ref": "#/$defs/SpanB" }, { "$ref": "#/$defs/Span" }] },
12+
"Span": {
13+
"type": "object",
14+
"properties": {
15+
"recur": {
16+
"type": "array",
17+
"items": [{ "$ref": "#/$defs/Either" }]
18+
}
19+
},
20+
"required": ["recur"]
21+
}
22+
},
23+
"title": "Defaults",
24+
"type": "object",
25+
"properties": {
26+
"a": {
27+
"type": "array",
28+
"items": [{ "$ref": "#/$defs/Span" }]
29+
}
30+
},
31+
"required": ["a"]
32+
}

tests/main/jsonschema/test_main_jsonschema.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3000,6 +3000,24 @@ def test_main_jsonschema_empty_items_array(output_file: Path) -> None:
30003000
)
30013001

30023002

3003+
@MSGSPEC_LEGACY_BLACK_SKIP
3004+
def test_main_jsonschema_type_alias_with_circular_ref_to_class_msgspec(min_version: str, output_file: Path) -> None:
3005+
"""Test TypeAlias with circular reference to class generates quoted forward refs."""
3006+
run_main_and_assert(
3007+
input_path=JSON_SCHEMA_DATA_PATH / "type_alias_with_circular_ref_to_class.json",
3008+
output_path=output_file,
3009+
input_file_type="jsonschema",
3010+
assert_func=assert_file_content,
3011+
expected_file="type_alias_with_circular_ref_to_class_msgspec.py",
3012+
extra_args=[
3013+
"--output-model-type",
3014+
"msgspec.Struct",
3015+
"--target-python-version",
3016+
min_version,
3017+
],
3018+
)
3019+
3020+
30033021
def test_main_jsonschema_enum_object_values(output_file: Path) -> None:
30043022
"""Test that enum with object values uses title/name/const for member names (issue #1620)."""
30053023
run_main_and_assert(

0 commit comments

Comments
 (0)