Skip to content

Commit f3f3912

Browse files
authored
Add deprecated decorators for dataclass output (#3044)
* Add deprecated decorators for dataclass output * Fix deprecated dataclass metadata handling * Fix deprecated import for existing decorators * Add E2E coverage for deprecated dataclass decorators
1 parent 569894a commit f3f3912

10 files changed

Lines changed: 187 additions & 7 deletions

File tree

src/datamodel_code_generator/model/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,15 @@ def create_reuse_model(self, base_ref: Reference) -> Self:
741741
treat_dot_as_module=self._treat_dot_as_module,
742742
)
743743

744+
def _set_deprecated_decorator(self) -> None:
745+
"""Add a class-level deprecated decorator when schema metadata requires it."""
746+
if not self.extra_template_data.get("deprecated"):
747+
return
748+
if not any(decorator.startswith("@deprecated") for decorator in self.decorators):
749+
message = f"{self.class_name} is deprecated."
750+
self.decorators = [*self.decorators, f"@deprecated({message!r})"]
751+
self._additional_imports.append(Import.from_full_path("typing_extensions.deprecated"))
752+
744753
def replace_children_in_models(self, models: list[DataModel], new_ref: Reference) -> None:
745754
"""Replace reference children if their parent model is in models list."""
746755
from datamodel_code_generator.parser.base import get_most_of_parent # noqa: PLC0415

src/datamodel_code_generator/model/dataclass.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def __init__( # noqa: PLR0913
9797
self.dataclass_arguments["frozen"] = True
9898
if keyword_only:
9999
self.dataclass_arguments["kw_only"] = True
100+
self._set_deprecated_decorator()
100101

101102
def create_reuse_model(self, base_ref: Reference) -> DataClass:
102103
"""Create inherited model with empty fields pointing to base reference."""

src/datamodel_code_generator/model/pydantic_v2/dataclass.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def __init__( # noqa: PLR0913
9595
self.dataclass_arguments["frozen"] = True
9696
if keyword_only:
9797
self.dataclass_arguments["kw_only"] = True
98+
self._set_deprecated_decorator()
9899

99100
config_parameters: dict[str, Any] = {}
100101

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,7 @@ def _create_variant_model( # noqa: PLR0913, PLR0917
11391139
unique_name = self.model_resolver.get_class_name(variant_name, unique=True).name
11401140
model_path = [*path[:-1], unique_name]
11411141
reference = self.model_resolver.add(model_path, unique_name, class_name=True, unique=False, loaded=True)
1142+
self._set_schema_metadata(reference.path, obj)
11421143
self.set_schema_extensions(reference.path, obj)
11431144
model = self._create_data_model(
11441145
model_type=data_model_type_class,
@@ -1427,6 +1428,12 @@ def _set_schema_metadata(self, path: str, obj: JsonSchemaObject) -> None:
14271428
self.set_schema_id(path, obj)
14281429
self.set_additional_properties(path, obj)
14291430
self.set_unevaluated_properties(path, obj)
1431+
self.set_deprecated(path, obj)
1432+
1433+
def set_deprecated(self, path: str, obj: JsonSchemaObject) -> None:
1434+
"""Set deprecated flag in extra template data."""
1435+
if obj.extras.get("deprecated") is True:
1436+
self.extra_template_data[path]["deprecated"] = True
14301437

14311438
def set_schema_extensions(self, path: str, obj: JsonSchemaObject) -> None:
14321439
"""Set schema extensions (x-* fields) in extra template data."""
@@ -2492,9 +2499,7 @@ def _parse_object_common_part( # noqa: PLR0912, PLR0913, PLR0915
24922499
)
24932500
name = self._apply_title_as_name(name, obj) # pragma: no cover
24942501
reference = self.model_resolver.add(path, name, class_name=True, loaded=True)
2495-
self.set_additional_properties(reference.path, obj)
2496-
self.set_unevaluated_properties(reference.path, obj)
2497-
self.set_schema_id(reference.path, obj)
2502+
self._set_schema_metadata(reference.path, obj)
24982503
self.set_schema_extensions(reference.path, obj)
24992504

25002505
generates_separate = self._should_generate_separate_models(fields, base_classes)
@@ -2657,6 +2662,7 @@ def parse_all_of(
26572662
existing_ref = self.model_resolver.references.get(full_path)
26582663
if existing_ref is not None and not existing_ref.loaded:
26592664
reference = self.model_resolver.add(path, name, class_name=True, loaded=True)
2665+
self._set_schema_metadata(reference.path, obj)
26602666
self.set_schema_extensions(reference.path, obj)
26612667
field = self.data_model_field_type(
26622668
name=None,
@@ -2711,6 +2717,7 @@ def parse_all_of(
27112717
required=required,
27122718
)
27132719
reference = self.model_resolver.add(path, name, class_name=True, loaded=True)
2720+
self._set_schema_metadata(reference.path, obj)
27142721
self.set_schema_extensions(reference.path, obj)
27152722
all_of_data_type = self._parse_object_common_part(
27162723
name,
@@ -2906,8 +2913,7 @@ def parse_object(
29062913
)
29072914
data_model_type_class = self.data_model_root_type
29082915

2909-
self.set_additional_properties(reference.path, obj)
2910-
self.set_unevaluated_properties(reference.path, obj)
2916+
self._set_schema_metadata(reference.path, obj)
29112917
self.set_schema_extensions(reference.path, obj)
29122918

29132919
generates_separate = self._should_generate_separate_models(fields, None)
@@ -3368,6 +3374,7 @@ def parse_array(
33683374
"""Parse array schema into a root model with array type."""
33693375
name = self._apply_title_as_name(name, obj)
33703376
reference = self.model_resolver.add(path, name, loaded=True, class_name=True)
3377+
self._set_schema_metadata(reference.path, obj)
33713378
self.set_schema_extensions(reference.path, obj)
33723379
field = self.parse_array_fields(original_name or name, obj, [*path, name])
33733380

@@ -3613,7 +3620,7 @@ def _get_field_name_from_dict_enum(cls, enum_part: dict[str, Any], index: int) -
36133620
return str(enum_part["const"])
36143621
return f"value_{index}"
36153622

3616-
def parse_enum(
3623+
def parse_enum( # noqa: PLR0915
36173624
self,
36183625
name: str,
36193626
obj: JsonSchemaObject,
@@ -3690,6 +3697,7 @@ def parse_enum(
36903697
loaded=True,
36913698
model_type="enum",
36923699
)
3700+
self._set_schema_metadata(reference.path, obj)
36933701
self.set_schema_extensions(reference.path, obj)
36943702
data_model_root_type = self.data_model_root_type(
36953703
reference=reference,
@@ -3761,11 +3769,12 @@ def create_enum(reference_: Reference) -> DataType:
37613769
loaded=True,
37623770
model_type="enum",
37633771
)
3772+
self._set_schema_metadata(reference.path, obj)
3773+
self.set_schema_extensions(reference.path, obj)
37643774

37653775
if not nullable:
37663776
return create_enum(reference)
37673777

3768-
self.set_schema_extensions(reference.path, obj)
37693778
enum_reference = self.model_resolver.add(
37703779
[*path, "Enum"],
37713780
f"{reference.name}Enum",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# generated by datamodel-codegen:
2+
# filename: deprecated_dataclass.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from dataclasses import dataclass
8+
9+
from typing_extensions import deprecated
10+
11+
12+
@deprecated('LegacyUser is deprecated.')
13+
@dataclass
14+
class LegacyUser:
15+
name: str
16+
email: str | None = None
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# generated by datamodel-codegen:
2+
# filename: deprecated_dataclass.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from dataclasses import dataclass
8+
9+
from some_module import some_decorator
10+
from typing_extensions import deprecated
11+
12+
13+
@some_decorator
14+
@deprecated('LegacyUser is deprecated.')
15+
@dataclass
16+
class LegacyUser:
17+
name: str
18+
email: str | None = None
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# generated by datamodel-codegen:
2+
# filename: deprecated_dataclass.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic.dataclasses import dataclass
8+
from typing_extensions import deprecated
9+
10+
11+
@deprecated('LegacyUser is deprecated.')
12+
@dataclass
13+
class LegacyUser:
14+
name: str
15+
email: str | None = None
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# generated by datamodel-codegen:
2+
# filename: deprecated_dataclass.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic.dataclasses import dataclass
8+
from some_module import some_decorator
9+
from typing_extensions import deprecated
10+
11+
12+
@some_decorator
13+
@deprecated('LegacyUser is deprecated.')
14+
@dataclass
15+
class LegacyUser:
16+
name: str
17+
email: str | None = None
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "LegacyUser",
4+
"type": "object",
5+
"deprecated": true,
6+
"properties": {
7+
"name": {
8+
"type": "string"
9+
},
10+
"email": {
11+
"type": "string"
12+
}
13+
},
14+
"required": ["name"]
15+
}

tests/main/jsonschema/test_main_jsonschema.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3963,6 +3963,85 @@ def test_main_dataclass_field(output_file: Path) -> None:
39633963
)
39643964

39653965

3966+
def test_main_dataclass_deprecated_model(output_file: Path) -> None:
3967+
"""Test dataclass generation with deprecated schema metadata."""
3968+
run_main_and_assert(
3969+
input_path=JSON_SCHEMA_DATA_PATH / "deprecated_dataclass.json",
3970+
output_path=output_file,
3971+
input_file_type="jsonschema",
3972+
assert_func=assert_file_content,
3973+
expected_file="deprecated_dataclass.py",
3974+
extra_args=["--output-model-type", "dataclasses.dataclass"],
3975+
)
3976+
3977+
3978+
def test_main_dataclass_deprecated_model_preserves_existing_decorator(output_file: Path) -> None:
3979+
"""Test deprecated dataclass generation keeps the import with an existing decorator."""
3980+
run_main_and_assert(
3981+
input_path=JSON_SCHEMA_DATA_PATH / "deprecated_dataclass.json",
3982+
output_path=output_file,
3983+
input_file_type="jsonschema",
3984+
assert_func=assert_file_content,
3985+
expected_file="deprecated_dataclass.py",
3986+
extra_args=[
3987+
"--output-model-type",
3988+
"dataclasses.dataclass",
3989+
"--class-decorators",
3990+
"@deprecated('LegacyUser is deprecated.')",
3991+
],
3992+
)
3993+
3994+
3995+
def test_main_dataclass_deprecated_model_with_other_decorator(output_file: Path) -> None:
3996+
"""Test deprecated dataclass generation adds deprecation alongside other decorators."""
3997+
run_main_and_assert(
3998+
input_path=JSON_SCHEMA_DATA_PATH / "deprecated_dataclass.json",
3999+
output_path=output_file,
4000+
input_file_type="jsonschema",
4001+
assert_func=assert_file_content,
4002+
expected_file="deprecated_dataclass_with_other_decorator.py",
4003+
extra_args=[
4004+
"--output-model-type",
4005+
"dataclasses.dataclass",
4006+
"--class-decorators",
4007+
"@some_decorator",
4008+
"--additional-imports",
4009+
"some_module.some_decorator",
4010+
],
4011+
)
4012+
4013+
4014+
def test_main_pydantic_v2_dataclass_deprecated_model(output_file: Path) -> None:
4015+
"""Test pydantic v2 dataclass generation with deprecated schema metadata."""
4016+
run_main_and_assert(
4017+
input_path=JSON_SCHEMA_DATA_PATH / "deprecated_dataclass.json",
4018+
output_path=output_file,
4019+
input_file_type="jsonschema",
4020+
assert_func=assert_file_content,
4021+
expected_file="deprecated_pydantic_v2_dataclass.py",
4022+
extra_args=["--output-model-type", "pydantic_v2.dataclass"],
4023+
)
4024+
4025+
4026+
def test_main_pydantic_v2_dataclass_deprecated_model_with_other_decorator(output_file: Path) -> None:
4027+
"""Test pydantic v2 dataclass generation adds deprecation alongside other decorators."""
4028+
run_main_and_assert(
4029+
input_path=JSON_SCHEMA_DATA_PATH / "deprecated_dataclass.json",
4030+
output_path=output_file,
4031+
input_file_type="jsonschema",
4032+
assert_func=assert_file_content,
4033+
expected_file="deprecated_pydantic_v2_dataclass_with_other_decorator.py",
4034+
extra_args=[
4035+
"--output-model-type",
4036+
"pydantic_v2.dataclass",
4037+
"--class-decorators",
4038+
"@some_decorator",
4039+
"--additional-imports",
4040+
"some_module.some_decorator",
4041+
],
4042+
)
4043+
4044+
39664045
@pytest.mark.skipif(
39674046
not is_supported_in_black(PythonVersion.PY_312),
39684047
reason="Black does not support Python 3.12",

0 commit comments

Comments
 (0)