Skip to content

Commit 9b9d97d

Browse files
authored
bpo-34363: dataclasses.asdict() and .astuple() now handle fields which are namedtuples. (GH-9151)
1 parent 73820a6 commit 9b9d97d

File tree

3 files changed

+118
-2
lines changed

3 files changed

+118
-2
lines changed

Diff for: Lib/dataclasses.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -1026,11 +1026,36 @@ def _asdict_inner(obj, dict_factory):
10261026
value = _asdict_inner(getattr(obj, f.name), dict_factory)
10271027
result.append((f.name, value))
10281028
return dict_factory(result)
1029+
elif isinstance(obj, tuple) and hasattr(obj, '_fields'):
1030+
# obj is a namedtuple. Recurse into it, but the returned
1031+
# object is another namedtuple of the same type. This is
1032+
# similar to how other list- or tuple-derived classes are
1033+
# treated (see below), but we just need to create them
1034+
# differently because a namedtuple's __init__ needs to be
1035+
# called differently (see bpo-34363).
1036+
1037+
# I'm not using namedtuple's _asdict()
1038+
# method, because:
1039+
# - it does not recurse in to the namedtuple fields and
1040+
# convert them to dicts (using dict_factory).
1041+
# - I don't actually want to return a dict here. The the main
1042+
# use case here is json.dumps, and it handles converting
1043+
# namedtuples to lists. Admittedly we're losing some
1044+
# information here when we produce a json list instead of a
1045+
# dict. Note that if we returned dicts here instead of
1046+
# namedtuples, we could no longer call asdict() on a data
1047+
# structure where a namedtuple was used as a dict key.
1048+
1049+
return type(obj)(*[_asdict_inner(v, dict_factory) for v in obj])
10291050
elif isinstance(obj, (list, tuple)):
1051+
# Assume we can create an object of this type by passing in a
1052+
# generator (which is not true for namedtuples, handled
1053+
# above).
10301054
return type(obj)(_asdict_inner(v, dict_factory) for v in obj)
10311055
elif isinstance(obj, dict):
1032-
return type(obj)((_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory))
1033-
for k, v in obj.items())
1056+
return type(obj)((_asdict_inner(k, dict_factory),
1057+
_asdict_inner(v, dict_factory))
1058+
for k, v in obj.items())
10341059
else:
10351060
return copy.deepcopy(obj)
10361061

@@ -1066,7 +1091,18 @@ def _astuple_inner(obj, tuple_factory):
10661091
value = _astuple_inner(getattr(obj, f.name), tuple_factory)
10671092
result.append(value)
10681093
return tuple_factory(result)
1094+
elif isinstance(obj, tuple) and hasattr(obj, '_fields'):
1095+
# obj is a namedtuple. Recurse into it, but the returned
1096+
# object is another namedtuple of the same type. This is
1097+
# similar to how other list- or tuple-derived classes are
1098+
# treated (see below), but we just need to create them
1099+
# differently because a namedtuple's __init__ needs to be
1100+
# called differently (see bpo-34363).
1101+
return type(obj)(*[_astuple_inner(v, tuple_factory) for v in obj])
10691102
elif isinstance(obj, (list, tuple)):
1103+
# Assume we can create an object of this type by passing in a
1104+
# generator (which is not true for namedtuples, handled
1105+
# above).
10701106
return type(obj)(_astuple_inner(v, tuple_factory) for v in obj)
10711107
elif isinstance(obj, dict):
10721108
return type(obj)((_astuple_inner(k, tuple_factory), _astuple_inner(v, tuple_factory))

Diff for: Lib/test/test_dataclasses.py

+79
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,70 @@ class C:
14291429
self.assertEqual(d, OrderedDict([('x', 42), ('y', 2)]))
14301430
self.assertIs(type(d), OrderedDict)
14311431

1432+
def test_helper_asdict_namedtuple(self):
1433+
T = namedtuple('T', 'a b c')
1434+
@dataclass
1435+
class C:
1436+
x: str
1437+
y: T
1438+
c = C('outer', T(1, C('inner', T(11, 12, 13)), 2))
1439+
1440+
d = asdict(c)
1441+
self.assertEqual(d, {'x': 'outer',
1442+
'y': T(1,
1443+
{'x': 'inner',
1444+
'y': T(11, 12, 13)},
1445+
2),
1446+
}
1447+
)
1448+
1449+
# Now with a dict_factory. OrderedDict is convenient, but
1450+
# since it compares to dicts, we also need to have separate
1451+
# assertIs tests.
1452+
d = asdict(c, dict_factory=OrderedDict)
1453+
self.assertEqual(d, {'x': 'outer',
1454+
'y': T(1,
1455+
{'x': 'inner',
1456+
'y': T(11, 12, 13)},
1457+
2),
1458+
}
1459+
)
1460+
1461+
# Make sure that the returned dicts are actuall OrderedDicts.
1462+
self.assertIs(type(d), OrderedDict)
1463+
self.assertIs(type(d['y'][1]), OrderedDict)
1464+
1465+
def test_helper_asdict_namedtuple_key(self):
1466+
# Ensure that a field that contains a dict which has a
1467+
# namedtuple as a key works with asdict().
1468+
1469+
@dataclass
1470+
class C:
1471+
f: dict
1472+
T = namedtuple('T', 'a')
1473+
1474+
c = C({T('an a'): 0})
1475+
1476+
self.assertEqual(asdict(c), {'f': {T(a='an a'): 0}})
1477+
1478+
def test_helper_asdict_namedtuple_derived(self):
1479+
class T(namedtuple('Tbase', 'a')):
1480+
def my_a(self):
1481+
return self.a
1482+
1483+
@dataclass
1484+
class C:
1485+
f: T
1486+
1487+
t = T(6)
1488+
c = C(t)
1489+
1490+
d = asdict(c)
1491+
self.assertEqual(d, {'f': T(a=6)})
1492+
# Make sure that t has been copied, not used directly.
1493+
self.assertIsNot(d['f'], t)
1494+
self.assertEqual(d['f'].my_a(), 6)
1495+
14321496
def test_helper_astuple(self):
14331497
# Basic tests for astuple(), it should return a new tuple.
14341498
@dataclass
@@ -1541,6 +1605,21 @@ def nt(lst):
15411605
self.assertEqual(t, NT(42, 2))
15421606
self.assertIs(type(t), NT)
15431607

1608+
def test_helper_astuple_namedtuple(self):
1609+
T = namedtuple('T', 'a b c')
1610+
@dataclass
1611+
class C:
1612+
x: str
1613+
y: T
1614+
c = C('outer', T(1, C('inner', T(11, 12, 13)), 2))
1615+
1616+
t = astuple(c)
1617+
self.assertEqual(t, ('outer', T(1, ('inner', (11, 12, 13)), 2)))
1618+
1619+
# Now, using a tuple_factory. list is convenient here.
1620+
t = astuple(c, tuple_factory=list)
1621+
self.assertEqual(t, ['outer', T(1, ['inner', T(11, 12, 13)], 2)])
1622+
15441623
def test_dynamic_class_creation(self):
15451624
cls_dict = {'__annotations__': {'x': int, 'y': int},
15461625
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dataclasses.asdict() and .astuple() now handle namedtuples correctly.

0 commit comments

Comments
 (0)