Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Doc/deprecations/pending-removal-in-future.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ although there is currently no date scheduled for their removal.

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

* :mod:`os.path`: :func:`os.path.commonprefix` is deprecated, use
:func:`os.path.commonpath` for path prefixes. The :func:`os.path.commonprefix`
function is being deprecated due to having a misleading name and module.
The function is not safe to use for path prefixes despite being included in a
module about path manipulation, meaning it is easy to accidentally
introduce path traversal vulnerabilities into Python programs by using this
function.

* :class:`!pydoc.ErrorDuringImport`: A tuple value for *exc_info* parameter is
deprecated, use an exception instance.

Expand Down
3 changes: 3 additions & 0 deletions Doc/library/base64.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ POST request.

If *ignorechars* is specified, it should be a :term:`bytes-like object`
containing characters to ignore from the input when *validate* is true.
If *ignorechars* contains the pad character ``'='``, the pad characters
presented before the end of the encoded data and the excess pad characters
will be ignored.
The default value of *validate* is ``True`` if *ignorechars* is specified,
``False`` otherwise.

Expand Down
3 changes: 3 additions & 0 deletions Doc/library/binascii.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ The :mod:`binascii` module defines the following functions:

If *ignorechars* is specified, it should be a :term:`bytes-like object`
containing characters to ignore from the input when *strict_mode* is true.
If *ignorechars* contains the pad character ``'='``, the pad characters
presented before the end of the encoded data and the excess pad characters
will be ignored.
The default value of *strict_mode* is ``True`` if *ignorechars* is specified,
``False`` otherwise.

Expand Down
4 changes: 2 additions & 2 deletions Doc/library/functools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ The :mod:`functools` module defines the following functions:
the *maxsize* at its default value of 128::

@lru_cache
def count_vowels(sentence):
return sum(sentence.count(vowel) for vowel in 'AEIOUaeiou')
def count_vowels(word):
return sum(word.count(vowel) for vowel in 'AEIOUaeiou')

If *maxsize* is set to ``None``, the LRU feature is disabled and the cache can
grow without bound.
Expand Down
8 changes: 8 additions & 0 deletions Doc/library/os.path.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ the :mod:`glob` module.)
.. versionchanged:: 3.6
Accepts a :term:`path-like object`.

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


.. function:: dirname(path, /)

Expand Down
4 changes: 3 additions & 1 deletion Doc/library/shutil.rst
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,9 @@ On Solaris :func:`os.sendfile` is used.

On Windows :func:`shutil.copyfile` uses a bigger default buffer size (1 MiB
instead of 64 KiB) and a :func:`memoryview`-based variant of
:func:`shutil.copyfileobj` is used.
:func:`shutil.copyfileobj` is used, which is still reads and writes in a loop.
:func:`shutil.copy2` uses the native ``CopyFile2`` call on Windows, which is the most
efficient method, supports copy-on-write, and preserves metadata.

If the fast-copy operation fails and no data was written in the destination
file then shutil will silently fallback on using less efficient
Expand Down
6 changes: 5 additions & 1 deletion Doc/reference/simple_stmts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,9 @@ where the :keyword:`import` statement occurs.

The *public names* defined by a module are determined by checking the module's
namespace for a variable named ``__all__``; if defined, it must be a sequence
of strings which are names defined or imported by that module. The names
of strings which are names defined or imported by that module.
Names containing non-ASCII characters must be in the `normalization form`_
NFKC; see :ref:`lexical-names-nonascii` for details. The names
given in ``__all__`` are all considered public and are required to exist. If
``__all__`` is not defined, the set of public names includes all names found
in the module's namespace which do not begin with an underscore character
Expand Down Expand Up @@ -865,6 +867,8 @@ determine dynamically the modules to be loaded.

.. audit-event:: import module,filename,sys.path,sys.meta_path,sys.path_hooks import

.. _normalization form: https://www.unicode.org/reports/tr15/#Norm_Forms

.. _future:

Future statements
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,13 @@ os.path
(Contributed by Petr Viktorin for :cve:`2025-4517`.)


pickle
------

* Add support for pickling private methods and nested classes.
(Contributed by Zackery Spytz and Serhiy Storchaka in :gh:`77188`.)


resource
--------

Expand Down
7 changes: 6 additions & 1 deletion Include/internal/pycore_symtable.h
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,12 @@ extern int _PySymtable_LookupOptional(struct symtable *, void *, PySTEntryObject
extern void _PySymtable_Free(struct symtable *);

extern PyObject *_Py_MaybeMangle(PyObject *privateobj, PySTEntryObject *ste, PyObject *name);
extern PyObject* _Py_Mangle(PyObject *p, PyObject *name);

// Export for '_pickle' shared extension
PyAPI_FUNC(PyObject *)
_Py_Mangle(PyObject *, PyObject *);
PyAPI_FUNC(int)
_Py_IsPrivateName(PyObject *);

/* Flags for def-use information */

Expand Down
9 changes: 9 additions & 0 deletions Lib/genericpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ def getctime(filename, /):
# Return the longest prefix of all list elements.
def commonprefix(m, /):
"Given a list of pathnames, returns the longest common leading component"
import warnings
warnings.warn('os.path.commonprefix() is deprecated. Use '
'os.path.commonpath() for longest path prefix.',
category=DeprecationWarning,
stacklevel=2)
return _commonprefix(m)

def _commonprefix(m, /):
"Internal implementation of commonprefix()"
if not m: return ''
# Some people pass in a list of pathname parts to operate in an OS-agnostic
# fashion; don't try to translate in that case as that's an abuse of the
Expand Down
11 changes: 11 additions & 0 deletions Lib/pickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,17 @@ def save_global(self, obj, name=None):
if name is None:
name = obj.__name__

if '.__' in name:
# Mangle names of private attributes.
dotted_path = name.split('.')
for i, subpath in enumerate(dotted_path):
if i and subpath.startswith('__') and not subpath.endswith('__'):
prev = prev.lstrip('_')
if prev:
dotted_path[i] = f"_{prev.lstrip('_')}{subpath}"
prev = subpath
name = '.'.join(dotted_path)

module_name = whichmodule(obj, name)
if self.proto >= 2:
code = _extension_registry.get((module_name, name), _NoValue)
Expand Down
2 changes: 1 addition & 1 deletion Lib/posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ def relpath(path, start=None):
start_list = start_tail.split(sep) if start_tail else []
path_list = path_tail.split(sep) if path_tail else []
# Work out how much of the filepath is shared by start and path.
i = len(commonprefix([start_list, path_list]))
i = len(genericpath._commonprefix([start_list, path_list]))

rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
if not rel_list:
Expand Down
45 changes: 45 additions & 0 deletions Lib/test/picklecommon.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,48 @@ 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

# For test_private_nested_classes
class PrivateNestedClasses:
@classmethod
def get_nested(cls):
return cls.__Nested

class __Nested:
@classmethod
def get_nested2(cls):
return cls.__Nested2

class __Nested2:
pass
27 changes: 27 additions & 0 deletions Lib/test/pickletester.py
Original file line number Diff line number Diff line change
Expand Up @@ -4118,6 +4118,33 @@ 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_private_nested_classes(self):
if self.py_version < (3, 15):
self.skipTest('not supported in Python < 3.15')
cls1 = PrivateNestedClasses.get_nested()
cls2 = cls1.get_nested2()
for proto in protocols:
with self.subTest(proto=proto):
unpickled = self.loads(self.dumps(cls1, proto))
self.assertIs(unpickled, cls1)
unpickled = self.loads(self.dumps(cls2, proto))
self.assertIs(unpickled, cls2)

def test_object_with_attrs(self):
obj = Object()
obj.a = 1
Expand Down
13 changes: 9 additions & 4 deletions Lib/test/test_base64.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,14 +306,20 @@ def test_b64decode_invalid_chars(self):
# issue 1466065: Test some invalid characters.
tests = ((b'%3d==', b'\xdd', b'%$'),
(b'$3d==', b'\xdd', b'%$'),
(b'[==', b'', None),
(b'[==', b'', b'[='),
(b'YW]3=', b'am', b']'),
(b'3{d==', b'\xdd', b'{}'),
(b'3d}==', b'\xdd', b'{}'),
(b'@@', b'', b'@!'),
(b'!', b'', b'@!'),
(b"YWJj\n", b"abc", b'\n'),
(b'YWJj\nYWI=', b'abcab', b'\n'),
(b'=YWJj', b'abc', b'='),
(b'Y=WJj', b'abc', b'='),
(b'Y==WJj', b'abc', b'='),
(b'Y===WJj', b'abc', b'='),
(b'YW=Jj', b'abc', b'='),
(b'YWJj=', b'abc', b'='),
(b'YW\nJj', b'abc', b'\n'),
(b'YW\nJj', b'abc', bytearray(b'\n')),
(b'YW\nJj', b'abc', memoryview(b'\n')),
Expand All @@ -335,9 +341,8 @@ def test_b64decode_invalid_chars(self):
with self.assertRaises(binascii.Error):
# Even empty ignorechars enables the strict mode.
base64.b64decode(bstr, ignorechars=b'')
if ignorechars is not None:
r = base64.b64decode(bstr, ignorechars=ignorechars)
self.assertEqual(r, res)
r = base64.b64decode(bstr, ignorechars=ignorechars)
self.assertEqual(r, res)

with self.assertRaises(TypeError):
base64.b64decode(b'', ignorechars='')
Expand Down
Loading
Loading