summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorT <tnie@tuta.io>2023-03-13 20:46:35 (GMT)
committerGitHub <noreply@github.com>2023-03-13 20:46:35 (GMT)
commit71e37d907905b0504c5bb7b25681adeea2157492 (patch)
tree02f0f081a14cf8909aed93b5cddc9e62a7fb7cf8
parent85ba8a3e03707092800cbf2a29d95e0b495e3cb7 (diff)
downloadcpython-71e37d907905b0504c5bb7b25681adeea2157492.zip
cpython-71e37d907905b0504c5bb7b25681adeea2157492.tar.gz
cpython-71e37d907905b0504c5bb7b25681adeea2157492.tar.bz2
gh-98169 dataclasses.astuple support DefaultDict (#98170)
Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com>
-rw-r--r--Lib/dataclasses.py25
-rw-r--r--Lib/test/test_dataclasses.py21
-rw-r--r--Misc/NEWS.d/next/Library/2022-10-10-19-14-51.gh-issue-98169.DBWIxL.rst2
3 files changed, 35 insertions, 13 deletions
diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index f4617b1..7c3285c 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 76bed0c..46d4e0f 100644
--- a/Lib/test/test_dataclasses.py
+++ b/Lib/test/test_dataclasses.py
@@ -1706,19 +1706,17 @@ class TestCase(unittest.TestCase):
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 TestCase(unittest.TestCase):
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 0000000..24c3aee
--- /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.