Skip to content

Commit 27929ad

Browse files
Fix incorrect import when module and class have the same name (#2648)
* Fix module and class name collision in OpenAPI schemas * [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 5de9bf7 commit 27929ad

9 files changed

Lines changed: 190 additions & 2 deletions

File tree

src/datamodel_code_generator/parser/base.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -938,8 +938,13 @@ def __change_from_import(
938938
from_ = from_[rel_path_depth:]
939939

940940
ref_module = tuple(data_type.full_name.split(".")[:-1])
941-
if from_ and ref_module in internal_modules:
942-
from_ = f"{from_}{import_}"
941+
942+
is_module_class_collision = (
943+
ref_module and import_ == data_type.reference.short_name and ref_module[-1] == import_
944+
)
945+
946+
if from_ and (ref_module in internal_modules or is_module_class_collision):
947+
from_ = f"{from_}{import_}" if from_.endswith(".") else f"{from_}.{import_}"
943948
import_ = data_type.reference.short_name
944949
full_path = from_, import_
945950

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# generated by datamodel-codegen:
2+
# filename: openapi.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel
10+
11+
12+
class A(BaseModel):
13+
name: Optional[str] = None
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# generated by datamodel-codegen:
2+
# filename: openapi.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import List
8+
9+
from pydantic import RootModel
10+
11+
from .A import A
12+
13+
14+
class AGetResponse(RootModel[List[A]]):
15+
root: List[A]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# generated by datamodel-codegen:
2+
# filename: openapi.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel
10+
11+
12+
class B(BaseModel):
13+
value: Optional[int] = None
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# generated by datamodel-codegen:
2+
# filename: openapi.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# generated by datamodel-codegen:
2+
# filename: openapi.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import List
8+
9+
from pydantic import RootModel
10+
11+
from .A.B import B
12+
13+
14+
class BGetResponse(RootModel[List[B]]):
15+
root: List[B]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "Test API",
5+
"version": "1.0.0"
6+
},
7+
"paths": {
8+
"/a": {
9+
"get": {
10+
"operationId": "getA",
11+
"responses": {
12+
"200": {
13+
"description": "Success",
14+
"content": {
15+
"application/json": {
16+
"schema": {
17+
"type": "array",
18+
"items": {
19+
"$ref": "#/components/schemas/A.A"
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}
27+
}
28+
},
29+
"components": {
30+
"schemas": {
31+
"A.A": {
32+
"type": "object",
33+
"properties": {
34+
"name": {
35+
"type": "string"
36+
}
37+
}
38+
}
39+
}
40+
}
41+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "Test API",
5+
"version": "1.0.0"
6+
},
7+
"paths": {
8+
"/b": {
9+
"get": {
10+
"operationId": "getB",
11+
"responses": {
12+
"200": {
13+
"description": "Success",
14+
"content": {
15+
"application/json": {
16+
"schema": {
17+
"type": "array",
18+
"items": {
19+
"$ref": "#/components/schemas/A.B.B"
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}
27+
}
28+
},
29+
"components": {
30+
"schemas": {
31+
"A.B.B": {
32+
"type": "object",
33+
"properties": {
34+
"value": {
35+
"type": "integer"
36+
}
37+
}
38+
}
39+
}
40+
}
41+
}

tests/main/openapi/test_main_openapi.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3320,6 +3320,48 @@ def test_main_allof_enum_ref(output_file: Path) -> None:
33203320
)
33213321

33223322

3323+
@pytest.mark.skipif(
3324+
version.parse(pydantic.VERSION) < version.parse("2.0.0"),
3325+
reason="Require Pydantic version 2.0.0 or later",
3326+
)
3327+
def test_main_openapi_module_class_name_collision_pydantic_v2(output_dir: Path) -> None:
3328+
"""Test Issue #1994: module and class name collision (e.g., A.A schema)."""
3329+
run_main_and_assert(
3330+
input_path=OPEN_API_DATA_PATH / "module_class_name_collision" / "openapi.json",
3331+
output_path=output_dir,
3332+
expected_directory=EXPECTED_OPENAPI_PATH / "module_class_name_collision",
3333+
extra_args=[
3334+
"--output-model-type",
3335+
"pydantic_v2.BaseModel",
3336+
"--openapi-scopes",
3337+
"schemas",
3338+
"--openapi-scopes",
3339+
"paths",
3340+
],
3341+
)
3342+
3343+
3344+
@pytest.mark.skipif(
3345+
version.parse(pydantic.VERSION) < version.parse("2.0.0"),
3346+
reason="Require Pydantic version 2.0.0 or later",
3347+
)
3348+
def test_main_openapi_module_class_name_collision_deep_pydantic_v2(output_dir: Path) -> None:
3349+
"""Test Issue #1994: deep module collision (e.g., A.B.B schema)."""
3350+
run_main_and_assert(
3351+
input_path=OPEN_API_DATA_PATH / "module_class_name_collision_deep" / "openapi.json",
3352+
output_path=output_dir,
3353+
expected_directory=EXPECTED_OPENAPI_PATH / "module_class_name_collision_deep",
3354+
extra_args=[
3355+
"--output-model-type",
3356+
"pydantic_v2.BaseModel",
3357+
"--openapi-scopes",
3358+
"schemas",
3359+
"--openapi-scopes",
3360+
"paths",
3361+
],
3362+
)
3363+
3364+
33233365
def test_main_nested_package_enum_default(output_dir: Path) -> None:
33243366
"""Test enum default values use short names in same module with nested package paths."""
33253367
with freeze_time(TIMESTAMP):

0 commit comments

Comments
 (0)