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.
Files changed (73) hide show
  1. datamodel_code_generator/__init__.py +654 -185
  2. datamodel_code_generator/__main__.py +872 -388
  3. datamodel_code_generator/arguments.py +798 -0
  4. datamodel_code_generator/cli_options.py +295 -0
  5. datamodel_code_generator/format.py +292 -54
  6. datamodel_code_generator/http.py +85 -10
  7. datamodel_code_generator/imports.py +152 -43
  8. datamodel_code_generator/model/__init__.py +138 -1
  9. datamodel_code_generator/model/base.py +531 -120
  10. datamodel_code_generator/model/dataclass.py +211 -0
  11. datamodel_code_generator/model/enum.py +133 -12
  12. datamodel_code_generator/model/imports.py +22 -0
  13. datamodel_code_generator/model/msgspec.py +462 -0
  14. datamodel_code_generator/model/pydantic/__init__.py +30 -25
  15. datamodel_code_generator/model/pydantic/base_model.py +304 -100
  16. datamodel_code_generator/model/pydantic/custom_root_type.py +11 -2
  17. datamodel_code_generator/model/pydantic/dataclass.py +15 -4
  18. datamodel_code_generator/model/pydantic/imports.py +40 -27
  19. datamodel_code_generator/model/pydantic/types.py +188 -96
  20. datamodel_code_generator/model/pydantic_v2/__init__.py +51 -0
  21. datamodel_code_generator/model/pydantic_v2/base_model.py +268 -0
  22. datamodel_code_generator/model/pydantic_v2/imports.py +15 -0
  23. datamodel_code_generator/model/pydantic_v2/root_model.py +35 -0
  24. datamodel_code_generator/model/pydantic_v2/types.py +143 -0
  25. datamodel_code_generator/model/scalar.py +124 -0
  26. datamodel_code_generator/model/template/Enum.jinja2 +15 -2
  27. datamodel_code_generator/model/template/ScalarTypeAliasAnnotation.jinja2 +6 -0
  28. datamodel_code_generator/model/template/ScalarTypeAliasType.jinja2 +6 -0
  29. datamodel_code_generator/model/template/ScalarTypeStatement.jinja2 +6 -0
  30. datamodel_code_generator/model/template/TypeAliasAnnotation.jinja2 +20 -0
  31. datamodel_code_generator/model/template/TypeAliasType.jinja2 +20 -0
  32. datamodel_code_generator/model/template/TypeStatement.jinja2 +20 -0
  33. datamodel_code_generator/model/template/TypedDict.jinja2 +5 -0
  34. datamodel_code_generator/model/template/TypedDictClass.jinja2 +25 -0
  35. datamodel_code_generator/model/template/TypedDictFunction.jinja2 +24 -0
  36. datamodel_code_generator/model/template/UnionTypeAliasAnnotation.jinja2 +10 -0
  37. datamodel_code_generator/model/template/UnionTypeAliasType.jinja2 +10 -0
  38. datamodel_code_generator/model/template/UnionTypeStatement.jinja2 +10 -0
  39. datamodel_code_generator/model/template/dataclass.jinja2 +50 -0
  40. datamodel_code_generator/model/template/msgspec.jinja2 +55 -0
  41. datamodel_code_generator/model/template/pydantic/BaseModel.jinja2 +17 -4
  42. datamodel_code_generator/model/template/pydantic/BaseModel_root.jinja2 +12 -4
  43. datamodel_code_generator/model/template/pydantic/Config.jinja2 +1 -1
  44. datamodel_code_generator/model/template/pydantic/dataclass.jinja2 +15 -2
  45. datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 +57 -0
  46. datamodel_code_generator/model/template/pydantic_v2/ConfigDict.jinja2 +5 -0
  47. datamodel_code_generator/model/template/pydantic_v2/RootModel.jinja2 +48 -0
  48. datamodel_code_generator/model/type_alias.py +70 -0
  49. datamodel_code_generator/model/typed_dict.py +161 -0
  50. datamodel_code_generator/model/types.py +106 -0
  51. datamodel_code_generator/model/union.py +105 -0
  52. datamodel_code_generator/parser/__init__.py +30 -12
  53. datamodel_code_generator/parser/_graph.py +67 -0
  54. datamodel_code_generator/parser/_scc.py +171 -0
  55. datamodel_code_generator/parser/base.py +2426 -380
  56. datamodel_code_generator/parser/graphql.py +652 -0
  57. datamodel_code_generator/parser/jsonschema.py +2518 -647
  58. datamodel_code_generator/parser/openapi.py +631 -222
  59. datamodel_code_generator/py.typed +0 -0
  60. datamodel_code_generator/pydantic_patch.py +28 -0
  61. datamodel_code_generator/reference.py +672 -290
  62. datamodel_code_generator/types.py +521 -145
  63. datamodel_code_generator/util.py +155 -0
  64. datamodel_code_generator/watch.py +65 -0
  65. datamodel_code_generator-0.45.0.dist-info/METADATA +301 -0
  66. datamodel_code_generator-0.45.0.dist-info/RECORD +69 -0
  67. {datamodel_code_generator-0.11.12.dist-info → datamodel_code_generator-0.45.0.dist-info}/WHEEL +1 -1
  68. datamodel_code_generator-0.45.0.dist-info/entry_points.txt +2 -0
  69. datamodel_code_generator/version.py +0 -1
  70. datamodel_code_generator-0.11.12.dist-info/METADATA +0 -440
  71. datamodel_code_generator-0.11.12.dist-info/RECORD +0 -31
  72. datamodel_code_generator-0.11.12.dist-info/entry_points.txt +0 -3
  73. {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 functools import lru_cache
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
- Any,
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 BaseModel
21
-
22
- from datamodel_code_generator import cached_property
23
- from datamodel_code_generator.imports import IMPORT_ANNOTATED, IMPORT_OPTIONAL, 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 DataType, chain_as_tuple
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
- TEMPLATE_DIR: Path = Path(__file__).parents[0] / 'template'
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
- ALL_MODEL: str = '#all#'
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
- class ConstraintsBase(BaseModel):
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
- name: Optional[str]
40
- default: Optional[Any]
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[Any] = None
48
- extras: Dict[str, Any] = {}
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
- _exclude_fields: ClassVar[Set[str]] = {'parent'}
51
- _pass_fields: ClassVar[Set[str]] = {'parent', 'data_type'}
52
-
53
- def __init__(self, **data: Any): # type: ignore
54
- super().__init__(**data)
55
- if self.data_type.reference or self.data_type.data_types:
56
- self.data_type.parent = self
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 OPTIONAL
64
- elif self.nullable is not None:
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 f'{OPTIONAL}[{type_hint}]'
227
+ return get_optional_type(type_hint, self.data_type.use_union_operator)
67
228
  return type_hint
68
- elif self.required:
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
- return f'{OPTIONAL}[{type_hint}]'
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) -> Tuple[Import, ...]:
74
- imports: List[Union[Tuple[Import], Iterator[Import]]] = [
75
- self.data_type.all_imports
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
- if self.nullable or (self.nullable is None and not self.required):
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 unresolved_types(self) -> FrozenSet[str]:
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) -> Optional[str]:
89
- """for backwards compatibility"""
286
+ def field(self) -> str | None:
287
+ """For backwards compatibility."""
90
288
  return None
91
289
 
92
290
  @property
93
- def method(self) -> Optional[str]:
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) -> Optional[str]:
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 get_module_path(name: str, file_path: Optional[Path]) -> List[str]:
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
- file_path.stem,
117
- *name.split('.')[:-1],
374
+ sanitized_stem,
375
+ *name.split(".")[:-1],
118
376
  ]
119
- return name.split('.')[:-1]
377
+ return name.split(".")[:-1]
120
378
 
121
379
 
122
- def get_module_name(name: str, file_path: Optional[Path]) -> str:
123
- return '.'.join(get_module_path(name, file_path))
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
- @property
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): # type: ignore
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
- TEMPLATE_FILE_PATH: ClassVar[str] = ''
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
- def __init__(
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: List[DataModelFieldBase],
161
- decorators: Optional[List[str]] = None,
162
- base_classes: Optional[List[Reference]] = None,
163
- custom_base_class: Optional[str] = None,
164
- custom_template_dir: Optional[Path] = None,
165
- extra_template_data: Optional[DefaultDict[str, Dict[str, Any]]] = None,
166
- methods: Optional[List[str]] = None,
167
- path: Optional[Path] = None,
168
- description: Optional[str] = None,
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
- raise Exception('TEMPLATE_FILE_PATH is undefined')
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.fields: List[DataModelFieldBase] = fields or []
181
- self.decorators: List[str] = decorators or []
182
- self._additional_imports: List[Import] = []
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: List[BaseClassDataType] = [
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: Optional[Path] = 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
- self.extra_template_data = (
197
- extra_template_data[self.name]
198
- if extra_template_data is not None
199
- else defaultdict(dict)
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
- self.extra_template_data.update(all_model_extra_template_data)
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: List[str] = methods or []
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
- base_class_import = Import.from_full_path(
221
- self.custom_base_class or self.BASE_CLASS
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
- @property
572
+ @cached_property
227
573
  def template_file_path(self) -> Path:
228
- return self._template_file_path
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) -> Tuple[Import, ...]:
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) -> FrozenSet[str]:
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
- return ', '.join(b.type_hint for b in self.base_classes)
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
- if '.' in self.name:
255
- return self.name.rsplit('.', 1)[-1]
256
- return self.name
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) -> List[str]:
260
- return get_module_path(self.name, self.file_path)
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
- return get_module_name(self.name, self.file_path)
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['DataType']:
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 render(self) -> str:
277
- response = self._render(
278
- class_name=self.class_name,
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
- return response
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})