Skip to content

Commit b72471d

Browse files
authored
Fix --use-unique-items-as-set to output set literals for default values (#2672)
* Add support for unique items as set literals in generated models * Add support for unique items as set literals in generated models
1 parent b458594 commit b72471d

11 files changed

Lines changed: 154 additions & 4 deletions

File tree

src/datamodel_code_generator/model/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@
4848

4949
ALL_MODEL: str = "#all#"
5050

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+
5165
ConstraintsBaseT = TypeVar("ConstraintsBaseT", bound="ConstraintsBase")
5266
DataModelFieldBaseT = TypeVar("DataModelFieldBaseT", bound="DataModelFieldBase")
5367

@@ -281,6 +295,8 @@ def method(self) -> str | None:
281295
@property
282296
def represented_default(self) -> str:
283297
"""Get the repr() string of the default value."""
298+
if isinstance(self.default, set):
299+
return repr_set_sorted(self.default)
284300
return repr(self.default)
285301

286302
@property

src/datamodel_code_generator/model/dataclass.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,13 @@ def __str__(self) -> str:
150150
if len(data) == 1 and "default" in data:
151151
default = data["default"]
152152

153-
if isinstance(default, (list, dict)):
154-
return f"field(default_factory=lambda :{default!r})"
153+
if isinstance(default, (list, dict, set)):
154+
if default:
155+
from datamodel_code_generator.model.base import repr_set_sorted # noqa: PLC0415
156+
157+
default_repr = repr_set_sorted(default) if isinstance(default, set) else repr(default)
158+
return f"field(default_factory=lambda: {default_repr})"
159+
return f"field(default_factory={type(default).__name__})"
155160
return repr(default)
156161
kwargs = [f"{k}={v if k == 'default_factory' else repr(v)}" for k, v in data.items()]
157162
return f"field({', '.join(kwargs)})"

src/datamodel_code_generator/model/msgspec.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,10 @@ def __str__(self) -> str:
277277
if "default" in data and isinstance(data["default"], (list, dict, set)) and "default_factory" not in data:
278278
default_value = data.pop("default")
279279
if default_value:
280-
data["default_factory"] = f"lambda: {default_value!r}"
280+
from datamodel_code_generator.model.base import repr_set_sorted # noqa: PLC0415
281+
282+
default_repr = repr_set_sorted(default_value) if isinstance(default_value, set) else repr(default_value)
283+
data["default_factory"] = f"lambda: {default_repr}"
281284
else:
282285
data["default_factory"] = type(default_value).__name__
283286

src/datamodel_code_generator/model/pydantic/base_model.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,10 @@ def __str__(self) -> str: # noqa: PLR0912
221221
elif self.required:
222222
field_arguments = ["...", *field_arguments]
223223
elif not default_factory:
224-
field_arguments = [f"{self.default!r}", *field_arguments]
224+
from datamodel_code_generator.model.base import repr_set_sorted # noqa: PLC0415
225+
226+
default_repr = repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default)
227+
field_arguments = [default_repr, *field_arguments]
225228

226229
return f"Field({', '.join(field_arguments)})"
227230

src/datamodel_code_generator/parser/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,15 @@ def __replace_unique_list_to_set(self, models: list[DataModel]) -> None:
13901390
continue
13911391
set_data_type = self._create_set_from_list(model_field.data_type)
13921392
if set_data_type: # pragma: no cover
1393+
# Check if default list elements are hashable before converting type
1394+
if isinstance(model_field.default, list):
1395+
try:
1396+
converted_default = set(model_field.default)
1397+
except TypeError:
1398+
# Elements are not hashable (e.g., contains dicts)
1399+
# Skip both type and default conversion to keep consistency
1400+
continue
1401+
model_field.default = converted_default
13931402
model_field.replace_data_type(set_data_type)
13941403

13951404
@classmethod
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: unique_items_default_set.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from dataclasses import dataclass, field
8+
from typing import Optional, Set
9+
10+
11+
@dataclass
12+
class TestModel:
13+
tags: Optional[Set[str]] = field(default_factory=lambda: {'tag1', 'tag2'})
14+
empty_tags: Optional[Set[str]] = field(default_factory=set)
15+
numbers: Optional[Set[int]] = field(default_factory=lambda: {1, 2, 3})
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: unique_items_default_set.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Set, Union
8+
9+
from msgspec import Struct, UnsetType, field
10+
11+
12+
class TestModel(Struct):
13+
tags: Union[Set[str], UnsetType] = field(default_factory=lambda: {'tag1', 'tag2'})
14+
empty_tags: Union[Set[str], UnsetType] = field(default_factory=set)
15+
numbers: Union[Set[int], UnsetType] = field(default_factory=lambda: {1, 2, 3})
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: unique_items_default_set.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional, Set
8+
9+
from pydantic import BaseModel, Field
10+
11+
12+
class TestModel(BaseModel):
13+
tags: Optional[Set[str]] = Field({'tag1', 'tag2'}, unique_items=True)
14+
empty_tags: Optional[Set[str]] = Field(set(), unique_items=True)
15+
numbers: Optional[Set[int]] = Field({1, 2, 3}, unique_items=True)
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: unique_items_default_set.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional, Set
8+
9+
from pydantic import BaseModel
10+
11+
12+
class TestModel(BaseModel):
13+
tags: Optional[Set[str]] = {'tag1', 'tag2'}
14+
empty_tags: Optional[Set[str]] = set()
15+
numbers: Optional[Set[int]] = {1, 2, 3}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
openapi: "3.0.0"
2+
info:
3+
title: Test API - Unique Items Default as Set
4+
version: 1.0.0
5+
paths: {}
6+
components:
7+
schemas:
8+
TestModel:
9+
type: object
10+
properties:
11+
tags:
12+
type: array
13+
items:
14+
type: string
15+
uniqueItems: true
16+
default:
17+
- tag1
18+
- tag2
19+
empty_tags:
20+
type: array
21+
items:
22+
type: string
23+
uniqueItems: true
24+
default: []
25+
numbers:
26+
type: array
27+
items:
28+
type: integer
29+
uniqueItems: true
30+
default:
31+
- 1
32+
- 2
33+
- 3

0 commit comments

Comments
 (0)