bpo-34363: dataclasses.asdict() and .astuple() now handle fields which are namedtuples. (GH-9151)
This commit is contained in:
parent
73820a60cc
commit
9b9d97dd13
|
@ -1026,11 +1026,36 @@ def _asdict_inner(obj, dict_factory):
|
|||
value = _asdict_inner(getattr(obj, f.name), dict_factory)
|
||||
result.append((f.name, value))
|
||||
return dict_factory(result)
|
||||
elif isinstance(obj, tuple) and hasattr(obj, '_fields'):
|
||||
# obj is a namedtuple. Recurse into it, but the returned
|
||||
# object is another namedtuple of the same type. This is
|
||||
# similar to how other list- or tuple-derived classes are
|
||||
# treated (see below), but we just need to create them
|
||||
# differently because a namedtuple's __init__ needs to be
|
||||
# called differently (see bpo-34363).
|
||||
|
||||
# I'm not using namedtuple's _asdict()
|
||||
# method, because:
|
||||
# - it does not recurse in to the namedtuple fields and
|
||||
# convert them to dicts (using dict_factory).
|
||||
# - I don't actually want to return a dict here. The the main
|
||||
# use case here is json.dumps, and it handles converting
|
||||
# namedtuples to lists. Admittedly we're losing some
|
||||
# information here when we produce a json list instead of a
|
||||
# dict. Note that if we returned dicts here instead of
|
||||
# namedtuples, we could no longer call asdict() on a data
|
||||
# structure where a namedtuple was used as a dict key.
|
||||
|
||||
return type(obj)(*[_asdict_inner(v, dict_factory) for v in obj])
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
# Assume we can create an object of this type by passing in a
|
||||
# generator (which is not true for namedtuples, handled
|
||||
# above).
|
||||
return type(obj)(_asdict_inner(v, dict_factory) for v in obj)
|
||||
elif isinstance(obj, dict):
|
||||
return type(obj)((_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory))
|
||||
for k, v in obj.items())
|
||||
return type(obj)((_asdict_inner(k, dict_factory),
|
||||
_asdict_inner(v, dict_factory))
|
||||
for k, v in obj.items())
|
||||
else:
|
||||
return copy.deepcopy(obj)
|
||||
|
||||
|
@ -1066,7 +1091,18 @@ def _astuple_inner(obj, tuple_factory):
|
|||
value = _astuple_inner(getattr(obj, f.name), tuple_factory)
|
||||
result.append(value)
|
||||
return tuple_factory(result)
|
||||
elif isinstance(obj, tuple) and hasattr(obj, '_fields'):
|
||||
# obj is a namedtuple. Recurse into it, but the returned
|
||||
# object is another namedtuple of the same type. This is
|
||||
# similar to how other list- or tuple-derived classes are
|
||||
# treated (see below), but we just need to create them
|
||||
# differently because a namedtuple's __init__ needs to be
|
||||
# called differently (see bpo-34363).
|
||||
return type(obj)(*[_astuple_inner(v, tuple_factory) for v in obj])
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
# Assume we can create an object of this type by passing in a
|
||||
# generator (which is not true for namedtuples, handled
|
||||
# above).
|
||||
return type(obj)(_astuple_inner(v, tuple_factory) for v in obj)
|
||||
elif isinstance(obj, dict):
|
||||
return type(obj)((_astuple_inner(k, tuple_factory), _astuple_inner(v, tuple_factory))
|
||||
|
|
|
@ -1429,6 +1429,70 @@ class TestCase(unittest.TestCase):
|
|||
self.assertEqual(d, OrderedDict([('x', 42), ('y', 2)]))
|
||||
self.assertIs(type(d), OrderedDict)
|
||||
|
||||
def test_helper_asdict_namedtuple(self):
|
||||
T = namedtuple('T', 'a b c')
|
||||
@dataclass
|
||||
class C:
|
||||
x: str
|
||||
y: T
|
||||
c = C('outer', T(1, C('inner', T(11, 12, 13)), 2))
|
||||
|
||||
d = asdict(c)
|
||||
self.assertEqual(d, {'x': 'outer',
|
||||
'y': T(1,
|
||||
{'x': 'inner',
|
||||
'y': T(11, 12, 13)},
|
||||
2),
|
||||
}
|
||||
)
|
||||
|
||||
# Now with a dict_factory. OrderedDict is convenient, but
|
||||
# since it compares to dicts, we also need to have separate
|
||||
# assertIs tests.
|
||||
d = asdict(c, dict_factory=OrderedDict)
|
||||
self.assertEqual(d, {'x': 'outer',
|
||||
'y': T(1,
|
||||
{'x': 'inner',
|
||||
'y': T(11, 12, 13)},
|
||||
2),
|
||||
}
|
||||
)
|
||||
|
||||
# Make sure that the returned dicts are actuall OrderedDicts.
|
||||
self.assertIs(type(d), OrderedDict)
|
||||
self.assertIs(type(d['y'][1]), OrderedDict)
|
||||
|
||||
def test_helper_asdict_namedtuple_key(self):
|
||||
# Ensure that a field that contains a dict which has a
|
||||
# namedtuple as a key works with asdict().
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
f: dict
|
||||
T = namedtuple('T', 'a')
|
||||
|
||||
c = C({T('an a'): 0})
|
||||
|
||||
self.assertEqual(asdict(c), {'f': {T(a='an a'): 0}})
|
||||
|
||||
def test_helper_asdict_namedtuple_derived(self):
|
||||
class T(namedtuple('Tbase', 'a')):
|
||||
def my_a(self):
|
||||
return self.a
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
f: T
|
||||
|
||||
t = T(6)
|
||||
c = C(t)
|
||||
|
||||
d = asdict(c)
|
||||
self.assertEqual(d, {'f': T(a=6)})
|
||||
# Make sure that t has been copied, not used directly.
|
||||
self.assertIsNot(d['f'], t)
|
||||
self.assertEqual(d['f'].my_a(), 6)
|
||||
|
||||
def test_helper_astuple(self):
|
||||
# Basic tests for astuple(), it should return a new tuple.
|
||||
@dataclass
|
||||
|
@ -1541,6 +1605,21 @@ class TestCase(unittest.TestCase):
|
|||
self.assertEqual(t, NT(42, 2))
|
||||
self.assertIs(type(t), NT)
|
||||
|
||||
def test_helper_astuple_namedtuple(self):
|
||||
T = namedtuple('T', 'a b c')
|
||||
@dataclass
|
||||
class C:
|
||||
x: str
|
||||
y: T
|
||||
c = C('outer', T(1, C('inner', T(11, 12, 13)), 2))
|
||||
|
||||
t = astuple(c)
|
||||
self.assertEqual(t, ('outer', T(1, ('inner', (11, 12, 13)), 2)))
|
||||
|
||||
# Now, using a tuple_factory. list is convenient here.
|
||||
t = astuple(c, tuple_factory=list)
|
||||
self.assertEqual(t, ['outer', T(1, ['inner', T(11, 12, 13)], 2)])
|
||||
|
||||
def test_dynamic_class_creation(self):
|
||||
cls_dict = {'__annotations__': {'x': int, 'y': int},
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
dataclasses.asdict() and .astuple() now handle namedtuples correctly.
|
Loading…
Reference in New Issue