Skip to content

Commit 7dbd163

Browse files
authored
Merge branch 'main' into refactor-dict-tuple-opt
2 parents 174a82b + 957f9fe commit 7dbd163

File tree

17 files changed

+231
-6
lines changed

17 files changed

+231
-6
lines changed

Doc/deprecations/pending-removal-in-future.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ although there is currently no date scheduled for their removal.
7878

7979
* :mod:`os`: Calling :func:`os.register_at_fork` in a multi-threaded process.
8080

81+
* :mod:`os.path`: :func:`os.path.commonprefix` is deprecated, use
82+
:func:`os.path.commonpath` for path prefixes. The :func:`os.path.commonprefix`
83+
function is being deprecated due to having a misleading name and module.
84+
The function is not safe to use for path prefixes despite being included in a
85+
module about path manipulation, meaning it is easy to accidentally
86+
introduce path traversal vulnerabilities into Python programs by using this
87+
function.
88+
8189
* :class:`!pydoc.ErrorDuringImport`: A tuple value for *exc_info* parameter is
8290
deprecated, use an exception instance.
8391

Doc/library/os.path.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ the :mod:`glob` module.)
120120
.. versionchanged:: 3.6
121121
Accepts a :term:`path-like object`.
122122

123+
.. deprecated:: next
124+
Deprecated in favor of :func:`os.path.commonpath` for path prefixes.
125+
The :func:`os.path.commonprefix` function is being deprecated due to
126+
having a misleading name and module. The function is not safe to use for
127+
path prefixes despite being included in a module about path manipulation,
128+
meaning it is easy to accidentally introduce path traversal
129+
vulnerabilities into Python programs by using this function.
130+
123131

124132
.. function:: dirname(path, /)
125133

Doc/whatsnew/3.15.rst

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

706706

707+
pickle
708+
------
709+
710+
* Add support for pickling private methods and nested classes.
711+
(Contributed by Zackery Spytz and Serhiy Storchaka in :gh:`77188`.)
712+
713+
707714
resource
708715
--------
709716

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/genericpath.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ def getctime(filename, /):
105105
# Return the longest prefix of all list elements.
106106
def commonprefix(m, /):
107107
"Given a list of pathnames, returns the longest common leading component"
108+
import warnings
109+
warnings.warn('os.path.commonprefix() is deprecated. Use '
110+
'os.path.commonpath() for longest path prefix.',
111+
category=DeprecationWarning,
112+
stacklevel=2)
113+
return _commonprefix(m)
114+
115+
def _commonprefix(m, /):
116+
"Internal implementation of commonprefix()"
108117
if not m: return ''
109118
# Some people pass in a list of pathname parts to operate in an OS-agnostic
110119
# fashion; don't try to translate in that case as that's an abuse of the

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/posixpath.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ def relpath(path, start=None):
542542
start_list = start_tail.split(sep) if start_tail else []
543543
path_list = path_tail.split(sep) if path_tail else []
544544
# Work out how much of the filepath is shared by start and path.
545-
i = len(commonprefix([start_list, path_list]))
545+
i = len(genericpath._commonprefix([start_list, path_list]))
546546

547547
rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
548548
if not rel_list:

Lib/test/picklecommon.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,48 @@ def pie(self):
388388
class Subclass(tuple):
389389
class Nested(str):
390390
pass
391+
392+
# For test_private_methods
393+
class PrivateMethods:
394+
def __init__(self, value):
395+
self.value = value
396+
397+
def __private_method(self):
398+
return self.value
399+
400+
def get_method(self):
401+
return self.__private_method
402+
403+
@classmethod
404+
def get_unbound_method(cls):
405+
return cls.__private_method
406+
407+
@classmethod
408+
def __private_classmethod(cls):
409+
return 43
410+
411+
@classmethod
412+
def get_classmethod(cls):
413+
return cls.__private_classmethod
414+
415+
@staticmethod
416+
def __private_staticmethod():
417+
return 44
418+
419+
@classmethod
420+
def get_staticmethod(cls):
421+
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4118,6 +4118,33 @@ def test_c_methods(self):
41184118
with self.subTest(proto=proto, descr=descr):
41194119
self.assertRaises(TypeError, self.dumps, descr, proto)
41204120

4121+
def test_private_methods(self):
4122+
if self.py_version < (3, 15):
4123+
self.skipTest('not supported in Python < 3.15')
4124+
obj = PrivateMethods(42)
4125+
for proto in protocols:
4126+
with self.subTest(proto=proto):
4127+
unpickled = self.loads(self.dumps(obj.get_method(), proto))
4128+
self.assertEqual(unpickled(), 42)
4129+
unpickled = self.loads(self.dumps(obj.get_unbound_method(), proto))
4130+
self.assertEqual(unpickled(obj), 42)
4131+
unpickled = self.loads(self.dumps(obj.get_classmethod(), proto))
4132+
self.assertEqual(unpickled(), 43)
4133+
unpickled = self.loads(self.dumps(obj.get_staticmethod(), proto))
4134+
self.assertEqual(unpickled(), 44)
4135+
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+
41214148
def test_object_with_attrs(self):
41224149
obj = Object()
41234150
obj.a = 1

Lib/test/test_genericpath.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ def test_no_argument(self):
3434
.format(self.pathmodule.__name__, attr))
3535

3636
def test_commonprefix(self):
37+
with warnings_helper.check_warnings((".*commonpath().*", DeprecationWarning)):
38+
self.do_test_commonprefix()
39+
40+
def do_test_commonprefix(self):
3741
commonprefix = self.pathmodule.commonprefix
3842
self.assertEqual(
3943
commonprefix([]),
@@ -606,8 +610,9 @@ def test_path_isdir(self):
606610
self.assertPathEqual(os.path.isdir)
607611

608612
def test_path_commonprefix(self):
609-
self.assertEqual(os.path.commonprefix([self.file_path, self.file_name]),
610-
self.file_name)
613+
with warnings_helper.check_warnings((".*commonpath().*", DeprecationWarning)):
614+
self.assertEqual(os.path.commonprefix([self.file_path, self.file_name]),
615+
self.file_name)
611616

612617
def test_path_getsize(self):
613618
self.assertPathEqual(os.path.getsize)

0 commit comments

Comments
 (0)