Skip to content

Commit 8e43f3d

Browse files
authored
gh-145056: Add support for frozendict in dataclass asdict and astuple (#145125)
1 parent be833e6 commit 8e43f3d

4 files changed

Lines changed: 28 additions & 11 deletions

File tree

Doc/library/dataclasses.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,8 @@ Module contents
371371
Converts the dataclass *obj* to a dict (by using the
372372
factory function *dict_factory*). Each dataclass is converted
373373
to a dict of its fields, as ``name: value`` pairs. dataclasses, dicts,
374-
lists, and tuples are recursed into. Other objects are copied with
375-
:func:`copy.deepcopy`.
374+
frozendicts, lists, and tuples are recursed into. Other objects are copied
375+
with :func:`copy.deepcopy`.
376376

377377
Example of using :func:`!asdict` on nested dataclasses::
378378

@@ -402,8 +402,8 @@ Module contents
402402

403403
Converts the dataclass *obj* to a tuple (by using the
404404
factory function *tuple_factory*). Each dataclass is converted
405-
to a tuple of its field values. dataclasses, dicts, lists, and
406-
tuples are recursed into. Other objects are copied with
405+
to a tuple of its field values. dataclasses, dicts, frozendicts, lists,
406+
and tuples are recursed into. Other objects are copied with
407407
:func:`copy.deepcopy`.
408408

409409
Continuing from the previous example::

Lib/dataclasses.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,7 +1496,8 @@ class C:
14961496
If given, 'dict_factory' will be used instead of built-in dict.
14971497
The function applies recursively to field values that are
14981498
dataclass instances. This will also look into built-in containers:
1499-
tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'.
1499+
tuples, lists, dicts, and frozendicts. Other objects are copied
1500+
with 'copy.deepcopy()'.
15001501
"""
15011502
if not _is_dataclass_instance(obj):
15021503
raise TypeError("asdict() should be called on dataclass instances")
@@ -1552,7 +1553,7 @@ def _asdict_inner(obj, dict_factory):
15521553
return obj_type(*[_asdict_inner(v, dict_factory) for v in obj])
15531554
else:
15541555
return obj_type(_asdict_inner(v, dict_factory) for v in obj)
1555-
elif issubclass(obj_type, dict):
1556+
elif issubclass(obj_type, (dict, frozendict)):
15561557
if hasattr(obj_type, 'default_factory'):
15571558
# obj is a defaultdict, which has a different constructor from
15581559
# dict as it requires the default_factory as its first arg.
@@ -1587,7 +1588,8 @@ class C:
15871588
If given, 'tuple_factory' will be used instead of built-in tuple.
15881589
The function applies recursively to field values that are
15891590
dataclass instances. This will also look into built-in containers:
1590-
tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'.
1591+
tuples, lists, dicts, and frozendicts. Other objects are copied
1592+
with 'copy.deepcopy()'.
15911593
"""
15921594

15931595
if not _is_dataclass_instance(obj):
@@ -1616,7 +1618,7 @@ def _astuple_inner(obj, tuple_factory):
16161618
# generator (which is not true for namedtuples, handled
16171619
# above).
16181620
return type(obj)(_astuple_inner(v, tuple_factory) for v in obj)
1619-
elif isinstance(obj, dict):
1621+
elif isinstance(obj, (dict, frozendict)):
16201622
obj_type = type(obj)
16211623
if hasattr(obj_type, 'default_factory'):
16221624
# obj is a defaultdict, which has a different constructor from

Lib/test/test_dataclasses/__init__.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,17 +1693,24 @@ class GroupTuple:
16931693
class GroupDict:
16941694
id: int
16951695
users: Dict[str, User]
1696+
@dataclass
1697+
class GroupFrozenDict:
1698+
id: int
1699+
users: frozendict[str, User]
16961700
a = User('Alice', 1)
16971701
b = User('Bob', 2)
16981702
gl = GroupList(0, [a, b])
16991703
gt = GroupTuple(0, (a, b))
17001704
gd = GroupDict(0, {'first': a, 'second': b})
1705+
gfd = GroupFrozenDict(0, frozendict({'first': a, 'second': b}))
17011706
self.assertEqual(asdict(gl), {'id': 0, 'users': [{'name': 'Alice', 'id': 1},
17021707
{'name': 'Bob', 'id': 2}]})
17031708
self.assertEqual(asdict(gt), {'id': 0, 'users': ({'name': 'Alice', 'id': 1},
17041709
{'name': 'Bob', 'id': 2})})
1705-
self.assertEqual(asdict(gd), {'id': 0, 'users': {'first': {'name': 'Alice', 'id': 1},
1706-
'second': {'name': 'Bob', 'id': 2}}})
1710+
expected_dict = {'id': 0, 'users': {'first': {'name': 'Alice', 'id': 1},
1711+
'second': {'name': 'Bob', 'id': 2}}}
1712+
self.assertEqual(asdict(gd), expected_dict)
1713+
self.assertEqual(asdict(gfd), expected_dict)
17071714

17081715
def test_helper_asdict_builtin_object_containers(self):
17091716
@dataclass
@@ -1884,14 +1891,21 @@ class GroupTuple:
18841891
class GroupDict:
18851892
id: int
18861893
users: Dict[str, User]
1894+
@dataclass
1895+
class GroupFrozenDict:
1896+
id: int
1897+
users: frozendict[str, User]
18871898
a = User('Alice', 1)
18881899
b = User('Bob', 2)
18891900
gl = GroupList(0, [a, b])
18901901
gt = GroupTuple(0, (a, b))
18911902
gd = GroupDict(0, {'first': a, 'second': b})
1903+
gfd = GroupFrozenDict(0, frozendict({'first': a, 'second': b}))
18921904
self.assertEqual(astuple(gl), (0, [('Alice', 1), ('Bob', 2)]))
18931905
self.assertEqual(astuple(gt), (0, (('Alice', 1), ('Bob', 2))))
1894-
self.assertEqual(astuple(gd), (0, {'first': ('Alice', 1), 'second': ('Bob', 2)}))
1906+
d = {'first': ('Alice', 1), 'second': ('Bob', 2)}
1907+
self.assertEqual(astuple(gd), (0, d))
1908+
self.assertEqual(astuple(gfd), (0, frozendict(d)))
18951909

18961910
def test_helper_astuple_builtin_object_containers(self):
18971911
@dataclass
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for :class:`frozendict` in :meth:`dataclasses.asdict` and :meth:`dataclasses.astuple`.

0 commit comments

Comments
 (0)