Skip to content

Commit 36e008c

Browse files
committed
gh-141510, PEP 814: Add frozendict support to pickle
1 parent fa73fd4 commit 36e008c

File tree

5 files changed

+174
-2
lines changed

5 files changed

+174
-2
lines changed

Lib/pickle.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ def __init__(self, value):
185185
BYTEARRAY8 = b'\x96' # push bytearray
186186
NEXT_BUFFER = b'\x97' # push next out-of-band buffer
187187
READONLY_BUFFER = b'\x98' # make top of stack readonly
188+
FROZENDICT = b'\x99' # A Python frozendict object
188189

189190
__all__.extend(x for x in dir() if x.isupper() and not x.startswith('_'))
190191

@@ -1081,6 +1082,30 @@ def save_dict(self, obj):
10811082

10821083
dispatch[dict] = save_dict
10831084

1085+
def save_frozendict(self, obj):
1086+
if self.proto < 4:
1087+
items = list(obj.items())
1088+
self.save_reduce(frozendict, (items,), obj=obj)
1089+
return
1090+
1091+
self.write(MARK)
1092+
1093+
save = self.save
1094+
for key, value in obj.items():
1095+
save(key)
1096+
save(value)
1097+
1098+
if id(obj) in self.memo:
1099+
# If the object is already in the memo, this means it is
1100+
# recursive. In this case, throw away everything we put on the
1101+
# stack, and fetch the object back from the memo.
1102+
self.write(POP_MARK + self.get(self.memo[id(obj)][0]))
1103+
return
1104+
1105+
self.write(FROZENDICT)
1106+
self.memoize(obj)
1107+
dispatch[frozendict] = save_frozendict
1108+
10841109
def _batch_setitems(self, items, obj):
10851110
# Helper to batch up SETITEMS sequences; proto >= 1 only
10861111
save = self.save
@@ -1621,6 +1646,13 @@ def load_dict(self):
16211646
self.append(d)
16221647
dispatch[DICT[0]] = load_dict
16231648

1649+
def load_frozendict(self):
1650+
items = self.pop_mark()
1651+
d = frozendict((items[i], items[i+1])
1652+
for i in range(0, len(items), 2))
1653+
self.append(d)
1654+
dispatch[FROZENDICT[0]] = load_frozendict
1655+
16241656
# INST and OBJ differ only in how they get a class object. It's not
16251657
# only sensible to do the rest in a common routine, the two routines
16261658
# previously diverged and grew different bugs.

Lib/pickletools.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,11 @@ def __repr__(self):
10351035
obtype=dict,
10361036
doc="A Python dict object.")
10371037

1038+
pyfrozendict = StackObject(
1039+
name="frozendict",
1040+
obtype=frozendict,
1041+
doc="A Python frozendict object.")
1042+
10381043
pyset = StackObject(
10391044
name="set",
10401045
obtype=set,
@@ -1384,6 +1389,23 @@ def __init__(self, name, code, arg,
13841389
proto=5,
13851390
doc="Make an out-of-band buffer object read-only."),
13861391

1392+
I(name='FROZENDICT',
1393+
code='\x99',
1394+
arg=None,
1395+
stack_before=[markobject, stackslice],
1396+
stack_after=[pyfrozendict],
1397+
proto=5,
1398+
doc="""Build a frozendict out of the topmost stack slice, after markobject.
1399+
1400+
All the stack entries following the topmost markobject are placed into
1401+
a single Python dict, which single dict object replaces all of the
1402+
stack from the topmost markobject onward. The stack slice alternates
1403+
key, value, key, value, .... For example,
1404+
1405+
Stack before: ... markobject 1 2 3 'abc'
1406+
Stack after: ... {1: 2, 3: 'abc'}
1407+
"""),
1408+
13871409
# Ways to spell None.
13881410

13891411
I(name='NONE',

Lib/test/pickletester.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2860,6 +2860,9 @@ def test_recursive_tuple_and_inst(self):
28602860
def test_recursive_dict_and_inst(self):
28612861
self._test_recursive_collection_and_inst(dict.fromkeys, oldminproto=0)
28622862

2863+
def test_recursive_frozendict_and_inst(self):
2864+
self._test_recursive_collection_and_inst(frozendict.fromkeys, oldminproto=0)
2865+
28632866
def test_recursive_set_and_inst(self):
28642867
self._test_recursive_collection_and_inst(set)
28652868

Lib/test/test_pickletools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ def test__all__(self):
510510
'StackObject',
511511
'pyint', 'pylong', 'pyinteger_or_bool', 'pybool', 'pyfloat',
512512
'pybytes_or_str', 'pystring', 'pybytes', 'pybytearray',
513-
'pyunicode', 'pynone', 'pytuple', 'pylist', 'pydict',
513+
'pyunicode', 'pynone', 'pytuple', 'pylist', 'pydict', 'pyfrozendict',
514514
'pyset', 'pyfrozenset', 'pybuffer', 'anyobject',
515515
'markobject', 'stackslice', 'OpcodeInfo', 'opcodes',
516516
'code2op',

Modules/_pickle.c

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ enum opcode {
137137
/* Protocol 5 */
138138
BYTEARRAY8 = '\x96',
139139
NEXT_BUFFER = '\x97',
140-
READONLY_BUFFER = '\x98'
140+
READONLY_BUFFER = '\x98',
141+
FROZENDICT = '\x99',
141142
};
142143

143144
enum {
@@ -596,6 +597,34 @@ Pdata_poplist(Pdata *self, Py_ssize_t start)
596597
return list;
597598
}
598599

600+
static PyObject *
601+
Pdata_poplist2(PickleState *state, Pdata *self, Py_ssize_t start)
602+
{
603+
if (start < self->fence) {
604+
Pdata_stack_underflow(state, self);
605+
return NULL;
606+
}
607+
608+
Py_ssize_t len = (Py_SIZE(self) - start) >> 1;
609+
610+
PyObject *list = PyList_New(len);
611+
if (list == NULL) {
612+
return NULL;
613+
}
614+
615+
for (Py_ssize_t i = start, j = 0; j < len; i+=2, j++) {
616+
PyObject *subtuple = PyTuple_Pack(2, self->data[i], self->data[i+1]);
617+
if (subtuple == NULL) {
618+
Py_DECREF(list);
619+
return NULL;
620+
}
621+
PyList_SET_ITEM(list, j, subtuple);
622+
}
623+
624+
Py_SET_SIZE(self, start);
625+
return list;
626+
}
627+
599628
typedef struct {
600629
PyObject *me_key;
601630
Py_ssize_t me_value;
@@ -3594,6 +3623,63 @@ save_dict(PickleState *state, PicklerObject *self, PyObject *obj)
35943623
return status;
35953624
}
35963625

3626+
static int
3627+
save_frozendict(PickleState *state, PicklerObject *self, PyObject *obj)
3628+
{
3629+
if (self->fast && !fast_save_enter(self, obj)) {
3630+
return -1;
3631+
}
3632+
3633+
if (self->proto < 4) {
3634+
PyObject *items = PyDict_Items(obj);
3635+
if (items == NULL) {
3636+
return -1;
3637+
}
3638+
3639+
PyObject *reduce_value;
3640+
reduce_value = Py_BuildValue("(O(O))", (PyObject*)&PyFrozenDict_Type,
3641+
items);
3642+
Py_DECREF(items);
3643+
if (reduce_value == NULL) {
3644+
return -1;
3645+
}
3646+
3647+
/* save_reduce() will memoize the object automatically */
3648+
int status = save_reduce(state, self, reduce_value, obj);
3649+
Py_DECREF(reduce_value);
3650+
return status;
3651+
}
3652+
3653+
const char mark_op = MARK;
3654+
if (_Pickler_Write(self, &mark_op, 1) < 0) {
3655+
return -1;
3656+
}
3657+
3658+
PyObject *key = NULL, *value = NULL;
3659+
Py_ssize_t pos = 0;
3660+
while (PyDict_Next(obj, &pos, &key, &value)) {
3661+
int res = save(state, self, key, 0);
3662+
if (res < 0) {
3663+
return -1;
3664+
}
3665+
3666+
res = save(state, self, value, 0);
3667+
if (res < 0) {
3668+
return -1;
3669+
}
3670+
}
3671+
3672+
const char frozendict_op = FROZENDICT;
3673+
if (_Pickler_Write(self, &frozendict_op, 1) < 0) {
3674+
return -1;
3675+
}
3676+
3677+
if (memo_put(state, self, obj) < 0) {
3678+
return -1;
3679+
}
3680+
return 0;
3681+
}
3682+
35973683
static int
35983684
save_set(PickleState *state, PicklerObject *self, PyObject *obj)
35993685
{
@@ -4569,6 +4655,10 @@ save(PickleState *st, PicklerObject *self, PyObject *obj, int pers_save)
45694655
status = save_dict(st, self, obj);
45704656
goto done;
45714657
}
4658+
else if (type == &PyFrozenDict_Type) {
4659+
status = save_frozendict(st, self, obj);
4660+
goto done;
4661+
}
45724662
else if (type == &PySet_Type) {
45734663
status = save_set(st, self, obj);
45744664
goto done;
@@ -6030,6 +6120,30 @@ load_dict(PickleState *st, UnpicklerObject *self)
60306120
return 0;
60316121
}
60326122

6123+
6124+
static int
6125+
load_frozendict(PickleState *st, UnpicklerObject *self)
6126+
{
6127+
Py_ssize_t i = marker(st, self);
6128+
if (i < 0) {
6129+
return -1;
6130+
}
6131+
6132+
PyObject *items = Pdata_poplist2(st, self->stack, i);
6133+
if (items == NULL) {
6134+
return -1;
6135+
}
6136+
6137+
PyObject *frozendict = PyFrozenDict_New(items);
6138+
Py_DECREF(items);
6139+
if (frozendict == NULL) {
6140+
return -1;
6141+
}
6142+
6143+
PDATA_PUSH(self->stack, frozendict, -1);
6144+
return 0;
6145+
}
6146+
60336147
static int
60346148
load_frozenset(PickleState *state, UnpicklerObject *self)
60356149
{
@@ -7130,6 +7244,7 @@ load(PickleState *st, UnpicklerObject *self)
71307244
OP(LIST, load_list)
71317245
OP(EMPTY_DICT, load_empty_dict)
71327246
OP(DICT, load_dict)
7247+
OP(FROZENDICT, load_frozendict)
71337248
OP(EMPTY_SET, load_empty_set)
71347249
OP(ADDITEMS, load_additems)
71357250
OP(FROZENSET, load_frozenset)

0 commit comments

Comments
 (0)