Skip to content

Commit e11fc8a

Browse files
Add support for class methods, unbound methods, static methods and nested classes.
1 parent 62f6c64 commit e11fc8a

File tree

9 files changed

+119
-14
lines changed

9 files changed

+119
-14
lines changed

Doc/whatsnew/3.15.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,13 @@ os.path
646646
(Contributed by Petr Viktorin for :cve:`2025-4517`.)
647647

648648

649+
pickle
650+
------
651+
652+
* Add support for pickling private methods and nested classes.
653+
(Contributed by Zackery Spytz and Serhiy Storchaka in :gh:`77188`.)
654+
655+
649656
resource
650657
--------
651658

Include/internal/pycore_symtable.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,12 @@ extern int _PySymtable_LookupOptional(struct symtable *, void *, PySTEntryObject
151151
extern void _PySymtable_Free(struct symtable *);
152152

153153
extern PyObject *_Py_MaybeMangle(PyObject *privateobj, PySTEntryObject *ste, PyObject *name);
154-
extern PyObject* _Py_Mangle(PyObject *p, PyObject *name);
154+
155+
// Export for '_pickle' shared extension
156+
PyAPI_FUNC(PyObject *)
157+
_Py_Mangle(PyObject *, PyObject *);
158+
PyAPI_FUNC(int)
159+
_Py_IsPrivateName(PyObject *);
155160

156161
/* Flags for def-use information */
157162

Lib/pickle.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,17 @@ def save_global(self, obj, name=None):
11751175
if name is None:
11761176
name = obj.__name__
11771177

1178+
if '.__' in name:
1179+
# Mangle names of private attributes.
1180+
dotted_path = name.split('.')
1181+
for i, subpath in enumerate(dotted_path):
1182+
if i and subpath.startswith('__') and not subpath.endswith('__'):
1183+
prev = prev.lstrip('_')
1184+
if prev:
1185+
dotted_path[i] = f"_{prev.lstrip('_')}{subpath}"
1186+
prev = subpath
1187+
name = '.'.join(dotted_path)
1188+
11781189
module_name = whichmodule(obj, name)
11791190
if self.proto >= 2:
11801191
code = _extension_registry.get((module_name, name), _NoValue)

Lib/test/picklecommon.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,17 @@ def __private_staticmethod():
419419
@classmethod
420420
def get_staticmethod(cls):
421421
return cls.__private_staticmethod
422+
423+
# For test_private_nested_classes
424+
class PrivateNestedClasses:
425+
@classmethod
426+
def get_nested(cls):
427+
return cls.__Nested
428+
429+
class __Nested:
430+
@classmethod
431+
def get_nested2(cls):
432+
return cls.__Nested2
433+
434+
class __Nested2:
435+
pass

Lib/test/pickletester.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4133,6 +4133,18 @@ def test_private_methods(self):
41334133
unpickled = self.loads(self.dumps(obj.get_staticmethod(), proto))
41344134
self.assertEqual(unpickled(), 44)
41354135

4136+
def test_private_nested_classes(self):
4137+
if self.py_version < (3, 15):
4138+
self.skipTest('not supported in Python < 3.15')
4139+
cls1 = PrivateNestedClasses.get_nested()
4140+
cls2 = cls1.get_nested2()
4141+
for proto in protocols:
4142+
with self.subTest(proto=proto):
4143+
unpickled = self.loads(self.dumps(cls1, proto))
4144+
self.assertIs(unpickled, cls1)
4145+
unpickled = self.loads(self.dumps(cls2, proto))
4146+
self.assertIs(unpickled, cls2)
4147+
41364148
def test_object_with_attrs(self):
41374149
obj = Object()
41384150
obj.a = 1

Misc/NEWS.d/next/Library/2020-07-14-23-54-18.bpo-33007.TyI3_Q.rst renamed to Misc/NEWS.d/next/Library/2020-07-14-23-54-18.gh-issue-77188.TyI3_Q.rst

File renamed without changes.

Modules/_pickle.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "pycore_pystate.h" // _PyThreadState_GET()
2020
#include "pycore_runtime.h" // _Py_ID()
2121
#include "pycore_setobject.h" // _PySet_NextEntry()
22+
#include "pycore_symtable.h" // _Py_Mangle()
2223
#include "pycore_sysmodule.h" // _PySys_GetSizeOf()
2324
#include "pycore_unicodeobject.h" // _PyUnicode_EqualToASCIIString()
2425

@@ -1928,6 +1929,37 @@ get_dotted_path(PyObject *name)
19281929
return PyUnicode_Split(name, _Py_LATIN1_CHR('.'), -1);
19291930
}
19301931

1932+
static PyObject *
1933+
join_dotted_path(PyObject *dotted_path)
1934+
{
1935+
return PyUnicode_Join(_Py_LATIN1_CHR('.'), dotted_path);
1936+
}
1937+
1938+
/* Returns -1 (with an exception set) on error, 0 if there were no changes,
1939+
* 1 if some names were mangled. */
1940+
static int
1941+
mangle_dotted_path(PyObject *dotted_path)
1942+
{
1943+
int rc = 0;
1944+
Py_ssize_t n = PyList_GET_SIZE(dotted_path);
1945+
for (Py_ssize_t i = n-1; i > 0; i--) {
1946+
PyObject *subpath = PyList_GET_ITEM(dotted_path, i);
1947+
if (_Py_IsPrivateName(subpath)) {
1948+
PyObject *parent = PyList_GET_ITEM(dotted_path, i-1);
1949+
PyObject *mangled = _Py_Mangle(parent, subpath);
1950+
if (mangled == NULL) {
1951+
return -1;
1952+
}
1953+
if (mangled != subpath) {
1954+
rc = 1;
1955+
}
1956+
PyList_SET_ITEM(dotted_path, i, mangled);
1957+
Py_DECREF(subpath);
1958+
}
1959+
}
1960+
return rc;
1961+
}
1962+
19311963
static int
19321964
check_dotted_path(PickleState *st, PyObject *obj, PyObject *dotted_path)
19331965
{
@@ -3809,6 +3841,15 @@ save_global(PickleState *st, PicklerObject *self, PyObject *obj,
38093841
dotted_path = get_dotted_path(global_name);
38103842
if (dotted_path == NULL)
38113843
goto error;
3844+
switch (mangle_dotted_path(dotted_path)) {
3845+
case -1:
3846+
goto error;
3847+
case 1:
3848+
Py_SETREF(global_name, join_dotted_path(dotted_path));
3849+
if (global_name == NULL) {
3850+
goto error;
3851+
}
3852+
}
38123853
module_name = whichmodule(st, obj, global_name, dotted_path);
38133854
if (module_name == NULL)
38143855
goto error;

Objects/classobject.c

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,25 +141,19 @@ method___reduce___impl(PyMethodObject *self)
141141
PyObject *funcself = PyMethod_GET_SELF(self);
142142
PyObject *func = PyMethod_GET_FUNCTION(self);
143143
PyObject *funcname = PyObject_GetAttr(func, &_Py_ID(__name__));
144-
Py_ssize_t len;
145144
if (funcname == NULL) {
146145
return NULL;
147146
}
148-
if (PyUnicode_Check(funcname) &&
149-
(len = PyUnicode_GET_LENGTH(funcname)) > 2 &&
150-
PyUnicode_READ_CHAR(funcname, 0) == '_' &&
151-
PyUnicode_READ_CHAR(funcname, 1) == '_' &&
152-
!(PyUnicode_READ_CHAR(funcname, len-1) == '_' &&
153-
PyUnicode_READ_CHAR(funcname, len-2) == '_'))
154-
{
155-
PyObject *name = PyObject_GetAttr((PyObject *)Py_TYPE(funcself),
156-
&_Py_ID(__name__));
157-
if (name == NULL) {
147+
if (_Py_IsPrivateName(funcname)) {
148+
PyObject *classname = PyType_Check(funcself)
149+
? PyType_GetName((PyTypeObject *)funcself)
150+
: PyType_GetName(Py_TYPE(funcself));
151+
if (classname == NULL) {
158152
Py_DECREF(funcname);
159153
return NULL;
160154
}
161-
Py_SETREF(funcname, _Py_Mangle(name, funcname));
162-
Py_DECREF(name);
155+
Py_SETREF(funcname, _Py_Mangle(classname, funcname));
156+
Py_DECREF(classname);
163157
if (funcname == NULL) {
164158
return NULL;
165159
}

Python/symtable.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3183,6 +3183,27 @@ _Py_MaybeMangle(PyObject *privateobj, PySTEntryObject *ste, PyObject *name)
31833183
return _Py_Mangle(privateobj, name);
31843184
}
31853185

3186+
int
3187+
_Py_IsPrivateName(PyObject *ident)
3188+
{
3189+
if (!PyUnicode_Check(ident)) {
3190+
return 0;
3191+
}
3192+
Py_ssize_t nlen = PyUnicode_GET_LENGTH(ident);
3193+
if (nlen < 3 ||
3194+
PyUnicode_READ_CHAR(ident, 0) != '_' ||
3195+
PyUnicode_READ_CHAR(ident, 1) != '_')
3196+
{
3197+
return 0;
3198+
}
3199+
if (PyUnicode_READ_CHAR(ident, nlen-1) == '_' &&
3200+
PyUnicode_READ_CHAR(ident, nlen-2) == '_')
3201+
{
3202+
return 0; /* Don't mangle __whatever__ */
3203+
}
3204+
return 1;
3205+
}
3206+
31863207
PyObject *
31873208
_Py_Mangle(PyObject *privateobj, PyObject *ident)
31883209
{

0 commit comments

Comments
 (0)