summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEric V. Smith <ericvsmith@users.noreply.github.com>2018-09-14 15:32:16 (GMT)
committerGitHub <noreply@github.com>2018-09-14 15:32:16 (GMT)
commit9b9d97dd139a799d28ff8bc90d118b1cac190b03 (patch)
treebfd25599bae88aa025341eb7aceba972185c6af8
parent73820a60cc3c990abb351540ca27bf7689bce8ac (diff)
downloadcpython-9b9d97dd139a799d28ff8bc90d118b1cac190b03.zip
cpython-9b9d97dd139a799d28ff8bc90d118b1cac190b03.tar.gz
cpython-9b9d97dd139a799d28ff8bc90d118b1cac190b03.tar.bz2
bpo-34363: dataclasses.asdict() and .astuple() now handle fields which are namedtuples. (GH-9151)
-rw-r--r--Lib/dataclasses.py40
-rwxr-xr-xLib/test/test_dataclasses.py79
-rw-r--r--Misc/NEWS.d/next/Library/2018-09-10-21-09-34.bpo-34363.YuSb0T.rst1
3 files changed, 118 insertions, 2 deletions
diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index a43d076..28e9f75 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -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))
diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py
index 4c93513..6efe785 100755
--- a/Lib/test/test_dataclasses.py
+++ b/Lib/test/test_dataclasses.py
@@ -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},
}
diff --git a/Misc/NEWS.d/next/Library/2018-09-10-21-09-34.bpo-34363.YuSb0T.rst b/Misc/NEWS.d/next/Library/2018-09-10-21-09-34.bpo-34363.YuSb0T.rst
new file mode 100644
index 0000000..5691efb
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-09-10-21-09-34.bpo-34363.YuSb0T.rst
@@ -0,0 +1 @@
+dataclasses.asdict() and .astuple() now handle namedtuples correctly.