From d7e9650a0caa5a23e2e3a8a26c580dec1f1c51bd Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Fri, 10 Aug 2018 14:19:34 -0400 Subject: [PATCH 1/3] bpo-34363: Handle namedtuples fields correctly in asdict() and astuple(). --- Lib/dataclasses.py | 16 +++++++- Lib/test/test_dataclasses.py | 74 +++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index e00a125bbd8711..7b22a683dff623 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1020,8 +1020,14 @@ 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, (list, tuple)): + elif type(obj) in (list, tuple): + # Actual list or tuple. return type(obj)(_asdict_inner(v, dict_factory) for v in obj) + elif isinstance(obj, (list, tuple)): + # Subclass of list or tuple. Probably a namedtuple, whose + # __new__ works differently than tuple.__new__ does with + # respect to iterators. + 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()) @@ -1060,8 +1066,14 @@ 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, (list, tuple)): + elif type(obj) in (list, tuple): + # Actual list or tuple. return type(obj)(_astuple_inner(v, tuple_factory) for v in obj) + elif isinstance(obj, (list, tuple)): + # Subclass of list or tuple. Probably a namedtuple, whose + # __new__ works differently than tuple.__new__ does with + # respect to iterators. + 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)) for k, v in obj.items()) diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index c5140e8d1d9c94..0e90700ce21e8c 100755 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -8,7 +8,7 @@ import inspect import unittest from unittest.mock import Mock -from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional +from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, NamedTuple from collections import deque, OrderedDict, namedtuple from functools import total_ordering @@ -1281,6 +1281,42 @@ class C: self.assertEqual(asdict(c), {'x': 42, 'y': 2}) self.assertIs(type(asdict(c)), dict) + def test_helper_asdict_namedtuple(self): + nt1 = namedtuple('nt1', 'f') + nt2 = namedtuple('nt2', 'f1 f2') + + class NT1(NamedTuple): + f: Any + + class NT2(NamedTuple): + f1: Any + f2: Any + + # Test with both namedtuple and NamedTuple. + for type1, type2 in ((nt1, nt2), (NT1, NT2)): + with self.subTest(type1=type1, type2=type2): + @dataclass + class C: + i1: int + nt1: type1 + nt2: type2 + + inst1 = type1((1,)) + inst2 = type2((3,), (5, 6)) + c = C(3, inst1, inst2) + d = asdict(c) + self.assertEqual(d, {'i1': 3, 'nt1': inst1, 'nt2': inst2}) + # Make sure we copied these objects and aren't using the + # originals. + self.assertIsNot(d['nt1'], inst1) + self.assertIsNot(d['nt2'], inst1) + # Due to the way that namedtuples compare to tuples, check + # that dict values can be accessed by field name (that is, + # they're namedtuples not tuples). + self.assertEqual(d['nt1'].f, (1,)) + self.assertEqual(d['nt2'].f1, (3,)) + self.assertEqual(d['nt2'].f2, (5, 6)) + def test_helper_asdict_raises_on_classes(self): # asdict() should raise on a class object. @dataclass @@ -1394,6 +1430,42 @@ class C: self.assertEqual(astuple(c), (1, 42)) self.assertIs(type(astuple(c)), tuple) + def test_helper_astuple_namedtuple(self): + nt1 = namedtuple('nt1', 'f') + nt2 = namedtuple('nt2', 'f1 f2') + + class NT1(NamedTuple): + f: Any + + class NT2(NamedTuple): + f1: Any + f2: Any + + # Test with both namedtuple and NamedTuple. + for type1, type2 in ((nt1, nt2), (NT1, NT2)): + with self.subTest(type1=type1, type2=type2): + @dataclass + class C: + i1: int + nt1: type1 + nt2: type2 + + inst1 = type1((1,)) + inst2 = type2((3,), (5, 6)) + c = C(3, inst1, inst2) + t = astuple(c) + self.assertEqual(t, (3, inst1, inst2)) + # Make sure we copied these objects and aren't using the + # originals. + self.assertIsNot(t[1], inst1) + self.assertIsNot(t[2], inst1) + # Due to the way that namedtuples compare to tuples, check + # that dict values can be accessed by field name (that is, + # they're namedtuples not tuples). + self.assertEqual(t[1].f, (1,)) + self.assertEqual(t[2].f1, (3,)) + self.assertEqual(t[2].f2, (5, 6)) + def test_helper_astuple_raises_on_classes(self): # astuple() should raise on a class object. @dataclass From 62a28b872393f58df400097b75ebb8f2b81a2df8 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Fri, 10 Aug 2018 14:22:09 -0400 Subject: [PATCH 2/3] Add blurb. --- .../NEWS.d/next/Library/2018-08-10-14-22-02.bpo-34363.OLQ4w1.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2018-08-10-14-22-02.bpo-34363.OLQ4w1.rst diff --git a/Misc/NEWS.d/next/Library/2018-08-10-14-22-02.bpo-34363.OLQ4w1.rst b/Misc/NEWS.d/next/Library/2018-08-10-14-22-02.bpo-34363.OLQ4w1.rst new file mode 100644 index 00000000000000..92258a59e258c0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-08-10-14-22-02.bpo-34363.OLQ4w1.rst @@ -0,0 +1 @@ +Handle namedtuple fields correct in dataclasses.astuple() and .asdict(). From 5f11ce0e75e7eb7f04af027a27b0de5bd697761b Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Fri, 10 Aug 2018 18:37:17 -0400 Subject: [PATCH 3/3] Fixed typo in test. --- Lib/test/test_dataclasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 0e90700ce21e8c..ded459a6ef3715 100755 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -1309,7 +1309,7 @@ class C: # Make sure we copied these objects and aren't using the # originals. self.assertIsNot(d['nt1'], inst1) - self.assertIsNot(d['nt2'], inst1) + self.assertIsNot(d['nt2'], inst2) # Due to the way that namedtuples compare to tuples, check # that dict values can be accessed by field name (that is, # they're namedtuples not tuples). @@ -1458,7 +1458,7 @@ class C: # Make sure we copied these objects and aren't using the # originals. self.assertIsNot(t[1], inst1) - self.assertIsNot(t[2], inst1) + self.assertIsNot(t[2], inst2) # Due to the way that namedtuples compare to tuples, check # that dict values can be accessed by field name (that is, # they're namedtuples not tuples).