Skip to content

Commit 1b5d3fb

Browse files
authored
fix: Improve state management in Imports, DataType, and DataModel classes (#2705)
* fix: Enhance deep copy functionality and improve import management in core classes * fix: Import Reference in dataclass.py to resolve missing dependency issues * fix: Resolve import issues and enhance deep copy functionality in core classes
1 parent 42a228e commit 1b5d3fb

14 files changed

Lines changed: 337 additions & 18 deletions

File tree

src/datamodel_code_generator/imports.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,27 +88,39 @@ def append(self, imports: Import | Iterable[Import] | None) -> None:
8888
if import_.alias:
8989
self.alias[import_.from_][import_.import_] = import_.alias
9090

91-
def remove(self, imports: Import | Iterable[Import]) -> None:
91+
def remove(self, imports: Import | Iterable[Import]) -> None: # noqa: PLR0912
9292
"""Remove one or more imports from the collection."""
9393
if isinstance(imports, Import): # pragma: no cover
9494
imports = [imports]
9595
for import_ in imports:
9696
if "." in import_.import_: # pragma: no cover
97-
self.counter[None, import_.import_] -= 1
98-
if self.counter[None, import_.import_] == 0: # pragma: no cover
99-
self[None].remove(import_.import_)
100-
if not self[None]:
101-
del self[None]
97+
key = (None, import_.import_)
98+
if self.counter.get(key, 0) <= 0:
99+
continue
100+
self.counter[key] -= 1
101+
if self.counter[key] == 0: # pragma: no cover
102+
del self.counter[key]
103+
if None in self and import_.import_ in self[None]:
104+
self[None].remove(import_.import_)
105+
if not self[None]:
106+
del self[None]
102107
else:
103-
self.counter[import_.from_, import_.import_] -= 1 # pragma: no cover
104-
if self.counter[import_.from_, import_.import_] == 0: # pragma: no cover
105-
self[import_.from_].remove(import_.import_)
106-
if not self[import_.from_]:
107-
del self[import_.from_]
108-
if import_.alias: # pragma: no cover
108+
key = (import_.from_, import_.import_)
109+
if self.counter.get(key, 0) <= 0:
110+
continue
111+
self.counter[key] -= 1 # pragma: no cover
112+
if self.counter[key] == 0: # pragma: no cover
113+
del self.counter[key]
114+
if import_.from_ in self and import_.import_ in self[import_.from_]:
115+
self[import_.from_].remove(import_.import_)
116+
if not self[import_.from_]:
117+
del self[import_.from_]
118+
if import_.alias and import_.from_ in self.alias and import_.import_ in self.alias[import_.from_]:
109119
del self.alias[import_.from_][import_.import_]
110120
if not self.alias[import_.from_]:
111121
del self.alias[import_.from_]
122+
if import_.reference_path and import_.reference_path in self.reference_paths:
123+
del self.reference_paths[import_.reference_path]
112124

113125
def remove_referenced_imports(self, reference_path: str) -> None:
114126
"""Remove imports associated with a reference path."""
@@ -126,6 +138,9 @@ def extract_future(self) -> Imports:
126138
future.counter[key] = self.counter.pop(key)
127139
if future_key in self.alias:
128140
future.alias[future_key] = self.alias.pop(future_key)
141+
for ref_path, import_ in list(self.reference_paths.items()):
142+
if import_.from_ == future_key:
143+
future.reference_paths[ref_path] = self.reference_paths.pop(ref_path)
129144
return future
130145

131146
def add_export(self, name: str) -> None:

src/datamodel_code_generator/model/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,9 +328,12 @@ def copy_deep(self) -> Self:
328328
"""Create a deep copy of this field to avoid mutating the original."""
329329
copied = self.copy()
330330
copied.parent = None
331+
copied.extras = deepcopy(self.extras)
331332
copied.data_type = self.data_type.copy()
332333
if self.data_type.data_types:
333334
copied.data_type.data_types = [dt.copy() for dt in self.data_type.data_types]
335+
if self.data_type.dict_key:
336+
copied.data_type.dict_key = self.data_type.dict_key.copy()
334337
return copied
335338

336339
def replace_data_type(self, new_data_type: DataType, *, clear_old_parent: bool = True) -> None:
@@ -549,6 +552,9 @@ def create_reuse_model(self, base_ref: Reference) -> Self:
549552
path=self.reference.path + "/reuse",
550553
),
551554
custom_template_dir=self._custom_template_dir,
555+
custom_base_class=self.custom_base_class,
556+
keyword_only=self.keyword_only,
557+
treat_dot_as_module=self._treat_dot_as_module,
552558
)
553559

554560
def replace_children_in_models(self, models: list[DataModel], new_ref: Reference) -> None:

src/datamodel_code_generator/model/dataclass.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,14 @@
2121
from datamodel_code_generator.model.pydantic.base_model import Constraints # noqa: TC001 # needed for pydantic
2222
from datamodel_code_generator.model.types import DataTypeManager as _DataTypeManager
2323
from datamodel_code_generator.model.types import type_map_factory
24+
from datamodel_code_generator.reference import Reference
2425
from datamodel_code_generator.types import DataType, StrictTypes, Types, chain_as_tuple
2526

2627
if TYPE_CHECKING:
2728
from collections import defaultdict
2829
from collections.abc import Sequence
2930
from pathlib import Path
3031

31-
from datamodel_code_generator.reference import Reference
32-
3332

3433
def has_field_assignment(field: DataModelFieldBase) -> bool:
3534
"""Check if a dataclass field has a default value or field() assignment."""
@@ -91,6 +90,24 @@ def __init__( # noqa: PLR0913
9190
if keyword_only:
9291
self.dataclass_arguments["kw_only"] = True
9392

93+
def create_reuse_model(self, base_ref: Reference) -> DataClass:
94+
"""Create inherited model with empty fields pointing to base reference."""
95+
return self.__class__(
96+
fields=[],
97+
base_classes=[base_ref],
98+
description=self.description,
99+
reference=Reference(
100+
name=self.name,
101+
path=self.reference.path + "/reuse",
102+
),
103+
custom_template_dir=self._custom_template_dir,
104+
custom_base_class=self.custom_base_class,
105+
keyword_only=self.keyword_only,
106+
frozen=self.frozen,
107+
treat_dot_as_module=self._treat_dot_as_module,
108+
dataclass_arguments=self.dataclass_arguments,
109+
)
110+
94111

95112
class DataModelField(DataModelFieldBase):
96113
"""Field implementation for dataclass models."""

src/datamodel_code_generator/types.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,10 @@ def _remove_none_from_union(type_: str, *, use_union_operator: bool) -> str: #
207207
elif char == "]" and in_constr == 0:
208208
inner_count -= 1
209209
elif char == "(":
210-
if current_part.strip().startswith("constr(") and current_part[-2] != "\\":
211-
# non-escaped opening round bracket found inside constraint string expression
210+
if current_part.strip().startswith("constr(") and (len(current_part) < 2 or current_part[-2] != "\\"): # noqa: PLR2004
212211
in_constr += 1
213212
elif char == ")":
214-
if in_constr > 0 and current_part[-2] != "\\":
215-
# non-escaped closing round bracket found inside constraint string expression
213+
if in_constr > 0 and (len(current_part) < 2 or current_part[-2] != "\\"): # noqa: PLR2004
216214
in_constr -= 1
217215
elif char == separator and inner_count == 0 and in_constr == 0:
218216
part = current_part[:-1].strip()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# generated by datamodel-codegen:
2+
# filename: copy_deep_pattern_properties.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 AwareDatetime, BaseModel, Field, constr
10+
11+
12+
class MetadataRequest(BaseModel):
13+
tags: Optional[dict[constr(pattern=r'^[a-z][a-z0-9_]*$'), str]] = Field(
14+
None, description='Dynamic key-value metadata'
15+
)
16+
17+
18+
class Metadata(BaseModel):
19+
tags: Optional[dict[constr(pattern=r'^[a-z][a-z0-9_]*$'), str]] = Field(
20+
None, description='Dynamic key-value metadata'
21+
)
22+
created_at: Optional[AwareDatetime] = None
23+
24+
25+
class ExtendedMetadataRequest(BaseModel):
26+
tags: Optional[dict[constr(pattern=r'^[a-z][a-z0-9_]*$'), str]] = Field(
27+
None, description='Dynamic key-value metadata'
28+
)
29+
owner: Optional[str] = None
30+
31+
32+
class ExtendedMetadata(Metadata):
33+
id: int
34+
owner: Optional[str] = None
35+
36+
37+
class Model(BaseModel):
38+
metadata: Optional[ExtendedMetadata] = 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: reuse_scope_tree_dataclass
3+
# timestamp: 2019-07-26T00:00:00+00:00
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# generated by datamodel-codegen:
2+
# filename: reuse_scope_tree_dataclass
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from dataclasses import dataclass
8+
from typing import Optional
9+
10+
from .shared import SharedModel as SharedModel_1
11+
12+
13+
@dataclass(frozen=True)
14+
class SharedModel(SharedModel_1):
15+
pass
16+
17+
18+
@dataclass(frozen=True)
19+
class Model:
20+
data: Optional[SharedModel] = 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: schema_b.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from dataclasses import dataclass
8+
from typing import Optional
9+
10+
from . import shared
11+
12+
13+
@dataclass(frozen=True)
14+
class Model:
15+
info: Optional[shared.SharedModel] = None
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: shared.py
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from dataclasses import dataclass
8+
from typing import Optional
9+
10+
11+
@dataclass(frozen=True)
12+
class SharedModel:
13+
id: Optional[int] = None
14+
name: Optional[str] = None
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"definitions": {
4+
"Metadata": {
5+
"type": "object",
6+
"properties": {
7+
"tags": {
8+
"type": "object",
9+
"description": "Dynamic key-value metadata",
10+
"patternProperties": {
11+
"^[a-z][a-z0-9_]*$": {
12+
"type": "string"
13+
}
14+
}
15+
},
16+
"created_at": {
17+
"type": "string",
18+
"format": "date-time",
19+
"readOnly": true
20+
}
21+
}
22+
},
23+
"ExtendedMetadata": {
24+
"allOf": [
25+
{
26+
"$ref": "#/definitions/Metadata"
27+
},
28+
{
29+
"type": "object",
30+
"required": ["id"],
31+
"properties": {
32+
"id": {
33+
"type": "integer",
34+
"readOnly": true
35+
},
36+
"owner": {
37+
"type": "string"
38+
}
39+
}
40+
}
41+
]
42+
}
43+
},
44+
"type": "object",
45+
"properties": {
46+
"metadata": {
47+
"$ref": "#/definitions/ExtendedMetadata"
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)