Skip to content

Commit 20b1535

Browse files
authored
gh-141510, PEP 814: Add frozendict support to pickle (#144967)
Add frozendict.__getnewargs__() method.
1 parent 85021bc commit 20b1535

File tree

4 files changed

+118
-5
lines changed

4 files changed

+118
-5
lines changed

Lib/test/picklecommon.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,17 @@ class MyFrozenSet(frozenset):
263263
MyStr, MyUnicode,
264264
MyTuple, MyList, MyDict, MySet, MyFrozenSet]
265265

266+
try:
267+
frozendict
268+
except NameError:
269+
# Python 3.14 and older
270+
pass
271+
else:
272+
class MyFrozenDict(dict):
273+
sample = frozendict({"a": 1, "b": 2})
274+
myclasses.append(MyFrozenDict)
275+
276+
266277
# For test_newobj_overridden_new
267278
class MyIntWithNew(int):
268279
def __new__(cls, value):

Lib/test/pickletester.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2839,11 +2839,13 @@ def test_recursive_multi(self):
28392839
self.assertEqual(list(x[0].attr.keys()), [1])
28402840
self.assertIs(x[0].attr[1], x)
28412841

2842-
def _test_recursive_collection_and_inst(self, factory, oldminproto=None):
2842+
def _test_recursive_collection_and_inst(self, factory, oldminproto=None,
2843+
minprotocol=0):
28432844
if self.py_version < (3, 0):
28442845
self.skipTest('"classic" classes are not interoperable with Python 2')
28452846
# Mutable object containing a collection containing the original
28462847
# object.
2848+
protocols = range(minprotocol, pickle.HIGHEST_PROTOCOL + 1)
28472849
o = Object()
28482850
o.attr = factory([o])
28492851
t = type(o.attr)
@@ -2883,6 +2885,11 @@ def test_recursive_tuple_and_inst(self):
28832885
def test_recursive_dict_and_inst(self):
28842886
self._test_recursive_collection_and_inst(dict.fromkeys, oldminproto=0)
28852887

2888+
def test_recursive_frozendict_and_inst(self):
2889+
if self.py_version < (3, 15):
2890+
self.skipTest('need frozendict')
2891+
self._test_recursive_collection_and_inst(frozendict.fromkeys, minprotocol=2)
2892+
28862893
def test_recursive_set_and_inst(self):
28872894
self._test_recursive_collection_and_inst(set)
28882895

@@ -2904,6 +2911,42 @@ def test_recursive_set_subclass_and_inst(self):
29042911
def test_recursive_frozenset_subclass_and_inst(self):
29052912
self._test_recursive_collection_and_inst(MyFrozenSet)
29062913

2914+
def _test_recursive_collection_in_key(self, factory, minprotocol=0):
2915+
protocols = range(minprotocol, pickle.HIGHEST_PROTOCOL + 1)
2916+
key = Object()
2917+
o = factory({key: 1})
2918+
key.attr = o
2919+
for proto in protocols:
2920+
with self.subTest(proto=proto):
2921+
s = self.dumps(o, proto)
2922+
x = self.loads(s)
2923+
keys = list(x.keys())
2924+
self.assertEqual(len(keys), 1)
2925+
self.assertIs(keys[0].attr, x)
2926+
2927+
def test_recursive_frozendict_in_key(self):
2928+
self._test_recursive_collection_in_key(frozendict, minprotocol=2)
2929+
2930+
def test_recursive_frozendict_subclass_in_key(self):
2931+
self._test_recursive_collection_in_key(MyFrozenDict)
2932+
2933+
def _test_recursive_collection_in_value(self, factory, minprotocol=0):
2934+
protocols = range(minprotocol, pickle.HIGHEST_PROTOCOL + 1)
2935+
o = factory(key=[])
2936+
o['key'].append(o)
2937+
for proto in protocols:
2938+
with self.subTest(proto=proto):
2939+
s = self.dumps(o, proto)
2940+
x = self.loads(s)
2941+
self.assertEqual(len(x['key']), 1)
2942+
self.assertIs(x['key'][0], x)
2943+
2944+
def test_recursive_frozendict_in_value(self):
2945+
self._test_recursive_collection_in_value(frozendict, minprotocol=2)
2946+
2947+
def test_recursive_frozendict_subclass_in_value(self):
2948+
self._test_recursive_collection_in_value(MyFrozenDict)
2949+
29072950
def test_recursive_inst_state(self):
29082951
# Mutable object containing itself.
29092952
y = REX_state()

Lib/test/test_dict.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,6 +1731,10 @@ class FrozenDict(frozendict):
17311731
pass
17321732

17331733

1734+
class FrozenDictSlots(frozendict):
1735+
__slots__ = ('slot_attr',)
1736+
1737+
17341738
class FrozenDictTests(unittest.TestCase):
17351739
def test_copy(self):
17361740
d = frozendict(x=1, y=2)
@@ -1773,10 +1777,8 @@ def test_repr(self):
17731777
d = frozendict(x=1, y=2)
17741778
self.assertEqual(repr(d), "frozendict({'x': 1, 'y': 2})")
17751779

1776-
class MyFrozenDict(frozendict):
1777-
pass
1778-
d = MyFrozenDict(x=1, y=2)
1779-
self.assertEqual(repr(d), "MyFrozenDict({'x': 1, 'y': 2})")
1780+
d = FrozenDict(x=1, y=2)
1781+
self.assertEqual(repr(d), "FrozenDict({'x': 1, 'y': 2})")
17801782

17811783
def test_hash(self):
17821784
# hash() doesn't rely on the items order
@@ -1825,6 +1827,50 @@ def __new__(self):
18251827
self.assertEqual(type(fd), DictSubclass)
18261828
self.assertEqual(created, frozendict(x=1))
18271829

1830+
def test_pickle(self):
1831+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
1832+
for fd in (
1833+
frozendict(),
1834+
frozendict(x=1, y=2),
1835+
FrozenDict(x=1, y=2),
1836+
FrozenDictSlots(x=1, y=2),
1837+
):
1838+
if type(fd) == FrozenDict:
1839+
fd.attr = 123
1840+
if type(fd) == FrozenDictSlots:
1841+
fd.slot_attr = 456
1842+
with self.subTest(fd=fd, proto=proto):
1843+
if proto >= 2:
1844+
p = pickle.dumps(fd, proto)
1845+
fd2 = pickle.loads(p)
1846+
self.assertEqual(fd2, fd)
1847+
self.assertEqual(type(fd2), type(fd))
1848+
if type(fd) == FrozenDict:
1849+
self.assertEqual(fd2.attr, 123)
1850+
if type(fd) == FrozenDictSlots:
1851+
self.assertEqual(fd2.slot_attr, 456)
1852+
else:
1853+
# protocol 0 and 1 don't support frozendict
1854+
with self.assertRaises(TypeError):
1855+
pickle.dumps(fd, proto)
1856+
1857+
def test_pickle_iter(self):
1858+
fd = frozendict(c=1, b=2, a=3, d=4, e=5, f=6)
1859+
for method_name in (None, 'keys', 'values', 'items'):
1860+
if method_name is not None:
1861+
meth = getattr(fd, method_name)
1862+
else:
1863+
meth = lambda: fd
1864+
expected = list(meth())[1:]
1865+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
1866+
with self.subTest(method_name=method_name, protocol=proto):
1867+
it = iter(meth())
1868+
next(it)
1869+
p = pickle.dumps(it, proto)
1870+
unpickled = pickle.loads(p)
1871+
self.assertEqual(list(unpickled), expected)
1872+
self.assertEqual(list(it), expected)
1873+
18281874

18291875
if __name__ == "__main__":
18301876
unittest.main()

Objects/dictobject.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7930,6 +7930,18 @@ _PyObject_InlineValuesConsistencyCheck(PyObject *obj)
79307930

79317931
// --- frozendict implementation ---------------------------------------------
79327932

7933+
static PyObject *
7934+
frozendict_getnewargs(PyObject *op, PyObject *Py_UNUSED(dummy))
7935+
{
7936+
// Call dict(op): convert 'op' frozendict to a dict
7937+
PyObject *arg = PyObject_CallOneArg((PyObject*)&PyDict_Type, op);
7938+
if (arg == NULL) {
7939+
return NULL;
7940+
}
7941+
return Py_BuildValue("(N)", arg);
7942+
}
7943+
7944+
79337945
static PyNumberMethods frozendict_as_number = {
79347946
.nb_or = frozendict_or,
79357947
};
@@ -7951,6 +7963,7 @@ static PyMethodDef frozendict_methods[] = {
79517963
DICT_COPY_METHODDEF
79527964
DICT___REVERSED___METHODDEF
79537965
{"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")},
7966+
{"__getnewargs__", frozendict_getnewargs, METH_NOARGS},
79547967
{NULL, NULL} /* sentinel */
79557968
};
79567969

0 commit comments

Comments
 (0)