Repository: koxudaxi/datamodel-code-generator

Issue:
Relative imports from parallel directories incorrect
JSON schemas that with a previous version of datamodel-codegenerator (maybe 6 months+ ago - precise version not known) produced the following correct import:
`from ..datatypes.FooStats import FooStats`

now (version 0.53.0) produces duplicate incorrect imports:

```
from ..datatypes import FooStats as FooStats_1
from ..datatypes.FooStats.FooStats import FooStats
```

Testing using the files given below shows that version 0.42.0 gives the correct import statement, whereas versions 0.43.0, 0.44.0 and 0.45.0 gives the following statement instead
`from ..datatypes.FooStats.FooStats import FooStats`


The following directory structure:
...mxlims/schematst/data/ ReflectionSetData.json:
```
{
    "$schema": "https://json-schema.org/draft-07/schema",
    "title": "ReflectionSetData",
    "type": "object",
    "properties": {
        "fooStatsOverall": {
            "allOf": [
                {
                    "$ref": "../datatypes/FooStats.json"
                }
            ]
        },
        "fooStatsShells": {
            "items": {
                "$ref": "../datatypes/FooStats.json"
            },
            "title": "Foo Stats Shells",
            "type": "array"
        }
    }
}

```
...mxlims/schematst/datatypes/ FooStats.json:
```
{
    "$schema": "https://json-schema.org/draft-07/schema",
    "title": "FooStats",
    "type": "object"
}

```


Run with the command
`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/schematst --output mxlims/pydantictst
`

Produced the following files:
...mxlims/pydantictst/data/ ReflectionSetData.py:
```
# generated by datamodel-codegen:
#   filename:  data/ReflectionSetData.json

from __future__ import annotations

from mxlims.impl.MxlimsBase import BaseModel
from pydantic import Field

from ..datatypes import FooStats as FooStats_1
from ..datatypes.FooStats.FooStats import FooStats


class ReflectionSetData(BaseModel):
    foo_stats_overall: FooStats | None = Field(None, alias="fooStatsOverall")
    foo_stats_shells: list[FooStats_1] | None = Field(
        None, alias="fooStatsShells", title="Foo Stats Shells"
    )
```

and

...mxlims/pydantictst/datatypes/ FooStats.py:
```
# generated by datamodel-codegen:
#   filename:  datatypes/FooStats.json

from __future__ import annotations

from mxlims.impl.MxlimsBase import BaseModel


class FooStats(BaseModel):
    pass
```

Expected behaviour is the restoration of the correct import syntax:

`from ..datatypes.FooStats import FooStats`

Discussion:

Fix (patch):
diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py
index 345b85af1..8544df3b2 100644
--- a/src/datamodel_code_generator/parser/base.py
+++ b/src/datamodel_code_generator/parser/base.py
@@ -2079,6 +2079,8 @@ def __collapse_root_models(  # noqa: PLR0912, PLR0914, PLR0915
                         if d.reference is None:
                             continue
                         from_, import_ = full_path = relative(model.module_name, d.full_name)
+                        if imports.use_exact:
+                            from_, import_ = full_path = exact_import(from_, import_, d.reference.short_name)
                         if from_ and import_:
                             alias = scoped_model_resolver.add(full_path, import_)
                             d.alias = (
@@ -3393,6 +3395,12 @@ def parse(  # noqa: PLR0913, PLR0914, PLR0917
 
         self._finalize_modules(contexts, unused_models, model_to_module_models, module_to_import)
 
+        root_init: ModulePath = ("__init__.py",)
+        if root_init not in results:
+            top_level_dirs = {k[0] for k in results if len(k) >= 2}  # noqa: PLR2004
+            if len(top_level_dirs) > 1:
+                results[root_init] = Result(body="")
+
         future_imports = self.imports.extract_future()
         future_imports_str = str(future_imports)
 


Test changes:
diff --git a/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/__init__.py b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/__init__.py
new file mode 100644
index 000000000..12cdce5c3
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/__init__.py
@@ -0,0 +1,2 @@
+# generated by datamodel-codegen:
+#   filename:  exact_imports_collapse_root_models
diff --git a/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/data/ReflectionSetData.py b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/data/ReflectionSetData.py
new file mode 100644
index 000000000..e0b1a4189
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/data/ReflectionSetData.py
@@ -0,0 +1,15 @@
+# generated by datamodel-codegen:
+#   filename:  data/ReflectionSetData.json
+
+from __future__ import annotations
+
+from pydantic import BaseModel, Field
+
+from ..datatypes.BarStats import BarStats
+from ..datatypes.FooStats import FooStats
+
+
+class ReflectionSetData(BaseModel):
+    stats: FooStats | None = None
+    stats_list: list[FooStats] | None = None
+    stats_collection: list[BarStats] | None = Field(None, title='BarStatsList')
diff --git a/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/data/__init__.py b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/data/__init__.py
new file mode 100644
index 000000000..12cdce5c3
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/data/__init__.py
@@ -0,0 +1,2 @@
+# generated by datamodel-codegen:
+#   filename:  exact_imports_collapse_root_models
diff --git a/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/datatypes/BarStats.py b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/datatypes/BarStats.py
new file mode 100644
index 000000000..90c377643
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/datatypes/BarStats.py
@@ -0,0 +1,10 @@
+# generated by datamodel-codegen:
+#   filename:  datatypes/BarStats.json
+
+from __future__ import annotations
+
+from pydantic import BaseModel
+
+
+class BarStats(BaseModel):
+    value: float | None = None
diff --git a/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/datatypes/FooStats.py b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/datatypes/FooStats.py
new file mode 100644
index 000000000..b49b734ef
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/datatypes/FooStats.py
@@ -0,0 +1,11 @@
+# generated by datamodel-codegen:
+#   filename:  datatypes/FooStats.json
+
+from __future__ import annotations
+
+from pydantic import BaseModel
+
+
+class FooStats(BaseModel):
+    count: int | None = None
+    average: float | None = None
diff --git a/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/datatypes/__init__.py b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/datatypes/__init__.py
new file mode 100644
index 000000000..12cdce5c3
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/exact_imports_collapse_root_models/datatypes/__init__.py
@@ -0,0 +1,2 @@
+# generated by datamodel-codegen:
+#   filename:  exact_imports_collapse_root_models
diff --git a/tests/data/jsonschema/exact_imports_collapse_root_models/data/ReflectionSetData.json b/tests/data/jsonschema/exact_imports_collapse_root_models/data/ReflectionSetData.json
new file mode 100644
index 000000000..28d651b29
--- /dev/null
+++ b/tests/data/jsonschema/exact_imports_collapse_root_models/data/ReflectionSetData.json
@@ -0,0 +1,23 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ReflectionSetData",
+  "type": "object",
+  "properties": {
+    "stats": {
+      "allOf": [
+        {
+          "$ref": "../datatypes/FooStats.json"
+        }
+      ]
+    },
+    "stats_list": {
+      "type": "array",
+      "items": {
+        "$ref": "../datatypes/FooStats.json"
+      }
+    },
+    "stats_collection": {
+      "$ref": "../datatypes/BarStatsList.json"
+    }
+  }
+}
diff --git a/tests/data/jsonschema/exact_imports_collapse_root_models/datatypes/BarStats.json b/tests/data/jsonschema/exact_imports_collapse_root_models/datatypes/BarStats.json
new file mode 100644
index 000000000..ed7acc2cd
--- /dev/null
+++ b/tests/data/jsonschema/exact_imports_collapse_root_models/datatypes/BarStats.json
@@ -0,0 +1,10 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "BarStats",
+    "type": "object",
+    "properties": {
+        "value": {
+            "type": "number"
+        }
+    }
+}
diff --git a/tests/data/jsonschema/exact_imports_collapse_root_models/datatypes/BarStatsList.json b/tests/data/jsonschema/exact_imports_collapse_root_models/datatypes/BarStatsList.json
new file mode 100644
index 000000000..6a1827075
--- /dev/null
+++ b/tests/data/jsonschema/exact_imports_collapse_root_models/datatypes/BarStatsList.json
@@ -0,0 +1,8 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "BarStatsList",
+    "type": "array",
+    "items": {
+        "$ref": "BarStats.json"
+    }
+}
diff --git a/tests/data/jsonschema/exact_imports_collapse_root_models/datatypes/FooStats.json b/tests/data/jsonschema/exact_imports_collapse_root_models/datatypes/FooStats.json
new file mode 100644
index 000000000..b072e7159
--- /dev/null
+++ b/tests/data/jsonschema/exact_imports_collapse_root_models/datatypes/FooStats.json
@@ -0,0 +1,13 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "FooStats",
+  "type": "object",
+  "properties": {
+    "count": {
+      "type": "integer"
+    },
+    "average": {
+      "type": "number"
+    }
+  }
+}
diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py
index 6740bba7a..6aabb6de6 100644
--- a/tests/main/jsonschema/test_main_jsonschema.py
+++ b/tests/main/jsonschema/test_main_jsonschema.py
@@ -8647,3 +8647,26 @@ def test_ref_merge_additional_properties(output_file: Path) -> None:
             "--use-annotated",
         ],
     )
+
+
+@PYDANTIC_V2_SKIP
+def test_main_exact_imports_collapse_root_models_module_class_collision(output_dir: Path) -> None:
+    """Test --use-exact-imports with --collapse-root-models when module and class names collide."""
+    run_main_and_assert(
+        input_path=JSON_SCHEMA_DATA_PATH / "exact_imports_collapse_root_models",
+        output_path=output_dir,
+        input_file_type="jsonschema",
+        expected_directory=EXPECTED_JSON_SCHEMA_PATH / "exact_imports_collapse_root_models",
+        extra_args=[
+            "--output-model-type",
+            "pydantic_v2.BaseModel",
+            "--target-python-version",
+            "3.10",
+            "--use-exact-imports",
+            "--collapse-root-models",
+            "--use-title-as-name",
+            "--snake-case-field",
+            "--disable-timestamp",
+        ],
+        force_exec_validation=True,
+    )