Skip to content

Commit 189fb45

Browse files
ICEPower420pre-commit-ci[bot]gaborbernatilovelinuxkoxudaxi
authored
add dataclass arguments (#2437)
* Feature/dataclass arguments (#1) Added dataclass_arguments and tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix ci (#2) Co-authored-by: Khaled Radmal <khaled> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix ci (#3) Co-authored-by: Khaled Radmal <khaled> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * updated readme (#4) Co-authored-by: Khaled Radmal <khaled> * Fix/ci failed (#5) * fix ci * added # noqa: UP045 --------- Co-authored-by: Khaled Radmal <khaled> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * default encoding: utf-8 * Update docs/index.md Co-authored-by: Antonio Spadaro <ilovelinux@users.noreply.github.com> * Tests for jsonschema * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Import Types from jsonschema parser in test_param.py * Refactor data type assignment in test_param.py to use built-in str type * Refactor dataclass argument handling in _create_data_model and add generated dataclass definitions for Star Wars entities * Refactor dataclass argument handling in _create_data_model for improved readability * Add tests for Pydantic model handling in GraphQL and JSON Schema code generation * Add test for GraphQL code generation with frozen and keyword-only dataclass * Refactor dataclass argument handling to simplify merging of existing arguments * Remove .venv/ from .gitignore to allow virtual environment tracking * Enhance dataclass_arguments handling in data model creation for improved flexibility and compatibility * feat: update dataclass_arguments type to DataclassArguments for improved type safety * refactor: simplify frozen_dataclasses tests with parameterization * feat: update JSON parsing to use DataclassArguments for improved validation * fix: cast result to DataclassArguments for type safety in argument parsing * test: add parameterized tests for _create_data_model in GraphQLParser --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Khaled Radmal <khaled> Co-authored-by: Bernát Gábor <gaborjbernat@gmail.com> Co-authored-by: Antonio Spadaro <ilovelinux@users.noreply.github.com> Co-authored-by: Koudai Aono <koxudaxi@gmail.com>
1 parent 61ae0da commit 189fb45

22 files changed

Lines changed: 1036 additions & 61 deletions

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,10 @@ Model customization:
450450
--collapse-root-models
451451
Models generated with a root-type field will be merged into the
452452
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.
453457
--disable-appending-item-suffix
454458
Disable appending `Item` suffix to model name in an array
455459
--disable-timestamp Disable timestamp on file headers

docs/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,10 @@ Model customization:
442442
--collapse-root-models
443443
Models generated with a root-type field will be merged into the
444444
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.
445449
--disable-appending-item-suffix
446450
Disable appending `Item` suffix to model name in an array
447451
--disable-timestamp Disable timestamp on file headers

src/datamodel_code_generator/__init__.py

Lines changed: 26 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:
@@ -340,6 +356,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
340356
no_alias: bool = False,
341357
formatters: list[Formatter] = DEFAULT_FORMATTERS,
342358
parent_scoped_naming: bool = False,
359+
dataclass_arguments: DataclassArguments | None = None,
343360
disable_future_imports: bool = False,
344361
type_mappings: list[str] | None = None,
345362
) -> None:
@@ -361,6 +378,13 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
361378
else:
362379
input_text = None
363380

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+
364388
if isinstance(input_, Path) and not input_.is_absolute():
365389
input_ = input_.expanduser().resolve()
366390
if input_file_type == InputFileType.Auto:
@@ -562,6 +586,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
562586
formatters=formatters,
563587
encoding=encoding,
564588
parent_scoped_naming=parent_scoped_naming,
589+
dataclass_arguments=dataclass_arguments,
565590
type_mappings=type_mappings,
566591
**kwargs,
567592
)

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/template/dataclass.jinja2

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
{% for decorator in decorators -%}
22
{{ decorator }}
33
{% endfor -%}
4+
{%- set args = [] %}
5+
{%- for k, v in (dataclass_arguments or {}).items() %}
6+
{%- if v is not none and v is not false %}
7+
{%- set _ = args.append(k ~ '=' ~ (v|pprint)) %}
8+
{%- endif %}
9+
{%- endfor %}
10+
{%- if args %}
11+
@dataclass({{ args | join(', ') }})
12+
{%- else %}
413
@dataclass
5-
{%- if keyword_only or frozen -%}
6-
(
7-
{%- if keyword_only -%}kw_only=True{%- endif -%}
8-
{%- if keyword_only and frozen -%}, {% endif -%}
9-
{%- if frozen -%}frozen=True{%- endif -%}
10-
)
1114
{%- endif %}
1215
{%- if base_class %}
1316
class {{ class_name }}({{ base_class }}):

src/datamodel_code_generator/parser/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
if TYPE_CHECKING:
5656
from collections.abc import Iterable, Iterator, Mapping, Sequence
5757

58+
from datamodel_code_generator import DataclassArguments
59+
5860

5961
@runtime_checkable
6062
class HashableComparable(Hashable, Protocol):
@@ -429,6 +431,7 @@ def __init__( # noqa: PLR0913, PLR0915
429431
no_alias: bool = False,
430432
formatters: list[Formatter] = DEFAULT_FORMATTERS,
431433
parent_scoped_naming: bool = False,
434+
dataclass_arguments: DataclassArguments | None = None,
432435
type_mappings: list[str] | None = None,
433436
) -> None:
434437
"""Initialize the Parser with configuration options."""
@@ -487,6 +490,7 @@ def __init__( # noqa: PLR0913, PLR0915
487490
self.use_title_as_name: bool = use_title_as_name
488491
self.use_operation_id_as_name: bool = use_operation_id_as_name
489492
self.use_unique_items_as_set: bool = use_unique_items_as_set
493+
self.dataclass_arguments = dataclass_arguments
490494

491495
if base_path:
492496
self.base_path = base_path

src/datamodel_code_generator/parser/graphql.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from urllib.parse import ParseResult
1616

1717
from datamodel_code_generator import (
18+
DataclassArguments,
1819
DefaultPutDict,
1920
LiteralType,
2021
PythonVersion,
@@ -173,6 +174,7 @@ def __init__( # noqa: PLR0913
173174
no_alias: bool = False,
174175
formatters: list[Formatter] = DEFAULT_FORMATTERS,
175176
parent_scoped_naming: bool = False,
177+
dataclass_arguments: DataclassArguments | None = None,
176178
type_mappings: list[str] | None = None,
177179
) -> None:
178180
"""Initialize the GraphQL parser with configuration options."""
@@ -255,6 +257,7 @@ def __init__( # noqa: PLR0913
255257
no_alias=no_alias,
256258
formatters=formatters,
257259
parent_scoped_naming=parent_scoped_naming,
260+
dataclass_arguments=dataclass_arguments,
258261
type_mappings=type_mappings,
259262
)
260263

@@ -310,10 +313,26 @@ def _resolve_types(self, paths: list[str], schema: graphql.GraphQLSchema) -> Non
310313
self.support_graphql_types[resolved_type].append(type_)
311314

312315
def _create_data_model(self, model_type: type[DataModel] | None = None, **kwargs: Any) -> DataModel:
313-
"""Create data model instance with conditional frozen parameter for DataClass."""
316+
"""Create data model instance with dataclass_arguments support for DataClass."""
314317
data_model_class = model_type or self.data_model_type
315318
if issubclass(data_model_class, DataClass):
316-
kwargs["frozen"] = self.frozen_dataclasses
319+
# Use dataclass_arguments from kwargs, or fall back to self.dataclass_arguments
320+
# If both are None, construct from legacy frozen_dataclasses/keyword_only flags
321+
dataclass_arguments = kwargs.pop("dataclass_arguments", None)
322+
if dataclass_arguments is None:
323+
dataclass_arguments = self.dataclass_arguments
324+
if dataclass_arguments is None:
325+
# Construct from legacy flags for library API compatibility
326+
dataclass_arguments = {}
327+
if self.frozen_dataclasses:
328+
dataclass_arguments["frozen"] = True
329+
if self.keyword_only:
330+
dataclass_arguments["kw_only"] = True
331+
kwargs["dataclass_arguments"] = dataclass_arguments
332+
kwargs.pop("frozen", None)
333+
kwargs.pop("keyword_only", None)
334+
else:
335+
kwargs.pop("dataclass_arguments", None)
317336
return data_model_class(**kwargs)
318337

319338
def _typename_field(self, name: str) -> DataModelFieldBase:
@@ -505,6 +524,7 @@ def parse_object_like(
505524
description=obj.description,
506525
keyword_only=self.keyword_only,
507526
treat_dot_as_module=self.treat_dot_as_module,
527+
dataclass_arguments=self.dataclass_arguments,
508528
)
509529
self.results.append(data_model_type)
510530

0 commit comments

Comments
 (0)