datamodel-code-generator 0.11.12__py3-none-any.whl → 0.45.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datamodel_code_generator/__init__.py +654 -185
- datamodel_code_generator/__main__.py +872 -388
- datamodel_code_generator/arguments.py +798 -0
- datamodel_code_generator/cli_options.py +295 -0
- datamodel_code_generator/format.py +292 -54
- datamodel_code_generator/http.py +85 -10
- datamodel_code_generator/imports.py +152 -43
- datamodel_code_generator/model/__init__.py +138 -1
- datamodel_code_generator/model/base.py +531 -120
- datamodel_code_generator/model/dataclass.py +211 -0
- datamodel_code_generator/model/enum.py +133 -12
- datamodel_code_generator/model/imports.py +22 -0
- datamodel_code_generator/model/msgspec.py +462 -0
- datamodel_code_generator/model/pydantic/__init__.py +30 -25
- datamodel_code_generator/model/pydantic/base_model.py +304 -100
- datamodel_code_generator/model/pydantic/custom_root_type.py +11 -2
- datamodel_code_generator/model/pydantic/dataclass.py +15 -4
- datamodel_code_generator/model/pydantic/imports.py +40 -27
- datamodel_code_generator/model/pydantic/types.py +188 -96
- datamodel_code_generator/model/pydantic_v2/__init__.py +51 -0
- datamodel_code_generator/model/pydantic_v2/base_model.py +268 -0
- datamodel_code_generator/model/pydantic_v2/imports.py +15 -0
- datamodel_code_generator/model/pydantic_v2/root_model.py +35 -0
- datamodel_code_generator/model/pydantic_v2/types.py +143 -0
- datamodel_code_generator/model/scalar.py +124 -0
- datamodel_code_generator/model/template/Enum.jinja2 +15 -2
- datamodel_code_generator/model/template/ScalarTypeAliasAnnotation.jinja2 +6 -0
- datamodel_code_generator/model/template/ScalarTypeAliasType.jinja2 +6 -0
- datamodel_code_generator/model/template/ScalarTypeStatement.jinja2 +6 -0
- datamodel_code_generator/model/template/TypeAliasAnnotation.jinja2 +20 -0
- datamodel_code_generator/model/template/TypeAliasType.jinja2 +20 -0
- datamodel_code_generator/model/template/TypeStatement.jinja2 +20 -0
- datamodel_code_generator/model/template/TypedDict.jinja2 +5 -0
- datamodel_code_generator/model/template/TypedDictClass.jinja2 +25 -0
- datamodel_code_generator/model/template/TypedDictFunction.jinja2 +24 -0
- datamodel_code_generator/model/template/UnionTypeAliasAnnotation.jinja2 +10 -0
- datamodel_code_generator/model/template/UnionTypeAliasType.jinja2 +10 -0
- datamodel_code_generator/model/template/UnionTypeStatement.jinja2 +10 -0
- datamodel_code_generator/model/template/dataclass.jinja2 +50 -0
- datamodel_code_generator/model/template/msgspec.jinja2 +55 -0
- datamodel_code_generator/model/template/pydantic/BaseModel.jinja2 +17 -4
- datamodel_code_generator/model/template/pydantic/BaseModel_root.jinja2 +12 -4
- datamodel_code_generator/model/template/pydantic/Config.jinja2 +1 -1
- datamodel_code_generator/model/template/pydantic/dataclass.jinja2 +15 -2
- datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 +57 -0
- datamodel_code_generator/model/template/pydantic_v2/ConfigDict.jinja2 +5 -0
- datamodel_code_generator/model/template/pydantic_v2/RootModel.jinja2 +48 -0
- datamodel_code_generator/model/type_alias.py +70 -0
- datamodel_code_generator/model/typed_dict.py +161 -0
- datamodel_code_generator/model/types.py +106 -0
- datamodel_code_generator/model/union.py +105 -0
- datamodel_code_generator/parser/__init__.py +30 -12
- datamodel_code_generator/parser/_graph.py +67 -0
- datamodel_code_generator/parser/_scc.py +171 -0
- datamodel_code_generator/parser/base.py +2426 -380
- datamodel_code_generator/parser/graphql.py +652 -0
- datamodel_code_generator/parser/jsonschema.py +2518 -647
- datamodel_code_generator/parser/openapi.py +631 -222
- datamodel_code_generator/py.typed +0 -0
- datamodel_code_generator/pydantic_patch.py +28 -0
- datamodel_code_generator/reference.py +672 -290
- datamodel_code_generator/types.py +521 -145
- datamodel_code_generator/util.py +155 -0
- datamodel_code_generator/watch.py +65 -0
- datamodel_code_generator-0.45.0.dist-info/METADATA +301 -0
- datamodel_code_generator-0.45.0.dist-info/RECORD +69 -0
- {datamodel_code_generator-0.11.12.dist-info → datamodel_code_generator-0.45.0.dist-info}/WHEEL +1 -1
- datamodel_code_generator-0.45.0.dist-info/entry_points.txt +2 -0
- datamodel_code_generator/version.py +0 -1
- datamodel_code_generator-0.11.12.dist-info/METADATA +0 -440
- datamodel_code_generator-0.11.12.dist-info/RECORD +0 -31
- datamodel_code_generator-0.11.12.dist-info/entry_points.txt +0 -3
- {datamodel_code_generator-0.11.12.dist-info → datamodel_code_generator-0.45.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,241 +1,595 @@
|
|
|
1
|
+
"""Base classes for data model generation.
|
|
2
|
+
|
|
3
|
+
Provides ConstraintsBase for field constraints, DataModelFieldBase for field
|
|
4
|
+
representation, and DataModel as the abstract base for all model types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
1
10
|
from abc import ABC, abstractmethod
|
|
2
11
|
from collections import defaultdict
|
|
3
|
-
from
|
|
12
|
+
from copy import deepcopy
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from functools import cached_property, lru_cache
|
|
4
15
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
6
|
-
|
|
7
|
-
ClassVar,
|
|
8
|
-
DefaultDict,
|
|
9
|
-
Dict,
|
|
10
|
-
FrozenSet,
|
|
11
|
-
Iterator,
|
|
12
|
-
List,
|
|
13
|
-
Optional,
|
|
14
|
-
Set,
|
|
15
|
-
Tuple,
|
|
16
|
-
Union,
|
|
17
|
-
)
|
|
16
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypeVar, Union
|
|
17
|
+
from warnings import warn
|
|
18
18
|
|
|
19
19
|
from jinja2 import Environment, FileSystemLoader, Template
|
|
20
|
-
from pydantic import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
from datamodel_code_generator.imports import
|
|
20
|
+
from pydantic import Field
|
|
21
|
+
from typing_extensions import Self
|
|
22
|
+
|
|
23
|
+
from datamodel_code_generator.imports import (
|
|
24
|
+
IMPORT_ANNOTATED,
|
|
25
|
+
IMPORT_OPTIONAL,
|
|
26
|
+
IMPORT_UNION,
|
|
27
|
+
Import,
|
|
28
|
+
)
|
|
24
29
|
from datamodel_code_generator.reference import Reference, _BaseModel
|
|
25
|
-
from datamodel_code_generator.types import
|
|
30
|
+
from datamodel_code_generator.types import (
|
|
31
|
+
ANY,
|
|
32
|
+
NONE,
|
|
33
|
+
OPTIONAL_PREFIX,
|
|
34
|
+
UNION_PREFIX,
|
|
35
|
+
DataType,
|
|
36
|
+
Nullable,
|
|
37
|
+
chain_as_tuple,
|
|
38
|
+
get_optional_type,
|
|
39
|
+
)
|
|
40
|
+
from datamodel_code_generator.util import PYDANTIC_V2, ConfigDict
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from collections.abc import Iterator
|
|
44
|
+
|
|
45
|
+
from datamodel_code_generator import DataclassArguments
|
|
46
|
+
|
|
47
|
+
TEMPLATE_DIR: Path = Path(__file__).parents[0] / "template"
|
|
48
|
+
|
|
49
|
+
ALL_MODEL: str = "#all#"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def repr_set_sorted(value: set[Any]) -> str:
|
|
53
|
+
"""Return a repr of a set with elements sorted for consistent output.
|
|
54
|
+
|
|
55
|
+
Uses (type_name, repr(x)) as sort key to safely handle any type including
|
|
56
|
+
Enum, custom classes, or types without __lt__ defined.
|
|
57
|
+
"""
|
|
58
|
+
if not value:
|
|
59
|
+
return "set()"
|
|
60
|
+
# Sort by type name first, then by repr for consistent output
|
|
61
|
+
sorted_elements = sorted(value, key=lambda x: (type(x).__name__, repr(x)))
|
|
62
|
+
return "{" + ", ".join(repr(e) for e in sorted_elements) + "}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
ConstraintsBaseT = TypeVar("ConstraintsBaseT", bound="ConstraintsBase")
|
|
66
|
+
DataModelFieldBaseT = TypeVar("DataModelFieldBaseT", bound="DataModelFieldBase")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ConstraintsBase(_BaseModel):
|
|
70
|
+
"""Base class for field constraints (min/max, patterns, etc.)."""
|
|
71
|
+
|
|
72
|
+
unique_items: Optional[bool] = Field(None, alias="uniqueItems") # noqa: UP045
|
|
73
|
+
_exclude_fields: ClassVar[set[str]] = {"has_constraints"}
|
|
74
|
+
if PYDANTIC_V2:
|
|
75
|
+
model_config = ConfigDict( # pyright: ignore[reportAssignmentType]
|
|
76
|
+
arbitrary_types_allowed=True, ignored_types=(cached_property,)
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
|
|
80
|
+
class Config:
|
|
81
|
+
"""Pydantic v1 configuration for ConstraintsBase."""
|
|
82
|
+
|
|
83
|
+
arbitrary_types_allowed = True
|
|
84
|
+
keep_untouched = (cached_property,)
|
|
85
|
+
|
|
86
|
+
@cached_property
|
|
87
|
+
def has_constraints(self) -> bool:
|
|
88
|
+
"""Check if any constraint values are set."""
|
|
89
|
+
return any(v is not None for v in self.dict().values())
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def merge_constraints(a: ConstraintsBaseT | None, b: ConstraintsBaseT | None) -> ConstraintsBaseT | None:
|
|
93
|
+
"""Merge two constraint objects, with b taking precedence over a."""
|
|
94
|
+
constraints_class = None
|
|
95
|
+
if isinstance(a, ConstraintsBase): # pragma: no cover
|
|
96
|
+
root_type_field_constraints = {k: v for k, v in a.dict(by_alias=True).items() if v is not None}
|
|
97
|
+
constraints_class = a.__class__
|
|
98
|
+
else:
|
|
99
|
+
root_type_field_constraints = {} # pragma: no cover
|
|
100
|
+
|
|
101
|
+
if isinstance(b, ConstraintsBase): # pragma: no cover
|
|
102
|
+
model_field_constraints = {k: v for k, v in b.dict(by_alias=True).items() if v is not None}
|
|
103
|
+
constraints_class = constraints_class or b.__class__
|
|
104
|
+
else:
|
|
105
|
+
model_field_constraints = {}
|
|
106
|
+
|
|
107
|
+
if constraints_class is None or not issubclass(constraints_class, ConstraintsBase): # pragma: no cover
|
|
108
|
+
return None
|
|
26
109
|
|
|
27
|
-
|
|
110
|
+
return constraints_class.parse_obj({
|
|
111
|
+
**root_type_field_constraints,
|
|
112
|
+
**model_field_constraints,
|
|
113
|
+
})
|
|
28
114
|
|
|
29
|
-
OPTIONAL: str = 'Optional'
|
|
30
115
|
|
|
31
|
-
|
|
116
|
+
@dataclass(repr=False)
|
|
117
|
+
class WrappedDefault:
|
|
118
|
+
"""Represents a default value wrapped with its type constructor."""
|
|
32
119
|
|
|
120
|
+
value: Any
|
|
121
|
+
type_name: str
|
|
33
122
|
|
|
34
|
-
|
|
35
|
-
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
"""Return type constructor representation, e.g., 'CountType(10)'."""
|
|
125
|
+
return f"{self.type_name}({self.value!r})"
|
|
36
126
|
|
|
37
127
|
|
|
38
128
|
class DataModelFieldBase(_BaseModel):
|
|
39
|
-
|
|
40
|
-
|
|
129
|
+
"""Base class for model field representation and rendering."""
|
|
130
|
+
|
|
131
|
+
if PYDANTIC_V2:
|
|
132
|
+
model_config = ConfigDict( # pyright: ignore[reportAssignmentType]
|
|
133
|
+
arbitrary_types_allowed=True,
|
|
134
|
+
defer_build=True,
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
|
|
138
|
+
class Config:
|
|
139
|
+
"""Pydantic v1 configuration for DataModelFieldBase."""
|
|
140
|
+
|
|
141
|
+
arbitrary_types_allowed = True
|
|
142
|
+
|
|
143
|
+
name: Optional[str] = None # noqa: UP045
|
|
144
|
+
default: Optional[Any] = None # noqa: UP045
|
|
41
145
|
required: bool = False
|
|
42
|
-
alias: Optional[str]
|
|
146
|
+
alias: Optional[str] = None # noqa: UP045
|
|
43
147
|
data_type: DataType
|
|
44
148
|
constraints: Any = None
|
|
45
149
|
strip_default_none: bool = False
|
|
46
|
-
nullable: Optional[bool] = None
|
|
47
|
-
parent: Optional[
|
|
48
|
-
extras:
|
|
150
|
+
nullable: Optional[bool] = None # noqa: UP045
|
|
151
|
+
parent: Optional[DataModel] = None # noqa: UP045
|
|
152
|
+
extras: dict[str, Any] = Field(default_factory=dict)
|
|
49
153
|
use_annotated: bool = False
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
154
|
+
use_serialize_as_any: bool = False
|
|
155
|
+
has_default: bool = False
|
|
156
|
+
use_field_description: bool = False
|
|
157
|
+
use_inline_field_description: bool = False
|
|
158
|
+
const: bool = False
|
|
159
|
+
original_name: Optional[str] = None # noqa: UP045
|
|
160
|
+
use_default_kwarg: bool = False
|
|
161
|
+
use_one_literal_as_default: bool = False
|
|
162
|
+
_exclude_fields: ClassVar[set[str]] = {"parent"}
|
|
163
|
+
_pass_fields: ClassVar[set[str]] = {"parent", "data_type"}
|
|
164
|
+
can_have_extra_keys: ClassVar[bool] = True
|
|
165
|
+
type_has_null: Optional[bool] = None # noqa: UP045
|
|
166
|
+
read_only: bool = False
|
|
167
|
+
write_only: bool = False
|
|
168
|
+
use_frozen_field: bool = False
|
|
169
|
+
|
|
170
|
+
if not TYPE_CHECKING:
|
|
171
|
+
if not PYDANTIC_V2:
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def model_rebuild(
|
|
175
|
+
cls,
|
|
176
|
+
*,
|
|
177
|
+
_types_namespace: dict[str, type] | None = None,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Update forward references for Pydantic v1."""
|
|
180
|
+
localns = _types_namespace or {}
|
|
181
|
+
cls.update_forward_refs(**localns)
|
|
182
|
+
|
|
183
|
+
def __init__(self, **data: Any) -> None:
|
|
184
|
+
"""Initialize the field and set up parent relationships."""
|
|
185
|
+
super().__init__(**data)
|
|
186
|
+
if self.data_type.reference or self.data_type.data_types:
|
|
187
|
+
self.data_type.parent = self
|
|
188
|
+
self.process_const()
|
|
189
|
+
|
|
190
|
+
def process_const(self) -> None:
|
|
191
|
+
"""Process const values by setting them as defaults."""
|
|
192
|
+
if "const" not in self.extras:
|
|
193
|
+
return
|
|
194
|
+
self.default = self.extras["const"]
|
|
195
|
+
self.const = True
|
|
196
|
+
self.required = False
|
|
197
|
+
self.nullable = False
|
|
198
|
+
|
|
199
|
+
def _process_const_as_literal(self) -> None:
|
|
200
|
+
"""Process const values by converting to literal type. Used by subclasses."""
|
|
201
|
+
if "const" not in self.extras:
|
|
202
|
+
return
|
|
203
|
+
const = self.extras["const"]
|
|
204
|
+
self.const = True
|
|
205
|
+
self.nullable = False
|
|
206
|
+
self.replace_data_type(self.data_type.__class__(literals=[const]), clear_old_parent=False)
|
|
207
|
+
if not self.default:
|
|
208
|
+
self.default = const
|
|
209
|
+
|
|
210
|
+
def self_reference(self) -> bool:
|
|
211
|
+
"""Check if field references its parent model."""
|
|
212
|
+
if self.parent is None or not self.parent.reference: # pragma: no cover
|
|
213
|
+
return False
|
|
214
|
+
return self.parent.reference.path in {d.reference.path for d in self.data_type.all_data_types if d.reference}
|
|
57
215
|
|
|
58
216
|
@property
|
|
59
|
-
def type_hint(self) -> str:
|
|
217
|
+
def type_hint(self) -> str: # noqa: PLR0911
|
|
218
|
+
"""Get the type hint string for this field, including nullability."""
|
|
60
219
|
type_hint = self.data_type.type_hint
|
|
61
220
|
|
|
62
221
|
if not type_hint:
|
|
63
|
-
return
|
|
64
|
-
|
|
222
|
+
return NONE
|
|
223
|
+
if self.has_default_factory or (self.data_type.is_optional and self.data_type.type != ANY):
|
|
224
|
+
return type_hint
|
|
225
|
+
if self.nullable is not None:
|
|
65
226
|
if self.nullable:
|
|
66
|
-
return
|
|
227
|
+
return get_optional_type(type_hint, self.data_type.use_union_operator)
|
|
67
228
|
return type_hint
|
|
68
|
-
|
|
229
|
+
if self.required:
|
|
230
|
+
if self.type_has_null:
|
|
231
|
+
return get_optional_type(type_hint, self.data_type.use_union_operator)
|
|
69
232
|
return type_hint
|
|
70
|
-
|
|
233
|
+
if self.fall_back_to_nullable:
|
|
234
|
+
return get_optional_type(type_hint, self.data_type.use_union_operator)
|
|
235
|
+
return type_hint
|
|
71
236
|
|
|
72
237
|
@property
|
|
73
|
-
def imports(self) ->
|
|
74
|
-
imports
|
|
75
|
-
|
|
238
|
+
def imports(self) -> tuple[Import, ...]:
|
|
239
|
+
"""Get all imports required for this field's type hint."""
|
|
240
|
+
type_hint = self.type_hint
|
|
241
|
+
has_union = not self.data_type.use_union_operator and UNION_PREFIX in type_hint
|
|
242
|
+
has_optional = OPTIONAL_PREFIX in type_hint
|
|
243
|
+
imports: list[tuple[Import] | Iterator[Import]] = [
|
|
244
|
+
iter(
|
|
245
|
+
i
|
|
246
|
+
for i in self.data_type.all_imports
|
|
247
|
+
if not ((not has_union and i == IMPORT_UNION) or (not has_optional and i == IMPORT_OPTIONAL))
|
|
248
|
+
)
|
|
76
249
|
]
|
|
77
|
-
|
|
250
|
+
|
|
251
|
+
if has_optional:
|
|
78
252
|
imports.append((IMPORT_OPTIONAL,))
|
|
79
|
-
if self.use_annotated:
|
|
253
|
+
if self.use_annotated and self.needs_annotated_import:
|
|
80
254
|
imports.append((IMPORT_ANNOTATED,))
|
|
81
255
|
return chain_as_tuple(*imports)
|
|
82
256
|
|
|
83
257
|
@property
|
|
84
|
-
def
|
|
258
|
+
def docstring(self) -> str | None:
|
|
259
|
+
"""Get the docstring for this field from its description."""
|
|
260
|
+
if self.use_field_description:
|
|
261
|
+
description = self.extras.get("description", None)
|
|
262
|
+
if description is not None:
|
|
263
|
+
return f"{description}"
|
|
264
|
+
elif self.use_inline_field_description:
|
|
265
|
+
# For inline mode, only use multi-line docstring format for multi-line descriptions
|
|
266
|
+
description = self.extras.get("description", None)
|
|
267
|
+
if description is not None and "\n" in description:
|
|
268
|
+
return f"{description}"
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def inline_field_docstring(self) -> str | None:
|
|
273
|
+
"""Get the inline docstring for this field if single-line."""
|
|
274
|
+
if self.use_inline_field_description:
|
|
275
|
+
description = self.extras.get("description", None)
|
|
276
|
+
if description is not None and "\n" not in description:
|
|
277
|
+
return f'"""{description}"""'
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def unresolved_types(self) -> frozenset[str]:
|
|
282
|
+
"""Get the set of unresolved type references."""
|
|
85
283
|
return self.data_type.unresolved_types
|
|
86
284
|
|
|
87
285
|
@property
|
|
88
|
-
def field(self) ->
|
|
89
|
-
"""
|
|
286
|
+
def field(self) -> str | None:
|
|
287
|
+
"""For backwards compatibility."""
|
|
90
288
|
return None
|
|
91
289
|
|
|
92
290
|
@property
|
|
93
|
-
def method(self) ->
|
|
291
|
+
def method(self) -> str | None:
|
|
292
|
+
"""Get the method string for this field, if any."""
|
|
94
293
|
return None
|
|
95
294
|
|
|
96
295
|
@property
|
|
97
296
|
def represented_default(self) -> str:
|
|
297
|
+
"""Get the repr() string of the default value."""
|
|
298
|
+
if isinstance(self.default, set):
|
|
299
|
+
return repr_set_sorted(self.default)
|
|
98
300
|
return repr(self.default)
|
|
99
301
|
|
|
100
302
|
@property
|
|
101
|
-
def annotated(self) ->
|
|
303
|
+
def annotated(self) -> str | None:
|
|
304
|
+
"""Get the Annotated type hint content, if any."""
|
|
102
305
|
return None
|
|
103
306
|
|
|
307
|
+
@property
|
|
308
|
+
def needs_annotated_import(self) -> bool:
|
|
309
|
+
"""Check if this field requires the Annotated import."""
|
|
310
|
+
return bool(self.annotated)
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def needs_meta_import(self) -> bool: # pragma: no cover
|
|
314
|
+
"""Check if this field requires the Meta import (msgspec only)."""
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def has_default_factory(self) -> bool:
|
|
319
|
+
"""Check if this field has a default_factory."""
|
|
320
|
+
return "default_factory" in self.extras
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def fall_back_to_nullable(self) -> bool:
|
|
324
|
+
"""Check if optional fields should be nullable by default."""
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
def copy_deep(self) -> Self:
|
|
328
|
+
"""Create a deep copy of this field to avoid mutating the original."""
|
|
329
|
+
copied = self.copy()
|
|
330
|
+
copied.parent = None
|
|
331
|
+
copied.data_type = self.data_type.copy()
|
|
332
|
+
if self.data_type.data_types:
|
|
333
|
+
copied.data_type.data_types = [dt.copy() for dt in self.data_type.data_types]
|
|
334
|
+
return copied
|
|
335
|
+
|
|
336
|
+
def replace_data_type(self, new_data_type: DataType, *, clear_old_parent: bool = True) -> None:
|
|
337
|
+
"""Replace data_type and update parent relationships.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
new_data_type: The new DataType to set.
|
|
341
|
+
clear_old_parent: If True, clear the old data_type's parent reference.
|
|
342
|
+
Set to False when the old data_type may be referenced elsewhere.
|
|
343
|
+
"""
|
|
344
|
+
if self.data_type.parent is self and clear_old_parent:
|
|
345
|
+
self.data_type.swap_with(new_data_type)
|
|
346
|
+
else:
|
|
347
|
+
self.data_type = new_data_type
|
|
348
|
+
new_data_type.parent = self
|
|
349
|
+
|
|
104
350
|
|
|
105
|
-
@lru_cache
|
|
351
|
+
@lru_cache
|
|
106
352
|
def get_template(template_file_path: Path) -> Template:
|
|
353
|
+
"""Load and cache a Jinja2 template from the template directory."""
|
|
107
354
|
loader = FileSystemLoader(str(TEMPLATE_DIR / template_file_path.parent))
|
|
108
|
-
environment: Environment = Environment(loader=loader)
|
|
355
|
+
environment: Environment = Environment(loader=loader) # noqa: S701
|
|
109
356
|
return environment.get_template(template_file_path.name)
|
|
110
357
|
|
|
111
358
|
|
|
112
|
-
def
|
|
359
|
+
def sanitize_module_name(name: str, *, treat_dot_as_module: bool) -> str:
|
|
360
|
+
"""Sanitize a module name by replacing invalid characters."""
|
|
361
|
+
pattern = r"[^0-9a-zA-Z_.]" if treat_dot_as_module else r"[^0-9a-zA-Z_]"
|
|
362
|
+
sanitized = re.sub(pattern, "_", name)
|
|
363
|
+
if sanitized and sanitized[0].isdigit():
|
|
364
|
+
sanitized = f"_{sanitized}"
|
|
365
|
+
return sanitized
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def get_module_path(name: str, file_path: Path | None, *, treat_dot_as_module: bool) -> list[str]:
|
|
369
|
+
"""Get the module path components from a name and file path."""
|
|
113
370
|
if file_path:
|
|
371
|
+
sanitized_stem = sanitize_module_name(file_path.stem, treat_dot_as_module=treat_dot_as_module)
|
|
114
372
|
return [
|
|
115
373
|
*file_path.parts[:-1],
|
|
116
|
-
|
|
117
|
-
*name.split(
|
|
374
|
+
sanitized_stem,
|
|
375
|
+
*name.split(".")[:-1],
|
|
118
376
|
]
|
|
119
|
-
return name.split(
|
|
377
|
+
return name.split(".")[:-1]
|
|
120
378
|
|
|
121
379
|
|
|
122
|
-
def get_module_name(name: str, file_path:
|
|
123
|
-
|
|
380
|
+
def get_module_name(name: str, file_path: Path | None, *, treat_dot_as_module: bool) -> str:
|
|
381
|
+
"""Get the full module name from a name and file path."""
|
|
382
|
+
return ".".join(get_module_path(name, file_path, treat_dot_as_module=treat_dot_as_module))
|
|
124
383
|
|
|
125
384
|
|
|
126
385
|
class TemplateBase(ABC):
|
|
127
|
-
|
|
386
|
+
"""Abstract base class for template-based code generation."""
|
|
387
|
+
|
|
388
|
+
@cached_property
|
|
128
389
|
@abstractmethod
|
|
129
390
|
def template_file_path(self) -> Path:
|
|
391
|
+
"""Get the path to the template file."""
|
|
130
392
|
raise NotImplementedError
|
|
131
393
|
|
|
132
394
|
@cached_property
|
|
133
395
|
def template(self) -> Template:
|
|
396
|
+
"""Get the cached Jinja2 template instance."""
|
|
134
397
|
return get_template(self.template_file_path)
|
|
135
398
|
|
|
136
399
|
@abstractmethod
|
|
137
400
|
def render(self) -> str:
|
|
401
|
+
"""Render the template to a string."""
|
|
138
402
|
raise NotImplementedError
|
|
139
403
|
|
|
140
404
|
def _render(self, *args: Any, **kwargs: Any) -> str:
|
|
405
|
+
"""Render the template with the given arguments."""
|
|
141
406
|
return self.template.render(*args, **kwargs)
|
|
142
407
|
|
|
143
408
|
def __str__(self) -> str:
|
|
409
|
+
"""Return the rendered template as a string."""
|
|
144
410
|
return self.render()
|
|
145
411
|
|
|
146
412
|
|
|
147
|
-
class BaseClassDataType(DataType):
|
|
148
|
-
|
|
413
|
+
class BaseClassDataType(DataType):
|
|
414
|
+
"""DataType subclass for base class references."""
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
UNDEFINED: Any = object()
|
|
149
418
|
|
|
150
419
|
|
|
151
|
-
class DataModel(TemplateBase, ABC):
|
|
152
|
-
|
|
153
|
-
BASE_CLASS: ClassVar[str] = ''
|
|
154
|
-
DEFAULT_IMPORTS: ClassVar[Tuple[Import, ...]] = ()
|
|
420
|
+
class DataModel(TemplateBase, Nullable, ABC): # noqa: PLR0904
|
|
421
|
+
"""Abstract base class for all data model types.
|
|
155
422
|
|
|
156
|
-
|
|
423
|
+
Handles template rendering, import collection, and model relationships.
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
TEMPLATE_FILE_PATH: ClassVar[str] = ""
|
|
427
|
+
BASE_CLASS: ClassVar[str] = ""
|
|
428
|
+
DEFAULT_IMPORTS: ClassVar[tuple[Import, ...]] = ()
|
|
429
|
+
IS_ALIAS: bool = False
|
|
430
|
+
|
|
431
|
+
def __init__( # noqa: PLR0913
|
|
157
432
|
self,
|
|
158
433
|
*,
|
|
159
434
|
reference: Reference,
|
|
160
|
-
fields:
|
|
161
|
-
decorators:
|
|
162
|
-
base_classes:
|
|
163
|
-
custom_base_class:
|
|
164
|
-
custom_template_dir:
|
|
165
|
-
extra_template_data:
|
|
166
|
-
methods:
|
|
167
|
-
path:
|
|
168
|
-
description:
|
|
435
|
+
fields: list[DataModelFieldBase],
|
|
436
|
+
decorators: list[str] | None = None,
|
|
437
|
+
base_classes: list[Reference] | None = None,
|
|
438
|
+
custom_base_class: str | None = None,
|
|
439
|
+
custom_template_dir: Path | None = None,
|
|
440
|
+
extra_template_data: defaultdict[str, dict[str, Any]] | None = None,
|
|
441
|
+
methods: list[str] | None = None,
|
|
442
|
+
path: Path | None = None,
|
|
443
|
+
description: str | None = None,
|
|
444
|
+
default: Any = UNDEFINED,
|
|
445
|
+
nullable: bool = False,
|
|
446
|
+
keyword_only: bool = False,
|
|
447
|
+
frozen: bool = False,
|
|
448
|
+
treat_dot_as_module: bool = False,
|
|
449
|
+
dataclass_arguments: DataclassArguments | None = None,
|
|
169
450
|
) -> None:
|
|
451
|
+
"""Initialize a data model with fields, base classes, and configuration."""
|
|
452
|
+
self.keyword_only = keyword_only
|
|
453
|
+
self.frozen = frozen
|
|
454
|
+
self.dataclass_arguments: DataclassArguments = dataclass_arguments if dataclass_arguments is not None else {}
|
|
170
455
|
if not self.TEMPLATE_FILE_PATH:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
template_file_path = Path(self.TEMPLATE_FILE_PATH)
|
|
174
|
-
if custom_template_dir is not None:
|
|
175
|
-
custom_template_file_path = custom_template_dir / template_file_path.name
|
|
176
|
-
if custom_template_file_path.exists():
|
|
177
|
-
template_file_path = custom_template_file_path
|
|
178
|
-
self._template_file_path = template_file_path
|
|
456
|
+
msg = "TEMPLATE_FILE_PATH is undefined"
|
|
457
|
+
raise Exception(msg) # noqa: TRY002
|
|
179
458
|
|
|
180
|
-
self.
|
|
181
|
-
self.decorators:
|
|
182
|
-
self._additional_imports:
|
|
459
|
+
self._custom_template_dir: Path | None = custom_template_dir
|
|
460
|
+
self.decorators: list[str] = decorators or []
|
|
461
|
+
self._additional_imports: list[Import] = []
|
|
183
462
|
self.custom_base_class = custom_base_class
|
|
184
463
|
if base_classes:
|
|
185
|
-
self.base_classes:
|
|
186
|
-
BaseClassDataType(reference=b) for b in base_classes
|
|
187
|
-
]
|
|
464
|
+
self.base_classes: list[BaseClassDataType] = [BaseClassDataType(reference=b) for b in base_classes]
|
|
188
465
|
else:
|
|
189
466
|
self.set_base_class()
|
|
190
467
|
|
|
191
|
-
self.file_path:
|
|
468
|
+
self.file_path: Path | None = path
|
|
192
469
|
self.reference: Reference = reference
|
|
193
470
|
|
|
194
471
|
self.reference.source = self
|
|
195
472
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
473
|
+
if extra_template_data is not None:
|
|
474
|
+
# The supplied defaultdict will either create a new entry,
|
|
475
|
+
# or already contain a predefined entry for this type
|
|
476
|
+
self.extra_template_data = extra_template_data[self.reference.path]
|
|
477
|
+
|
|
478
|
+
# We use the full object reference path as dictionary key, but
|
|
479
|
+
# we still support `name` as key because it was used for
|
|
480
|
+
# `--extra-template-data` input file and we don't want to break the
|
|
481
|
+
# existing behavior.
|
|
482
|
+
self.extra_template_data.update(extra_template_data[self.name])
|
|
483
|
+
else:
|
|
484
|
+
self.extra_template_data = defaultdict(dict)
|
|
485
|
+
|
|
486
|
+
self.fields = self._validate_fields(fields) if fields else []
|
|
201
487
|
|
|
202
488
|
for base_class in self.base_classes:
|
|
203
489
|
if base_class.reference:
|
|
204
490
|
base_class.reference.children.append(self)
|
|
205
491
|
|
|
206
|
-
if extra_template_data:
|
|
492
|
+
if extra_template_data is not None:
|
|
207
493
|
all_model_extra_template_data = extra_template_data.get(ALL_MODEL)
|
|
208
494
|
if all_model_extra_template_data:
|
|
209
|
-
|
|
495
|
+
# The deepcopy is needed here to ensure that different models don't
|
|
496
|
+
# end up inadvertently sharing state (such as "base_class_kwargs")
|
|
497
|
+
self.extra_template_data.update(deepcopy(all_model_extra_template_data))
|
|
210
498
|
|
|
211
|
-
self.methods:
|
|
499
|
+
self.methods: list[str] = methods or []
|
|
212
500
|
|
|
213
501
|
self.description = description
|
|
214
502
|
for field in self.fields:
|
|
215
503
|
field.parent = self
|
|
216
504
|
|
|
217
505
|
self._additional_imports.extend(self.DEFAULT_IMPORTS)
|
|
506
|
+
self.default: Any = default
|
|
507
|
+
self._nullable: bool = nullable
|
|
508
|
+
self._treat_dot_as_module: bool = treat_dot_as_module
|
|
509
|
+
|
|
510
|
+
def _validate_fields(self, fields: list[DataModelFieldBase]) -> list[DataModelFieldBase]:
|
|
511
|
+
names: set[str] = set()
|
|
512
|
+
unique_fields: list[DataModelFieldBase] = []
|
|
513
|
+
for field in fields:
|
|
514
|
+
if field.name:
|
|
515
|
+
if field.name in names:
|
|
516
|
+
warn(f"Field name `{field.name}` is duplicated on {self.name}", stacklevel=2)
|
|
517
|
+
continue
|
|
518
|
+
names.add(field.name)
|
|
519
|
+
unique_fields.append(field)
|
|
520
|
+
return unique_fields
|
|
521
|
+
|
|
522
|
+
def iter_all_fields(self, visited: set[str] | None = None) -> Iterator[DataModelFieldBase]:
|
|
523
|
+
"""Yield all fields including those from base classes (parent fields first)."""
|
|
524
|
+
if visited is None:
|
|
525
|
+
visited = set()
|
|
526
|
+
if self.reference.path in visited: # pragma: no cover
|
|
527
|
+
return
|
|
528
|
+
visited.add(self.reference.path)
|
|
529
|
+
for base_class in self.base_classes:
|
|
530
|
+
if base_class.reference and isinstance(base_class.reference.source, DataModel):
|
|
531
|
+
yield from base_class.reference.source.iter_all_fields(visited)
|
|
532
|
+
yield from self.fields
|
|
533
|
+
|
|
534
|
+
def get_dedup_key(self, class_name: str | None = None, *, use_default: bool = True) -> tuple[Any, ...]:
|
|
535
|
+
"""Generate hashable key for model deduplication."""
|
|
536
|
+
from datamodel_code_generator.parser.base import to_hashable # noqa: PLC0415
|
|
537
|
+
|
|
538
|
+
render_class_name = class_name if class_name is not None or not use_default else "M"
|
|
539
|
+
return tuple(to_hashable(v) for v in (self.render(class_name=render_class_name), self.imports))
|
|
540
|
+
|
|
541
|
+
def create_reuse_model(self, base_ref: Reference) -> Self:
|
|
542
|
+
"""Create inherited model with empty fields pointing to base reference."""
|
|
543
|
+
return self.__class__(
|
|
544
|
+
fields=[],
|
|
545
|
+
base_classes=[base_ref],
|
|
546
|
+
description=self.description,
|
|
547
|
+
reference=Reference(
|
|
548
|
+
name=self.name,
|
|
549
|
+
path=self.reference.path + "/reuse",
|
|
550
|
+
),
|
|
551
|
+
custom_template_dir=self._custom_template_dir,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
def replace_children_in_models(self, models: list[DataModel], new_ref: Reference) -> None:
|
|
555
|
+
"""Replace reference children if their parent model is in models list."""
|
|
556
|
+
from datamodel_code_generator.parser.base import get_most_of_parent # noqa: PLC0415
|
|
557
|
+
|
|
558
|
+
for child in self.reference.children[:]:
|
|
559
|
+
if isinstance(child, DataType) and get_most_of_parent(child) in models:
|
|
560
|
+
child.replace_reference(new_ref)
|
|
218
561
|
|
|
219
562
|
def set_base_class(self) -> None:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
563
|
+
"""Set up the base class for this model."""
|
|
564
|
+
base_class = self.custom_base_class or self.BASE_CLASS
|
|
565
|
+
if not base_class:
|
|
566
|
+
self.base_classes = []
|
|
567
|
+
return
|
|
568
|
+
base_class_import = Import.from_full_path(base_class)
|
|
223
569
|
self._additional_imports.append(base_class_import)
|
|
224
570
|
self.base_classes = [BaseClassDataType.from_import(base_class_import)]
|
|
225
571
|
|
|
226
|
-
@
|
|
572
|
+
@cached_property
|
|
227
573
|
def template_file_path(self) -> Path:
|
|
228
|
-
|
|
574
|
+
"""Get the path to the template file, checking custom directory first."""
|
|
575
|
+
template_file_path = Path(self.TEMPLATE_FILE_PATH)
|
|
576
|
+
if self._custom_template_dir is not None:
|
|
577
|
+
custom_template_file_path = self._custom_template_dir / template_file_path
|
|
578
|
+
if custom_template_file_path.exists():
|
|
579
|
+
return custom_template_file_path
|
|
580
|
+
return template_file_path
|
|
229
581
|
|
|
230
582
|
@property
|
|
231
|
-
def imports(self) ->
|
|
583
|
+
def imports(self) -> tuple[Import, ...]:
|
|
584
|
+
"""Get all imports required by this model and its fields."""
|
|
232
585
|
return chain_as_tuple(
|
|
233
586
|
(i for f in self.fields for i in f.imports),
|
|
234
587
|
self._additional_imports,
|
|
235
588
|
)
|
|
236
589
|
|
|
237
590
|
@property
|
|
238
|
-
def reference_classes(self) ->
|
|
591
|
+
def reference_classes(self) -> frozenset[str]:
|
|
592
|
+
"""Get all referenced class paths used by this model."""
|
|
239
593
|
return frozenset(
|
|
240
594
|
{r.reference.path for r in self.base_classes if r.reference}
|
|
241
595
|
| {t for f in self.fields for t in f.unresolved_types}
|
|
@@ -243,44 +597,101 @@ class DataModel(TemplateBase, ABC):
|
|
|
243
597
|
|
|
244
598
|
@property
|
|
245
599
|
def name(self) -> str:
|
|
600
|
+
"""Get the full name of this model."""
|
|
246
601
|
return self.reference.name
|
|
247
602
|
|
|
603
|
+
@property
|
|
604
|
+
def duplicate_name(self) -> str:
|
|
605
|
+
"""Get the duplicate name for this model if it exists."""
|
|
606
|
+
return self.reference.duplicate_name or ""
|
|
607
|
+
|
|
248
608
|
@property
|
|
249
609
|
def base_class(self) -> str:
|
|
250
|
-
|
|
610
|
+
"""Get the comma-separated string of base class names."""
|
|
611
|
+
return ", ".join(b.type_hint for b in self.base_classes)
|
|
612
|
+
|
|
613
|
+
@staticmethod
|
|
614
|
+
def _get_class_name(name: str) -> str:
|
|
615
|
+
if "." in name:
|
|
616
|
+
return name.rsplit(".", 1)[-1]
|
|
617
|
+
return name
|
|
251
618
|
|
|
252
619
|
@property
|
|
253
620
|
def class_name(self) -> str:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
621
|
+
"""Get the class name without module path."""
|
|
622
|
+
return self._get_class_name(self.name)
|
|
623
|
+
|
|
624
|
+
@class_name.setter
|
|
625
|
+
def class_name(self, class_name: str) -> None:
|
|
626
|
+
if "." in self.reference.name:
|
|
627
|
+
self.reference.name = f"{self.reference.name.rsplit('.', 1)[0]}.{class_name}"
|
|
628
|
+
else:
|
|
629
|
+
self.reference.name = class_name
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def duplicate_class_name(self) -> str:
|
|
633
|
+
"""Get the duplicate class name without module path."""
|
|
634
|
+
return self._get_class_name(self.duplicate_name)
|
|
257
635
|
|
|
258
636
|
@property
|
|
259
|
-
def module_path(self) ->
|
|
260
|
-
|
|
637
|
+
def module_path(self) -> list[str]:
|
|
638
|
+
"""Get the module path components for this model."""
|
|
639
|
+
return get_module_path(self.name, self.file_path, treat_dot_as_module=self._treat_dot_as_module)
|
|
261
640
|
|
|
262
641
|
@property
|
|
263
642
|
def module_name(self) -> str:
|
|
264
|
-
|
|
643
|
+
"""Get the full module name for this model."""
|
|
644
|
+
return get_module_name(self.name, self.file_path, treat_dot_as_module=self._treat_dot_as_module)
|
|
265
645
|
|
|
266
646
|
@property
|
|
267
|
-
def all_data_types(self) -> Iterator[
|
|
647
|
+
def all_data_types(self) -> Iterator[DataType]:
|
|
648
|
+
"""Iterate over all data types used in this model."""
|
|
268
649
|
for field in self.fields:
|
|
269
650
|
yield from field.data_type.all_data_types
|
|
270
651
|
yield from self.base_classes
|
|
271
652
|
|
|
653
|
+
@property
|
|
654
|
+
def is_alias(self) -> bool:
|
|
655
|
+
"""Whether is a type alias (i.e. not an instance of BaseModel/RootModel)."""
|
|
656
|
+
return self.IS_ALIAS
|
|
657
|
+
|
|
658
|
+
@property
|
|
659
|
+
def nullable(self) -> bool:
|
|
660
|
+
"""Check if this model is nullable."""
|
|
661
|
+
return self._nullable
|
|
662
|
+
|
|
272
663
|
@cached_property
|
|
273
664
|
def path(self) -> str:
|
|
665
|
+
"""Get the full reference path for this model."""
|
|
274
666
|
return self.reference.path
|
|
275
667
|
|
|
276
|
-
def
|
|
277
|
-
|
|
278
|
-
|
|
668
|
+
def set_reference_path(self, new_path: str) -> None:
|
|
669
|
+
"""Set reference path and clear cached path property."""
|
|
670
|
+
self.reference.path = new_path
|
|
671
|
+
if "path" in self.__dict__:
|
|
672
|
+
del self.__dict__["path"]
|
|
673
|
+
|
|
674
|
+
def render(self, *, class_name: str | None = None) -> str:
|
|
675
|
+
"""Render the model to a string using the template."""
|
|
676
|
+
return self._render(
|
|
677
|
+
class_name=class_name or self.class_name,
|
|
279
678
|
fields=self.fields,
|
|
280
679
|
decorators=self.decorators,
|
|
281
680
|
base_class=self.base_class,
|
|
282
681
|
methods=self.methods,
|
|
283
682
|
description=self.description,
|
|
683
|
+
dataclass_arguments=self.dataclass_arguments,
|
|
284
684
|
**self.extra_template_data,
|
|
285
685
|
)
|
|
286
|
-
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
if PYDANTIC_V2:
|
|
689
|
+
_rebuild_namespace = {"Union": Union, "DataModelFieldBase": DataModelFieldBase, "DataType": DataType}
|
|
690
|
+
DataType.model_rebuild(_types_namespace=_rebuild_namespace)
|
|
691
|
+
BaseClassDataType.model_rebuild(_types_namespace=_rebuild_namespace)
|
|
692
|
+
DataModelFieldBase.model_rebuild(_types_namespace={"DataModel": DataModel})
|
|
693
|
+
else:
|
|
694
|
+
_rebuild_namespace = {"Union": Union, "DataModelFieldBase": DataModelFieldBase, "DataType": DataType}
|
|
695
|
+
DataType.model_rebuild(_types_namespace=_rebuild_namespace)
|
|
696
|
+
BaseClassDataType.model_rebuild(_types_namespace=_rebuild_namespace)
|
|
697
|
+
DataModelFieldBase.model_rebuild(_types_namespace={"DataModel": DataModel})
|