From 7567680cb6cf18835deb049b286b4db05b1506fc Mon Sep 17 00:00:00 2001 From: Tiger Nie Date: Tue, 22 Mar 2022 13:07:52 -0500 Subject: [PATCH 1/4] bpo-35540: Fix dataclasses.asdict support for defaultdict fields dataclasses.asdict breaks when there are `defaultdict`s in the dataclass attributes because it assumes `defaultdict` and `dict` are the same thing, but in reality `defaultdict` takes a default_factory as the first argument in its constructor. This change adds support for defaultdict fields. --- Lib/dataclasses.py | 8 ++++++++ .../next/Library/2022-03-22-18-28-55.bpo-35540.nyijX9.rst | 1 + 2 files changed, 9 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2022-03-22-18-28-55.bpo-35540.nyijX9.rst diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 6be7c7b5de917f..998d312cceb5d6 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1295,6 +1295,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(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): return type(obj)((_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory)) diff --git a/Misc/NEWS.d/next/Library/2022-03-22-18-28-55.bpo-35540.nyijX9.rst b/Misc/NEWS.d/next/Library/2022-03-22-18-28-55.bpo-35540.nyijX9.rst new file mode 100644 index 00000000000000..4b8d455ef439b4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-03-22-18-28-55.bpo-35540.nyijX9.rst @@ -0,0 +1 @@ +Fix :func:`dataclasses.asdict` crash when :class:`collections.defaultdict`s are present in the attributes. From 82b41dc440a0ff5550dfee5c647cd22da1699231 Mon Sep 17 00:00:00 2001 From: Tiger Nie Date: Thu, 24 Mar 2022 16:58:06 -0500 Subject: [PATCH 2/4] fix blurb --- .../next/Library/2022-03-22-18-28-55.bpo-35540.nyijX9.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-03-22-18-28-55.bpo-35540.nyijX9.rst b/Misc/NEWS.d/next/Library/2022-03-22-18-28-55.bpo-35540.nyijX9.rst index 4b8d455ef439b4..b7aeee6c8c8f78 100644 --- a/Misc/NEWS.d/next/Library/2022-03-22-18-28-55.bpo-35540.nyijX9.rst +++ b/Misc/NEWS.d/next/Library/2022-03-22-18-28-55.bpo-35540.nyijX9.rst @@ -1 +1 @@ -Fix :func:`dataclasses.asdict` crash when :class:`collections.defaultdict`s are present in the attributes. +Fix :func:`dataclasses.asdict` crash when :class:`collections.defaultdict` is present in the attributes. From 3fe88733eec07f82630d274a8af3fe36d4b34c58 Mon Sep 17 00:00:00 2001 From: Tiger Nie Date: Wed, 4 May 2022 11:37:33 -0500 Subject: [PATCH 3/4] added tests for asdict() with defaultdict --- Lib/test/test_dataclasses.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 847bcd46a69268..10404950d5cfed 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -11,9 +11,9 @@ import types import unittest from unittest.mock import Mock -from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol +from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol, DefaultDict from typing import get_type_hints -from collections import deque, OrderedDict, namedtuple +from collections import deque, OrderedDict, namedtuple, defaultdict from functools import total_ordering import typing # Needed for the string "typing.ClassVar[int]" to work as an annotation. @@ -1650,6 +1650,23 @@ class C: self.assertIsNot(d['f'], t) self.assertEqual(d['f'].my_a(), 6) + 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 + def test_helper_astuple(self): # Basic tests for astuple(), it should return a new tuple. @dataclass From 4fcb8d0830d18e5c67aefcf2bcd1b3464f397eb8 Mon Sep 17 00:00:00 2001 From: Tiger Nie Date: Wed, 5 Oct 2022 23:22:28 -0500 Subject: [PATCH 4/4] check 'default_factory' attr on the type --- Lib/dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 2c4cd5cd40ff8c..17769ccd18c29a 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1309,7 +1309,7 @@ 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(obj, 'default_factory'): + 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