Skip to content

Commit 95cd6d5

Browse files
authored
Fix nested type imports in x-python-type override (#2842)
* Fix nested type imports in x-python-type override * Add test for nested unknown type to cover partial branch
1 parent 0b07112 commit 95cd6d5

6 files changed

Lines changed: 88 additions & 1 deletion

File tree

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import enum as _enum
1010
import json
11+
import re
1112
from collections import defaultdict
1213
from collections.abc import Iterable
1314
from contextlib import contextmanager, suppress
@@ -1334,6 +1335,13 @@ def _is_compatible_python_type(self, schema_type: str | None, python_type: str)
13341335
compatible = self.COMPATIBLE_PYTHON_TYPES.get(schema_type, frozenset())
13351336
return base_type in compatible
13361337

1338+
def _extract_all_type_names(self, type_str: str) -> list[str]: # noqa: PLR6301
1339+
"""Extract all type names from a type annotation string."""
1340+
# Match type names: word characters starting with uppercase, not preceded by a dot
1341+
# This handles cases like Callable[[Iterable[str]], str]
1342+
pattern = r"(?<![.\w])([A-Z]\w*)"
1343+
return re.findall(pattern, type_str)
1344+
13371345
def _get_python_type_override(self, obj: JsonSchemaObject) -> DataType | None:
13381346
"""Get DataType from x-python-type if it's incompatible with schema type."""
13391347
x_python_type = obj.extras.get("x-python-type")
@@ -1357,7 +1365,18 @@ def _get_python_type_override(self, obj: JsonSchemaObject) -> DataType | None:
13571365
# If not in predefined imports, create import from the full path
13581366
import_ = Import.from_full_path(prefix)
13591367

1360-
return self.data_type(type=type_str, import_=import_)
1368+
# Collect imports for all nested types (e.g., Iterable inside Callable[[Iterable[str]], str])
1369+
nested_imports: list[DataType] = []
1370+
for type_name in self._extract_all_type_names(type_str):
1371+
if type_name != base_type:
1372+
nested_import = self.PYTHON_TYPE_IMPORTS.get(type_name)
1373+
if nested_import:
1374+
nested_imports.append(self.data_type(import_=nested_import))
1375+
1376+
result = self.data_type(type=type_str, import_=import_)
1377+
if nested_imports:
1378+
result.data_types.extend(nested_imports)
1379+
return result
13611380

13621381
def _apply_title_as_name(self, name: str, obj: JsonSchemaObject) -> str:
13631382
"""Apply title as name if use_title_as_name is enabled."""
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# generated by datamodel-codegen:
2+
# filename: x_python_type_nested_imports.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from collections.abc import Callable, Iterable
8+
from typing import TypedDict
9+
10+
from typing_extensions import NotRequired
11+
12+
13+
class Model(TypedDict):
14+
callback: NotRequired[Callable[[Iterable[str]], str]]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# generated by datamodel-codegen:
2+
# filename: x_python_type_nested_unknown_type.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from collections.abc import Callable
8+
from typing import TypedDict
9+
10+
from typing_extensions import NotRequired
11+
12+
13+
class Model(TypedDict):
14+
callback: NotRequired[Callable[[MyCustomType[str]], str]]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"callback": {
5+
"type": "string",
6+
"x-python-type": "Callable[[Iterable[str]], str]"
7+
}
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"callback": {
5+
"type": "string",
6+
"x-python-type": "Callable[[MyCustomType[str]], str]"
7+
}
8+
}
9+
}

tests/main/jsonschema/test_main_jsonschema.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7031,3 +7031,25 @@ def test_x_python_type_custom_fqpath(output_file: Path) -> None:
70317031
assert_func=assert_file_content,
70327032
extra_args=["--output-model-type", "typing.TypedDict"],
70337033
)
7034+
7035+
7036+
def test_x_python_type_nested_imports(output_file: Path) -> None:
7037+
"""Test x-python-type with nested types that require imports (e.g., Callable[[Iterable[str]], str])."""
7038+
run_main_and_assert(
7039+
input_path=JSON_SCHEMA_DATA_PATH / "x_python_type_nested_imports.json",
7040+
output_path=output_file,
7041+
input_file_type=None,
7042+
assert_func=assert_file_content,
7043+
extra_args=["--output-model-type", "typing.TypedDict"],
7044+
)
7045+
7046+
7047+
def test_x_python_type_nested_unknown_type(output_file: Path) -> None:
7048+
"""Test x-python-type with nested types not in PYTHON_TYPE_IMPORTS (e.g., MyCustomType)."""
7049+
run_main_and_assert(
7050+
input_path=JSON_SCHEMA_DATA_PATH / "x_python_type_nested_unknown_type.json",
7051+
output_path=output_file,
7052+
input_file_type=None,
7053+
assert_func=assert_file_content,
7054+
extra_args=["--output-model-type", "typing.TypedDict"],
7055+
)

0 commit comments

Comments
 (0)