Repository: koxudaxi/datamodel-code-generator

Issue:
Corrupted import statements in generated Pydantic classes
Import statements in some specific contexts are replaced with wrong file names.
The error is absent in version 0.37.0 but appears in 0.38.0 and subsequent version up to 0.53.0

I could not reduce this to a reproducible example, it being part of a huge multifile structure, but the examples below at least show the problem, which might be enough given that it is known when the problem appeared?

The project directory structure is

```
.../mxlims/
    schemas/
        data/
        references/
        messages/
        objects/
```

With relevant files

mesages/MsgTop.json

```
{
    "$schema": "https://json-schema.org/draft-07/schema",
    "title": "MsgTop",
    "type": "object",
    "allOf": [
        {
            "$ref": "BaseMessageData.json"
        }
    ],
    "properties": {
        "Plate": {
            "description": "idString:object dictionary of Plates.",
            "title": "Plates",
            "type": "object",
            "minProperties": 1,
            "additionalProperties": {
                "$ref": "../objects/Plate.json"
            }
        },
        "PlateWell": {
            "description": "idString:object dictionary of PlateWells.",
            "title": "PlateWells",
            "type": "object",
            "minProperties": 1,
            "additionalProperties": {
                "$ref": "../objects/PlateWell.json"
            }
        }
    }
}
```

objects/Plate.json

```
{
    "$schema": "https://json-schema.org/draft-07/schema",
    "description": "A crystallization plate, with typed JSON containment lists",
    "title": "Plate",
    "type": "object",
    "allOf": [
        {
            "$ref": "../data/PlateData.json"
        },
        {
            "$ref": "../data/LogisticalSampleData.json"
        }
    ],
    "properties": {
        "mxlimsType": {
            "const": "Plate",
            "description": "The type of MXLIMS object.",
            "title": "MxlimsType",
            "type": "string"
        },
        "containerRef": {
            "allOf": [
                {
                    "reverseLinkName": "contents"
                },
                {
                    "$ref": "../references/ShipmentRef.json"
                }
            ]
        }
    }
}
```

objects/PlateWell.json

```
{
    "$schema": "https://json-schema.org/draft-07/schema",
    "description": "A well in a crystallization plate, with typed JSON containment lists",
    "title": "PlateWell",
    "type": "object",
    "allOf": [
        {
            "$ref": "../data/PlateWellData.json"
        },
        {
            "$ref": "../data/LogisticalSampleData.json"
        }
    ],
    "properties": {
        "mxlimsType": {
            "const": "PlateWell",
            "description": "The type of MXLIMS object.",
            "title": "MxlimsType",
            "type": "string"
        },
        "sampleRef": {
            "allOf": [
                {
                    "reverseLinkName": "logisticalSamples"
                },
                {
                    "$ref": "../references/MacromoleculeSampleRef.json"
                }
            ]
        },
        "containerRef": {
            "allOf": [
                {
                    "reverseLinkName": "contents"
                },
                {
                    "$ref": "../references/PlateRef.json"
                }
            ]
        }
    }
}
```

Incorrect result (version 0.38.0)

pydantic/messages/MsgTop.py

```
# generated by datamodel-codegen:
#   filename:  messages/MsgTop.json

from __future__ import annotations

from typing import Dict, Optional

from pydantic import Field

from ..objects.Plate import Plate
from ..objects.PlateWell import LogisticalSampleData
from .BaseMessageData import BaseMessageData


class MsgTop(BaseMessageData):
    plate: Optional[Dict[str, Plate]] = Field(
        None,
        alias="Plate",
        description="idString:object dictionary of Plates.",
        title="Plates",
    )
    plate_well: Optional[Dict[str, LogisticalSampleData]] = Field(
        None,
        alias="PlateWell",
        description="idString:object dictionary of PlateWells.",
        title="PlateWells",
    )

```


Used commandline:

```

datamodel-codegen --input-file-type jsonschema --output-model-type pydantic_v2.BaseModel --base-class mxlims.impl.MxlimsBase.BaseModel --use-schema-description --use-double-quotes --disable-timestamp --use-default --target-python-version 3.10 --snake-case-field --output-datetime-class datetime --use-exact-imports --capitalise-enum-members --use-title-as-name --use-one-literal-as-default --use-non-positive-negative-number-constrained-types --collapse-root-models --input mxlims/schemas --output mxlims/pydantic 

```

Under Python 3.10 in a conda environment under OpenSUSE 15.6

The correct result, as obtained with version 0.37.0 is

pydantic/messages.MsgTop.py

```
# generated by datamodel-codegen:
#   filename:  messages/MsgTop.json

from __future__ import annotations

from typing import Dict, Optional

from pydantic import Field

from ..objects.Plate import Plate
from ..objects.PlateWell import PlateWell
from .BaseMessageData import BaseMessageData


class MsgTop(BaseMessageData):
    plate: Optional[Dict[str, Plate]] = Field(
        None,
        alias="Plate",
        description="idString:object dictionary of Plates.",
        title="Plates",
    )
    plate_well: Optional[Dict[str, PlateWell]] = Field(
        None,
        alias="PlateWell",
        description="idString:object dictionary of PlateWells.",
        title="PlateWells",
    )

```

Discussion:

Fix (patch):
diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py
index 0436fe89d..e04e02349 100644
--- a/src/datamodel_code_generator/parser/base.py
+++ b/src/datamodel_code_generator/parser/base.py
@@ -1433,7 +1433,11 @@ def __change_from_import(  # noqa: PLR0913, PLR0914
                         ref_module and import_ == data_type.reference.short_name and ref_module[-1] == import_
                     )
 
-                    if from_ and (ref_module in internal_modules or is_module_class_collision):
+                    if (
+                        from_
+                        and not imports.use_exact
+                        and (ref_module in internal_modules or is_module_class_collision)
+                    ):
                         from_ = f"{from_}{import_}" if from_.endswith(".") else f"{from_}.{import_}"
                         import_ = data_type.reference.short_name
                         full_path = from_, import_


Test changes:
diff --git a/tests/data/expected/main/openapi/module_class_name_collision_deep_exact_imports/A/B.py b/tests/data/expected/main/openapi/module_class_name_collision_deep_exact_imports/A/B.py
new file mode 100644
index 000000000..ba3ce7225
--- /dev/null
+++ b/tests/data/expected/main/openapi/module_class_name_collision_deep_exact_imports/A/B.py
@@ -0,0 +1,10 @@
+# generated by datamodel-codegen:
+#   filename:  openapi.json
+
+from __future__ import annotations
+
+from pydantic import BaseModel
+
+
+class B(BaseModel):
+    value: int | None = None
diff --git a/tests/data/expected/main/openapi/module_class_name_collision_deep_exact_imports/A/__init__.py b/tests/data/expected/main/openapi/module_class_name_collision_deep_exact_imports/A/__init__.py
new file mode 100644
index 000000000..528c50c8b
--- /dev/null
+++ b/tests/data/expected/main/openapi/module_class_name_collision_deep_exact_imports/A/__init__.py
@@ -0,0 +1,2 @@
+# generated by datamodel-codegen:
+#   filename:  openapi.json
diff --git a/tests/data/expected/main/openapi/module_class_name_collision_deep_exact_imports/__init__.py b/tests/data/expected/main/openapi/module_class_name_collision_deep_exact_imports/__init__.py
new file mode 100644
index 000000000..d9c9e5b85
--- /dev/null
+++ b/tests/data/expected/main/openapi/module_class_name_collision_deep_exact_imports/__init__.py
@@ -0,0 +1,12 @@
+# generated by datamodel-codegen:
+#   filename:  openapi.json
+
+from __future__ import annotations
+
+from pydantic import RootModel
+
+from .A.B import B
+
+
+class BGetResponse(RootModel[list[B]]):
+    root: list[B]
diff --git a/tests/data/expected/main/openapi/module_class_name_collision_exact_imports/A.py b/tests/data/expected/main/openapi/module_class_name_collision_exact_imports/A.py
new file mode 100644
index 000000000..2f2c552c5
--- /dev/null
+++ b/tests/data/expected/main/openapi/module_class_name_collision_exact_imports/A.py
@@ -0,0 +1,10 @@
+# generated by datamodel-codegen:
+#   filename:  openapi.json
+
+from __future__ import annotations
+
+from pydantic import BaseModel
+
+
+class A(BaseModel):
+    name: str | None = None
diff --git a/tests/data/expected/main/openapi/module_class_name_collision_exact_imports/__init__.py b/tests/data/expected/main/openapi/module_class_name_collision_exact_imports/__init__.py
new file mode 100644
index 000000000..5a2d71e82
--- /dev/null
+++ b/tests/data/expected/main/openapi/module_class_name_collision_exact_imports/__init__.py
@@ -0,0 +1,12 @@
+# generated by datamodel-codegen:
+#   filename:  openapi.json
+
+from __future__ import annotations
+
+from pydantic import RootModel
+
+from .A import A
+
+
+class AGetResponse(RootModel[list[A]]):
+    root: list[A]
diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py
index df7cd8149..e51c27be8 100644
--- a/tests/main/openapi/test_main_openapi.py
+++ b/tests/main/openapi/test_main_openapi.py
@@ -4697,6 +4697,52 @@ def test_main_openapi_module_class_name_collision_deep_pydantic_v2(output_dir: P
     )
 
 
+@pytest.mark.skipif(
+    version.parse(pydantic.VERSION) < version.parse("2.0.0"),
+    reason="Require Pydantic version 2.0.0 or later",
+)
+def test_main_openapi_module_class_name_collision_exact_imports_pydantic_v2(output_dir: Path) -> None:
+    """Test --use-exact-imports with module/class name collision."""
+    run_main_and_assert(
+        input_path=OPEN_API_DATA_PATH / "module_class_name_collision" / "openapi.json",
+        output_path=output_dir,
+        expected_directory=EXPECTED_OPENAPI_PATH / "module_class_name_collision_exact_imports",
+        extra_args=[
+            "--output-model-type",
+            "pydantic_v2.BaseModel",
+            "--openapi-scopes",
+            "schemas",
+            "--openapi-scopes",
+            "paths",
+            "--use-exact-imports",
+            "--disable-timestamp",
+        ],
+    )
+
+
+@pytest.mark.skipif(
+    version.parse(pydantic.VERSION) < version.parse("2.0.0"),
+    reason="Require Pydantic version 2.0.0 or later",
+)
+def test_main_openapi_module_class_name_collision_deep_exact_imports_pydantic_v2(output_dir: Path) -> None:
+    """Test --use-exact-imports with deep module/class name collision."""
+    run_main_and_assert(
+        input_path=OPEN_API_DATA_PATH / "module_class_name_collision_deep" / "openapi.json",
+        output_path=output_dir,
+        expected_directory=EXPECTED_OPENAPI_PATH / "module_class_name_collision_deep_exact_imports",
+        extra_args=[
+            "--output-model-type",
+            "pydantic_v2.BaseModel",
+            "--openapi-scopes",
+            "schemas",
+            "--openapi-scopes",
+            "paths",
+            "--use-exact-imports",
+            "--disable-timestamp",
+        ],
+    )
+
+
 def test_main_nested_package_enum_default(output_dir: Path) -> None:
     """Test enum default values use short names in same module with nested package paths."""
     with freeze_time(TIMESTAMP):