[Python-checkins] gh-98169 dataclasses.astuple support DefaultDict (#98170)

carljm webhook-mailer at python.org
Mon Mar 13 16:46:43 EDT 2023


https://github.com/python/cpython/commit/71e37d907905b0504c5bb7b25681adeea2157492
commit: 71e37d907905b0504c5bb7b25681adeea2157492
branch: main
author: T <tnie at tuta.io>
committer: carljm <carl at oddbird.net>
date: 2023-03-13T14:46:35-06:00
summary:

gh-98169 dataclasses.astuple support DefaultDict (#98170)

Co-authored-by: Pieter Eendebak <pieter.eendebak at gmail.com>

files:
A Misc/NEWS.d/next/Library/2022-10-10-19-14-51.gh-issue-98169.DBWIxL.rst
M Lib/dataclasses.py
M Lib/test/test_dataclasses.py

diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index f4617b1dbdac..7c3285cf440a 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -1321,15 +1321,14 @@ def _asdict_inner(obj, dict_factory):
         # 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) and hasattr(type(obj), 'default_factory'):
-        # obj is a defaultdict, which has a different constructor from
-        # dict as it requires the default_factory as its first arg.
-        # https://bugs.python.org/issue35540
-        result = type(obj)(getattr(obj, 'default_factory'))
-        for k, v in obj.items():
-            result[_asdict_inner(k, dict_factory)] = _asdict_inner(v, dict_factory)
-        return result
     elif isinstance(obj, dict):
+        if hasattr(type(obj), 'default_factory'):
+            # obj is a defaultdict, which has a different constructor from
+            # dict as it requires the default_factory as its first arg.
+            result = type(obj)(getattr(obj, 'default_factory'))
+            for k, v in obj.items():
+                result[_asdict_inner(k, dict_factory)] = _asdict_inner(v, dict_factory)
+            return result
         return type(obj)((_asdict_inner(k, dict_factory),
                           _asdict_inner(v, dict_factory))
                          for k, v in obj.items())
@@ -1382,7 +1381,15 @@ def _astuple_inner(obj, tuple_factory):
         # 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))
+        obj_type = type(obj)
+        if hasattr(obj_type, 'default_factory'):
+            # obj is a defaultdict, which has a different constructor from
+            # dict as it requires the default_factory as its first arg.
+            result = obj_type(getattr(obj, 'default_factory'))
+            for k, v in obj.items():
+                result[_astuple_inner(k, tuple_factory)] = _astuple_inner(v, tuple_factory)
+            return result
+        return obj_type((_astuple_inner(k, tuple_factory), _astuple_inner(v, tuple_factory))
                           for k, v in obj.items())
     else:
         return copy.deepcopy(obj)
diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py
index 76bed0c33146..46d4e0fedad2 100644
--- a/Lib/test/test_dataclasses.py
+++ b/Lib/test/test_dataclasses.py
@@ -1706,19 +1706,17 @@ class C:
     def test_helper_asdict_defaultdict(self):
         # Ensure asdict() does not throw exceptions when a
         # defaultdict is a member of a dataclass
-
         @dataclass
         class C:
             mp: DefaultDict[str, List]
 
-
         dd = defaultdict(list)
         dd["x"].append(12)
         c = C(mp=dd)
         d = asdict(c)
 
-        assert d == {"mp": {"x": [12]}}
-        assert d["mp"] is not c.mp  # make sure defaultdict is copied
+        self.assertEqual(d, {"mp": {"x": [12]}})
+        self.assertTrue(d["mp"] is not c.mp)  # make sure defaultdict is copied
 
     def test_helper_astuple(self):
         # Basic tests for astuple(), it should return a new tuple.
@@ -1847,6 +1845,21 @@ class C:
         t = astuple(c, tuple_factory=list)
         self.assertEqual(t, ['outer', T(1, ['inner', T(11, 12, 13)], 2)])
 
+    def test_helper_astuple_defaultdict(self):
+        # Ensure astuple() does not throw exceptions when a
+        # defaultdict is a member of a dataclass
+        @dataclass
+        class C:
+            mp: DefaultDict[str, List]
+
+        dd = defaultdict(list)
+        dd["x"].append(12)
+        c = C(mp=dd)
+        t = astuple(c)
+
+        self.assertEqual(t, ({"x": [12]},))
+        self.assertTrue(t[0] is not dd) # make sure defaultdict is copied
+
     def test_dynamic_class_creation(self):
         cls_dict = {'__annotations__': {'x': int, 'y': int},
                     }
diff --git a/Misc/NEWS.d/next/Library/2022-10-10-19-14-51.gh-issue-98169.DBWIxL.rst b/Misc/NEWS.d/next/Library/2022-10-10-19-14-51.gh-issue-98169.DBWIxL.rst
new file mode 100644
index 000000000000..24c3aeecc83f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-10-10-19-14-51.gh-issue-98169.DBWIxL.rst
@@ -0,0 +1,2 @@
+Fix :func:`dataclasses.astuple` crash when :class:`collections.defaultdict`
+is present in the attributes.



More information about the Python-checkins mailing list