Skip to content

Commit eb05935

Browse files
authored
Merge branch 'main' into required-nullable-annotated
2 parents a4d26d3 + 189fb45 commit eb05935

62 files changed

Lines changed: 2089 additions & 262 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
.*_cache
33
__pycache__
44
coverage.xml
5+
.coverage
6+
.idea/
7+
.vscode/

README.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -252,17 +252,11 @@ class Apis(BaseModel):
252252
<table>
253253
<tr>
254254
<td valign="top" align="center">
255-
<a href="https://github.com/JetBrainsOfficial">
256-
<img src="https://avatars.githubusercontent.com/u/60931315?s=100&v=4" alt="JetBrains Logo" style="width: 100px;">
257-
<p>JetBrains</p>
258-
</a>
255+
<a href="https://github.com/astral-sh">
256+
<img src="https://avatars.githubusercontent.com/u/115962839?s=200&v=4" alt="Astral Logo" style="width: 100px;">
257+
<p>Astral</p>
258+
</a>
259259
</td>
260-
<td valign="top" align="center">
261-
<a href="https://github.com/astral-sh">
262-
<img src="https://avatars.githubusercontent.com/u/115962839?s=200&v=4" alt="Astral Logo" style="width: 100px;">
263-
<p>Astral</p>
264-
</a>
265-
</td>
266260
</tr>
267261
</table>
268262
@@ -456,6 +450,10 @@ Model customization:
456450
--collapse-root-models
457451
Models generated with a root-type field will be merged into the
458452
models using that root-type model
453+
--dataclass-arguments DATACLASS_ARGUMENTS
454+
Custom dataclass arguments as a JSON dictionary, e.g. ''{"frozen":
455+
true, "kw_only": true}''. Overrides --frozen-dataclasses and similar
456+
flags.
459457
--disable-appending-item-suffix
460458
Disable appending `Item` suffix to model name in an array
461459
--disable-timestamp Disable timestamp on file headers
@@ -518,7 +516,7 @@ OpenAPI-only options:
518516
--include-path-parameters
519517
Include path parameters in generated parameter models in addition to
520518
query parameters (Only OpenAPI)
521-
--openapi-scopes {schemas,paths,tags,parameters} [{schemas,paths,tags,parameters} ...]
519+
--openapi-scopes {schemas,paths,tags,parameters,webhooks} [{schemas,paths,tags,parameters,webhooks} ...]
522520
Scopes of OpenAPI model generation (default: schemas)
523521
--strict-nullable Treat default field as a non-nullable field (Only OpenAPI)
524522
--use-operation-id-as-name

docs/index.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -248,17 +248,11 @@ class Apis(BaseModel):
248248
<table>
249249
<tr>
250250
<td valign="top" align="center">
251-
<a href="https://github.com/JetBrainsOfficial">
252-
<img src="https://avatars.githubusercontent.com/u/60931315?s=100&v=4" alt="JetBrains Logo" style="width: 100px;">
253-
<p>JetBrains</p>
254-
</a>
251+
<a href="https://github.com/astral-sh">
252+
<img src="https://avatars.githubusercontent.com/u/115962839?s=200&v=4" alt="Astral Logo" style="width: 100px;">
253+
<p>Astral</p>
254+
</a>
255255
</td>
256-
<td valign="top" align="center">
257-
<a href="https://github.com/astral-sh">
258-
<img src="https://avatars.githubusercontent.com/u/115962839?s=200&v=4" alt="Astral Logo" style="width: 100px;">
259-
<p>Astral</p>
260-
</a>
261-
</td>
262256
</tr>
263257
</table>
264258
## Projects that use datamodel-code-generator
@@ -448,6 +442,10 @@ Model customization:
448442
--collapse-root-models
449443
Models generated with a root-type field will be merged into the
450444
models using that root-type model
445+
--dataclass-arguments DATACLASS_ARGUMENTS
446+
Custom dataclass arguments as a JSON dictionary, e.g. ''{"frozen":
447+
true, "kw_only": true}''. Overrides --frozen-dataclasses and similar
448+
flags.
451449
--disable-appending-item-suffix
452450
Disable appending `Item` suffix to model name in an array
453451
--disable-timestamp Disable timestamp on file headers
@@ -510,7 +508,7 @@ OpenAPI-only options:
510508
--include-path-parameters
511509
Include path parameters in generated parameter models in addition to
512510
query parameters (Only OpenAPI)
513-
--openapi-scopes {schemas,paths,tags,parameters} [{schemas,paths,tags,parameters} ...]
511+
--openapi-scopes {schemas,paths,tags,parameters,webhooks} [{schemas,paths,tags,parameters,webhooks} ...]
514512
Scopes of OpenAPI model generation (default: schemas)
515513
--strict-nullable Treat default field as a non-nullable field (Only OpenAPI)
516514
--use-operation-id-as-name

src/datamodel_code_generator/__init__.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
import yaml
3030
import yaml.parser
31-
from typing_extensions import TypeAlias, TypeAliasType
31+
from typing_extensions import TypeAlias, TypeAliasType, TypedDict
3232

3333
import datamodel_code_generator.pydantic_patch # noqa: F401
3434
from datamodel_code_generator.format import (
@@ -56,6 +56,22 @@
5656

5757
T = TypeVar("T")
5858

59+
60+
class DataclassArguments(TypedDict, total=False):
61+
"""Arguments for @dataclass decorator."""
62+
63+
init: bool
64+
repr: bool
65+
eq: bool
66+
order: bool
67+
unsafe_hash: bool
68+
frozen: bool
69+
match_args: bool
70+
kw_only: bool
71+
slots: bool
72+
weakref_slot: bool
73+
74+
5975
if not TYPE_CHECKING:
6076
YamlScalar: TypeAlias = Union[str, int, float, bool, None]
6177
if PYDANTIC_V2:
@@ -215,6 +231,7 @@ class OpenAPIScope(Enum):
215231
Paths = "paths"
216232
Tags = "tags"
217233
Parameters = "parameters"
234+
Webhooks = "webhooks"
218235

219236

220237
class GraphQLScope(Enum):
@@ -339,6 +356,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
339356
no_alias: bool = False,
340357
formatters: list[Formatter] = DEFAULT_FORMATTERS,
341358
parent_scoped_naming: bool = False,
359+
dataclass_arguments: DataclassArguments | None = None,
342360
disable_future_imports: bool = False,
343361
type_mappings: list[str] | None = None,
344362
) -> None:
@@ -360,6 +378,13 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
360378
else:
361379
input_text = None
362380

381+
if dataclass_arguments is None:
382+
dataclass_arguments = {}
383+
if frozen_dataclasses:
384+
dataclass_arguments["frozen"] = True
385+
if keyword_only:
386+
dataclass_arguments["kw_only"] = True
387+
363388
if isinstance(input_, Path) and not input_.is_absolute():
364389
input_ = input_.expanduser().resolve()
365390
if input_file_type == InputFileType.Auto:
@@ -561,6 +586,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
561586
formatters=formatters,
562587
encoding=encoding,
563588
parent_scoped_naming=parent_scoped_naming,
589+
dataclass_arguments=dataclass_arguments,
564590
type_mappings=type_mappings,
565591
**kwargs,
566592
)

src/datamodel_code_generator/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from typing_extensions import TypeAlias
2020

2121
from datamodel_code_generator import (
22+
DataclassArguments,
2223
DataModelType,
2324
Error,
2425
InputFileType,
@@ -402,6 +403,7 @@ def validate_root(cls, values: dict[str, Any]) -> dict[str, Any]: # noqa: N805
402403
output_datetime_class: Optional[DatetimeClassType] = None # noqa: UP045
403404
keyword_only: bool = False
404405
frozen_dataclasses: bool = False
406+
dataclass_arguments: Optional[DataclassArguments] = None # noqa: UP045
405407
no_alias: bool = False
406408
formatters: list[Formatter] = DEFAULT_FORMATTERS
407409
parent_scoped_naming: bool = False
@@ -660,6 +662,7 @@ def main(args: Sequence[str] | None = None) -> Exit: # noqa: PLR0911, PLR0912,
660662
no_alias=config.no_alias,
661663
formatters=config.formatters,
662664
parent_scoped_naming=config.parent_scoped_naming,
665+
dataclass_arguments=config.dataclass_arguments,
663666
disable_future_imports=config.disable_future_imports,
664667
type_mappings=config.type_mappings,
665668
)

src/datamodel_code_generator/arguments.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77

88
from __future__ import annotations
99

10+
import json
1011
import locale
11-
from argparse import ArgumentParser, BooleanOptionalAction, HelpFormatter, Namespace
12+
from argparse import ArgumentParser, ArgumentTypeError, BooleanOptionalAction, HelpFormatter, Namespace
1213
from operator import attrgetter
1314
from pathlib import Path
14-
from typing import TYPE_CHECKING
15+
from typing import TYPE_CHECKING, cast
1516

16-
from datamodel_code_generator import DataModelType, InputFileType, OpenAPIScope
17+
from datamodel_code_generator import DataclassArguments, DataModelType, InputFileType, OpenAPIScope
1718
from datamodel_code_generator.format import DatetimeClassType, Formatter, PythonVersion
1819
from datamodel_code_generator.model.pydantic_v2 import UnionMode
1920
from datamodel_code_generator.parser import LiteralType
@@ -28,6 +29,28 @@
2829
namespace = Namespace(no_color=False)
2930

3031

32+
def _dataclass_arguments(value: str) -> DataclassArguments:
33+
"""Parse JSON string and validate it as DataclassArguments."""
34+
try:
35+
result = json.loads(value)
36+
except json.JSONDecodeError as e:
37+
msg = f"Invalid JSON: {e}"
38+
raise ArgumentTypeError(msg) from e
39+
if not isinstance(result, dict):
40+
msg = f"Expected a JSON dictionary, got {type(result).__name__}"
41+
raise ArgumentTypeError(msg)
42+
valid_keys = set(DataclassArguments.__annotations__.keys())
43+
invalid_keys = set(result.keys()) - valid_keys
44+
if invalid_keys:
45+
msg = f"Invalid keys: {invalid_keys}. Valid keys are: {valid_keys}"
46+
raise ArgumentTypeError(msg)
47+
for key, val in result.items():
48+
if not isinstance(val, bool):
49+
msg = f"Expected bool for '{key}', got {type(val).__name__}"
50+
raise ArgumentTypeError(msg)
51+
return cast("DataclassArguments", result)
52+
53+
3154
class SortingHelpFormatter(HelpFormatter):
3255
"""Help formatter that sorts arguments and adds color to section headers."""
3356

@@ -179,6 +202,16 @@ def start_section(self, heading: str | None) -> None:
179202
action="store_true",
180203
default=None,
181204
)
205+
model_options.add_argument(
206+
"--dataclass-arguments",
207+
type=_dataclass_arguments,
208+
default=None,
209+
help=(
210+
"Custom dataclass arguments as a JSON dictionary, "
211+
'e.g. \'{"frozen": true, "kw_only": true}\'. '
212+
"Overrides --frozen-dataclasses and similar flags."
213+
),
214+
)
182215
model_options.add_argument(
183216
"--reuse-model",
184217
help="Reuse models on the field when a module has the model with the same content",

src/datamodel_code_generator/model/base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
if TYPE_CHECKING:
4040
from collections.abc import Iterator
4141

42+
from datamodel_code_generator import DataclassArguments
43+
4244
TEMPLATE_DIR: Path = Path(__file__).parents[0] / "template"
4345

4446
ALL_MODEL: str = "#all#"
@@ -357,10 +359,12 @@ def __init__( # noqa: PLR0913
357359
keyword_only: bool = False,
358360
frozen: bool = False,
359361
treat_dot_as_module: bool = False,
362+
dataclass_arguments: DataclassArguments | None = None,
360363
) -> None:
361364
"""Initialize a data model with fields, base classes, and configuration."""
362365
self.keyword_only = keyword_only
363366
self.frozen = frozen
367+
self.dataclass_arguments: DataclassArguments = dataclass_arguments if dataclass_arguments is not None else {}
364368
if not self.TEMPLATE_FILE_PATH:
365369
msg = "TEMPLATE_FILE_PATH is undefined"
366370
raise Exception(msg) # noqa: TRY002
@@ -538,8 +542,7 @@ def render(self, *, class_name: str | None = None) -> str:
538542
base_class=self.base_class,
539543
methods=self.methods,
540544
description=self.description,
541-
keyword_only=self.keyword_only,
542-
frozen=self.frozen,
545+
dataclass_arguments=self.dataclass_arguments,
543546
**self.extra_template_data,
544547
)
545548

src/datamodel_code_generator/model/dataclass.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from typing import TYPE_CHECKING, Any, ClassVar, Optional
99

10-
from datamodel_code_generator import DatetimeClassType, PythonVersion, PythonVersionMin
10+
from datamodel_code_generator import DataclassArguments, DatetimeClassType, PythonVersion, PythonVersionMin
1111
from datamodel_code_generator.imports import (
1212
IMPORT_DATE,
1313
IMPORT_DATETIME,
@@ -61,6 +61,7 @@ def __init__( # noqa: PLR0913
6161
keyword_only: bool = False,
6262
frozen: bool = False,
6363
treat_dot_as_module: bool = False,
64+
dataclass_arguments: DataclassArguments | None = None,
6465
) -> None:
6566
"""Initialize dataclass with fields sorted by field assignment requirement."""
6667
super().__init__(
@@ -80,6 +81,14 @@ def __init__( # noqa: PLR0913
8081
frozen=frozen,
8182
treat_dot_as_module=treat_dot_as_module,
8283
)
84+
if dataclass_arguments is not None:
85+
self.dataclass_arguments = dataclass_arguments
86+
else:
87+
self.dataclass_arguments = {}
88+
if frozen:
89+
self.dataclass_arguments["frozen"] = True
90+
if keyword_only:
91+
self.dataclass_arguments["kw_only"] = True
8392

8493

8594
class DataModelField(DataModelFieldBase):

src/datamodel_code_generator/model/imports.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@
1818
IMPORT_MSGSPEC_FIELD = Import.from_full_path("msgspec.field")
1919
IMPORT_MSGSPEC_META = Import.from_full_path("msgspec.Meta")
2020
IMPORT_MSGSPEC_CONVERT = Import.from_full_path("msgspec.convert")
21+
IMPORT_MSGSPEC_UNSET = Import.from_full_path("msgspec.UNSET")
22+
IMPORT_MSGSPEC_UNSETTYPE = Import.from_full_path("msgspec.UnsetType")

0 commit comments

Comments
 (0)