Skip to content
31 changes: 31 additions & 0 deletions Lib/test/picklecommon.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,34 @@ def pie(self):
class Subclass(tuple):
class Nested(str):
pass

# For test_private_methods
class PrivateMethods:
def __init__(self, value):
self.value = value

def __private_method(self):
return self.value

def get_method(self):
return self.__private_method

@classmethod
def get_unbound_method(cls):
return cls.__private_method

@classmethod
def __private_classmethod(cls):
return 43

@classmethod
def get_classmethod(cls):
return cls.__private_classmethod

@staticmethod
def __private_staticmethod():
return 44

@classmethod
def get_staticmethod(cls):
return cls.__private_staticmethod
15 changes: 15 additions & 0 deletions Lib/test/pickletester.py
Original file line number Diff line number Diff line change
Expand Up @@ -4117,6 +4117,21 @@ def test_c_methods(self):
with self.subTest(proto=proto, descr=descr):
self.assertRaises(TypeError, self.dumps, descr, proto)

def test_private_methods(self):
if self.py_version < (3, 15):
self.skipTest('not supported in Python < 3.15')
obj = PrivateMethods(42)
for proto in protocols:
with self.subTest(proto=proto):
unpickled = self.loads(self.dumps(obj.get_method(), proto))
self.assertEqual(unpickled(), 42)
unpickled = self.loads(self.dumps(obj.get_unbound_method(), proto))
self.assertEqual(unpickled(obj), 42)
unpickled = self.loads(self.dumps(obj.get_classmethod(), proto))
self.assertEqual(unpickled(), 43)
unpickled = self.loads(self.dumps(obj.get_staticmethod(), proto))
self.assertEqual(unpickled(), 44)

def test_compat_pickle(self):
if self.py_version < (3, 4):
self.skipTest("doesn't work in Python < 3.4'")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The :mod:`pickle` module now properly handles name-mangled private methods.
21 changes: 21 additions & 0 deletions Objects/classobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "pycore_object.h"
#include "pycore_pyerrors.h"
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "pycore_symtable.h" // _Py_Mangle()
#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS()


Expand Down Expand Up @@ -140,9 +141,29 @@ method___reduce___impl(PyMethodObject *self)
PyObject *funcself = PyMethod_GET_SELF(self);
PyObject *func = PyMethod_GET_FUNCTION(self);
PyObject *funcname = PyObject_GetAttr(func, &_Py_ID(__name__));
Py_ssize_t len;
if (funcname == NULL) {
return NULL;
}
if (PyUnicode_Check(funcname) &&
(len = PyUnicode_GET_LENGTH(funcname)) > 2 &&
PyUnicode_READ_CHAR(funcname, 0) == '_' &&
PyUnicode_READ_CHAR(funcname, 1) == '_' &&
!(PyUnicode_READ_CHAR(funcname, len-1) == '_' &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would also fail to trigger on methods named ___ and ____ (3 and 4 underscores). it seems rather pedantic to care, but perhaps add a len > 4 check before checking for trailing _s and cover def ____(self): in the regression test?

PyUnicode_READ_CHAR(funcname, len-2) == '_'))
{
PyObject *name = PyObject_GetAttr((PyObject *)Py_TYPE(funcself),
&_Py_ID(__name__));
if (name == NULL) {
Py_DECREF(funcname);
return NULL;
}
Py_SETREF(funcname, _Py_Mangle(name, funcname));
Py_DECREF(name);
if (funcname == NULL) {
return NULL;
}
}
return Py_BuildValue(
"N(ON)", _PyEval_GetBuiltin(&_Py_ID(getattr)), funcself, funcname);
}
Expand Down
Loading