Skip to content

Commit 9d65ea1

Browse files
[3.12] gh-82951: Fix serializing by name in pickle protocols < 4 (GH-122149) (GH-122265)
Serializing objects with complex __qualname__ (such as unbound methods and nested classes) by name no longer involves serializing parent objects by value in pickle protocols < 4. (cherry picked from commit dc07f65) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent be5e229 commit 9d65ea1

File tree

4 files changed

+82
-26
lines changed

4 files changed

+82
-26
lines changed

Lib/pickle.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,11 +1108,35 @@ def save_global(self, obj, name=None):
11081108
self.save(module_name)
11091109
self.save(name)
11101110
write(STACK_GLOBAL)
1111-
elif parent is not module:
1112-
self.save_reduce(getattr, (parent, lastname))
1113-
elif self.proto >= 3:
1114-
write(GLOBAL + bytes(module_name, "utf-8") + b'\n' +
1115-
bytes(name, "utf-8") + b'\n')
1111+
elif '.' in name:
1112+
# In protocol < 4, objects with multi-part __qualname__
1113+
# are represented as
1114+
# getattr(getattr(..., attrname1), attrname2).
1115+
dotted_path = name.split('.')
1116+
name = dotted_path.pop(0)
1117+
save = self.save
1118+
for attrname in dotted_path:
1119+
save(getattr)
1120+
if self.proto < 2:
1121+
write(MARK)
1122+
self._save_toplevel_by_name(module_name, name)
1123+
for attrname in dotted_path:
1124+
save(attrname)
1125+
if self.proto < 2:
1126+
write(TUPLE)
1127+
else:
1128+
write(TUPLE2)
1129+
write(REDUCE)
1130+
else:
1131+
self._save_toplevel_by_name(module_name, name)
1132+
1133+
self.memoize(obj)
1134+
1135+
def _save_toplevel_by_name(self, module_name, name):
1136+
if self.proto >= 3:
1137+
# Non-ASCII identifiers are supported only with protocols >= 3.
1138+
self.write(GLOBAL + bytes(module_name, "utf-8") + b'\n' +
1139+
bytes(name, "utf-8") + b'\n')
11161140
else:
11171141
if self.fix_imports:
11181142
r_name_mapping = _compat_pickle.REVERSE_NAME_MAPPING
@@ -1122,15 +1146,13 @@ def save_global(self, obj, name=None):
11221146
elif module_name in r_import_mapping:
11231147
module_name = r_import_mapping[module_name]
11241148
try:
1125-
write(GLOBAL + bytes(module_name, "ascii") + b'\n' +
1126-
bytes(name, "ascii") + b'\n')
1149+
self.write(GLOBAL + bytes(module_name, "ascii") + b'\n' +
1150+
bytes(name, "ascii") + b'\n')
11271151
except UnicodeEncodeError:
11281152
raise PicklingError(
11291153
"can't pickle global identifier '%s.%s' using "
11301154
"pickle protocol %i" % (module, name, self.proto)) from None
11311155

1132-
self.memoize(obj)
1133-
11341156
def save_type(self, obj):
11351157
if obj is type(None):
11361158
return self.save_reduce(type, (None,), obj=obj)

Lib/test/pickletester.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2802,6 +2802,18 @@ class Recursive:
28022802
self.assertIs(unpickled, Recursive)
28032803
del Recursive.mod # break reference loop
28042804

2805+
def test_recursive_nested_names2(self):
2806+
global Recursive
2807+
class Recursive:
2808+
pass
2809+
Recursive.ref = Recursive
2810+
Recursive.__qualname__ = 'Recursive.ref'
2811+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
2812+
with self.subTest(proto=proto):
2813+
unpickled = self.loads(self.dumps(Recursive, proto))
2814+
self.assertIs(unpickled, Recursive)
2815+
del Recursive.ref # break reference loop
2816+
28052817
def test_py_methods(self):
28062818
global PyMethodsTest
28072819
class PyMethodsTest:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Serializing objects with complex ``__qualname__`` (such as unbound methods
2+
and nested classes) by name no longer involves serializing parent objects by
3+
value in pickle protocols < 4.

Modules/_pickle.c

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3651,7 +3651,6 @@ save_global(PickleState *st, PicklerObject *self, PyObject *obj,
36513651
PyObject *module = NULL;
36523652
PyObject *parent = NULL;
36533653
PyObject *dotted_path = NULL;
3654-
PyObject *lastname = NULL;
36553654
PyObject *cls;
36563655
int status = 0;
36573656

@@ -3692,10 +3691,7 @@ save_global(PickleState *st, PicklerObject *self, PyObject *obj,
36923691
obj, module_name);
36933692
goto error;
36943693
}
3695-
lastname = Py_NewRef(PyList_GET_ITEM(dotted_path,
3696-
PyList_GET_SIZE(dotted_path) - 1));
36973694
cls = get_deep_attribute(module, dotted_path, &parent);
3698-
Py_CLEAR(dotted_path);
36993695
if (cls == NULL) {
37003696
PyErr_Format(st->PicklingError,
37013697
"Can't pickle %R: attribute lookup %S on %S failed",
@@ -3783,7 +3779,10 @@ save_global(PickleState *st, PicklerObject *self, PyObject *obj,
37833779
else {
37843780
gen_global:
37853781
if (parent == module) {
3786-
Py_SETREF(global_name, Py_NewRef(lastname));
3782+
Py_SETREF(global_name,
3783+
Py_NewRef(PyList_GET_ITEM(dotted_path,
3784+
PyList_GET_SIZE(dotted_path) - 1)));
3785+
Py_CLEAR(dotted_path);
37873786
}
37883787
if (self->proto >= 4) {
37893788
const char stack_global_op = STACK_GLOBAL;
@@ -3796,20 +3795,30 @@ save_global(PickleState *st, PicklerObject *self, PyObject *obj,
37963795
if (_Pickler_Write(self, &stack_global_op, 1) < 0)
37973796
goto error;
37983797
}
3799-
else if (parent != module) {
3800-
PyObject *reduce_value = Py_BuildValue("(O(OO))",
3801-
st->getattr, parent, lastname);
3802-
if (reduce_value == NULL)
3803-
goto error;
3804-
status = save_reduce(st, self, reduce_value, NULL);
3805-
Py_DECREF(reduce_value);
3806-
if (status < 0)
3807-
goto error;
3808-
}
38093798
else {
38103799
/* Generate a normal global opcode if we are using a pickle
38113800
protocol < 4, or if the object is not registered in the
3812-
extension registry. */
3801+
extension registry.
3802+
3803+
Objects with multi-part __qualname__ are represented as
3804+
getattr(getattr(..., attrname1), attrname2). */
3805+
const char mark_op = MARK;
3806+
const char tupletwo_op = (self->proto < 2) ? TUPLE : TUPLE2;
3807+
const char reduce_op = REDUCE;
3808+
Py_ssize_t i;
3809+
if (dotted_path) {
3810+
if (PyList_GET_SIZE(dotted_path) > 1) {
3811+
Py_SETREF(global_name, Py_NewRef(PyList_GET_ITEM(dotted_path, 0)));
3812+
}
3813+
for (i = 1; i < PyList_GET_SIZE(dotted_path); i++) {
3814+
if (save(st, self, st->getattr, 0) < 0 ||
3815+
(self->proto < 2 && _Pickler_Write(self, &mark_op, 1) < 0))
3816+
{
3817+
goto error;
3818+
}
3819+
}
3820+
}
3821+
38133822
PyObject *encoded;
38143823
PyObject *(*unicode_encoder)(PyObject *);
38153824

@@ -3871,6 +3880,17 @@ save_global(PickleState *st, PicklerObject *self, PyObject *obj,
38713880
Py_DECREF(encoded);
38723881
if (_Pickler_Write(self, "\n", 1) < 0)
38733882
goto error;
3883+
3884+
if (dotted_path) {
3885+
for (i = 1; i < PyList_GET_SIZE(dotted_path); i++) {
3886+
if (save(st, self, PyList_GET_ITEM(dotted_path, i), 0) < 0 ||
3887+
_Pickler_Write(self, &tupletwo_op, 1) < 0 ||
3888+
_Pickler_Write(self, &reduce_op, 1) < 0)
3889+
{
3890+
goto error;
3891+
}
3892+
}
3893+
}
38743894
}
38753895
/* Memoize the object. */
38763896
if (memo_put(st, self, obj) < 0)
@@ -3886,7 +3906,6 @@ save_global(PickleState *st, PicklerObject *self, PyObject *obj,
38863906
Py_XDECREF(module);
38873907
Py_XDECREF(parent);
38883908
Py_XDECREF(dotted_path);
3889-
Py_XDECREF(lastname);
38903909

38913910
return status;
38923911
}

0 commit comments

Comments
 (0)