From 6d5be4b1d6ca91a18e76ae8dad2c5e94837d6309 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 18 May 2026 21:55:27 +0300 Subject: [PATCH 1/8] gh-149977: Fix extra output of `-m test test_lazy_import` (#149978) --- Lib/test/test_lazy_import/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 5d770eeae07a15..bcbf1a23233ba8 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -10,6 +10,7 @@ import unittest import tempfile import os +import contextlib from test import support from test.support.script_helper import assert_python_ok @@ -441,10 +442,14 @@ def tearDown(self): def test_lazy_import_pkg(self): """lazy import of package submodule should load the package.""" - import test.test_lazy_import.data.lazy_import_pkg + out = io.StringIO() + + with contextlib.redirect_stdout(out): + import test.test_lazy_import.data.lazy_import_pkg self.assertIn("test.test_lazy_import.data.pkg", sys.modules) self.assertIn("test.test_lazy_import.data.pkg.bar", sys.modules) + self.assertIn("BAR_MODULE_LOADED", out.getvalue()) def test_lazy_import_pkg_cross_import(self): """Cross-imports within package should preserve lazy imports.""" From 5775aa8e295102156de14fd1ba284722c6ede95a Mon Sep 17 00:00:00 2001 From: ankhikarmakar <100954262+ankhikarmakar@users.noreply.github.com> Date: Mon, 18 May 2026 12:31:15 -0700 Subject: [PATCH 2/8] gh-149980: Strip all trailing slashes from GNU long directory names in tarfile (GH-150019) --- Lib/tarfile.py | 2 +- Lib/test/test_tarfile.py | 21 +++++++++++++++++++ ...-05-19-12-00-00.gh-issue-149980.Kx7p2A.rst | 3 +++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-19-12-00-00.gh-issue-149980.Kx7p2A.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 772b51295fcfbe..87500c726ce9a8 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -1425,7 +1425,7 @@ def _proc_gnulong(self, tarfile): # Remove redundant slashes from directories. This is to be consistent # with frombuf(). if next.isdir(): - next.name = next.name.removesuffix("/") + next.name = next.name.rstrip("/") return next diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 8e213a8f999218..02fd9620bcf33d 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -1313,6 +1313,27 @@ def _fs_supports_holes(): else: return False + def test_gnulong_dirname_strips_all_trailing_slashes(self): + # gh-149980: _proc_gnulong must normalize trailing slashes the same + # way _frombuf and _proc_builtin do (rstrip, not removesuffix), so + # a GNU long-name directory entry agrees with a short-name one. + long_name = "a" * 120 + "///" # > 100 bytes => GNUTYPE_LONGNAME + short_name = "b" * 20 + "///" + + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w", + format=tarfile.GNU_FORMAT) as tar: + for name in (short_name, long_name): + info = tarfile.TarInfo(name=name) + info.type = tarfile.DIRTYPE + tar.addfile(info) + + buf.seek(0) + with tarfile.open(fileobj=buf, mode="r") as tar: + names = [m.name for m in tar.getmembers()] + + self.assertEqual(names, ["b" * 20, "a" * 120]) + class PaxReadTest(LongnameTest, ReadTest, unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2026-05-19-12-00-00.gh-issue-149980.Kx7p2A.rst b/Misc/NEWS.d/next/Library/2026-05-19-12-00-00.gh-issue-149980.Kx7p2A.rst new file mode 100644 index 00000000000000..b056f6a3cda25d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-19-12-00-00.gh-issue-149980.Kx7p2A.rst @@ -0,0 +1,3 @@ +Fix :mod:`tarfile` so that GNU long-name directory entries have all +trailing slashes stripped from their names, matching the behavior for +short-name entries. Previously, only a single trailing slash was removed. From 0aa59ce2d4f007a9d19740eb2f6230ed302096f7 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 18 May 2026 18:42:55 -0400 Subject: [PATCH 3/8] gh-72088: clarify `inspect.ismethod` and `inspect.isfunction` (and related) usage with class-level access (GH-150013) Co-authored-by: CHINMAY <89741289+Das-Chinmay@users.noreply.github.com> --- Doc/library/inspect.rst | 45 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 48ae9147587c64..92840e702fbbfe 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -424,12 +424,39 @@ attributes (see :ref:`import-mod-attrs` for module attributes): Return ``True`` if the object is a bound method written in Python. + .. note:: -.. function:: ispackage(object) + For example, given this class:: - Return ``True`` if the object is a :term:`package`. + >>> class Greeter: + ... def say_hello(self): + ... print('hello!') - .. versionadded:: 3.14 + A bound method (also known as an *instance method*) is created when + accessing ``say_hello`` (a :term:`function` defined in the + ``Greeter`` namespace) through an instance of the ``Greeter`` class:: + + >>> instance = Greeter() + + >>> instance.say_hello + > + >>> ismethod(instance.say_hello) + True + >>> isfunction(instance.say_hello) + False + + Accessing ``say_hello`` through the ``Greeter`` class will return the + function itself. For this function, :func:`ismethod` will return + ``False``, but :func:`isfunction` will return ``True``:: + + >>> Greeter.say_hello + + >>> ismethod(Greeter.say_hello) + False + >>> isfunction(Greeter.say_hello) + True + + See :ref:`typesmethods` for details. .. function:: isfunction(object) @@ -437,11 +464,23 @@ attributes (see :ref:`import-mod-attrs` for module attributes): Return ``True`` if the object is a Python function, which includes functions created by a :term:`lambda` expression. + See the note for :func:`~inspect.ismethod` for an example. + + +.. function:: ispackage(object) + + Return ``True`` if the object is a :term:`package`. + + .. versionadded:: 3.14 + .. function:: isgeneratorfunction(object) Return ``True`` if the object is a Python generator function. + It also returns ``True`` for bound methods created from Python generator functions + (see :ref:`typesmethods` for more information). + .. versionchanged:: 3.8 Functions wrapped in :func:`functools.partial` now return ``True`` if the wrapped function is a Python generator function. From 9eb50a40b4186d665aa98c8dbea7cf60fb2aacca Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 19 May 2026 00:45:35 +0200 Subject: [PATCH 4/8] gh-149879: Fix multiprocessing tests on Cygwin (#150031) * Disable AF_UNIX connection family on Cygwin. * forkserver start method is not available on Cygwin: update tests for that. * test_logging calls multiprocessing.get_all_start_methods(). --- Lib/multiprocessing/connection.py | 2 +- Lib/multiprocessing/context.py | 4 +++- Lib/test/_test_multiprocessing.py | 7 +++++-- Lib/test/test_logging.py | 12 +++--------- Lib/test/test_multiprocessing_forkserver/__init__.py | 2 +- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py index e37ec07d722ca8..be2fc3edf5dce6 100644 --- a/Lib/multiprocessing/connection.py +++ b/Lib/multiprocessing/connection.py @@ -50,7 +50,7 @@ default_family = 'AF_INET' families = ['AF_INET'] -if hasattr(socket, 'AF_UNIX'): +if hasattr(socket, 'AF_UNIX') and sys.platform != 'cygwin': default_family = 'AF_UNIX' families += ['AF_UNIX'] diff --git a/Lib/multiprocessing/context.py b/Lib/multiprocessing/context.py index e1d251456024c0..45c393798deaca 100644 --- a/Lib/multiprocessing/context.py +++ b/Lib/multiprocessing/context.py @@ -326,8 +326,10 @@ def _check_available(self): _concrete_contexts = { 'fork': ForkContext(), 'spawn': SpawnContext(), - 'forkserver': ForkServerContext(), } + if reduction.HAVE_SEND_HANDLE: + _concrete_contexts['forkserver'] = ForkServerContext() + # bpo-33725: running arbitrary code after fork() is no longer reliable # on macOS since macOS 10.14 (Mojave). Use spawn by default instead. # gh-84559: We changed everyones default to a thread safeish one in 3.14. diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 580d9f2b32544e..f044358dcbd40f 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6143,7 +6143,10 @@ def test_preload_resources(self): @only_run_in_spawn_testsuite("avoids redundant testing.") def test_mixed_startmethod(self): # Fork-based locks cannot be used with spawned process - for process_method in ["spawn", "forkserver"]: + test_methods = ["spawn"] + if "forkserver" in multiprocessing.get_all_start_methods(): + test_methods.append("forkserver") + for process_method in test_methods: queue = multiprocessing.get_context("fork").Queue() process_ctx = multiprocessing.get_context(process_method) p = process_ctx.Process(target=close_queue, args=(queue,)) @@ -6152,7 +6155,7 @@ def test_mixed_startmethod(self): p.start() # non-fork-based locks can be used with all other start methods - for queue_method in ["spawn", "forkserver"]: + for queue_method in test_methods: for process_method in multiprocessing.get_all_start_methods(): queue = multiprocessing.get_context(queue_method).Queue() process_ctx = multiprocessing.get_context(process_method) diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index f2cbc2514fce53..2ab9e0b336c9fb 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -4065,11 +4065,7 @@ def test_config_reject_simple_queue_handler_multiprocessing_context(self): # and thus cannot be used as a queue-like object (gh-124653) import multiprocessing - - if support.MS_WINDOWS: - start_methods = ['spawn'] - else: - start_methods = ['spawn', 'fork', 'forkserver'] + start_methods = multiprocessing.get_all_start_methods() for start_method in start_methods: with self.subTest(start_method=start_method): @@ -4085,10 +4081,8 @@ def test_config_reject_simple_queue_handler_multiprocessing_context(self): " assertions in multiprocessing") def test_config_queue_handler_multiprocessing_context(self): # regression test for gh-121723 - if support.MS_WINDOWS: - start_methods = ['spawn'] - else: - start_methods = ['spawn', 'fork', 'forkserver'] + import multiprocessing + start_methods = multiprocessing.get_all_start_methods() for start_method in start_methods: with self.subTest(start_method=start_method): ctx = multiprocessing.get_context(start_method) diff --git a/Lib/test/test_multiprocessing_forkserver/__init__.py b/Lib/test/test_multiprocessing_forkserver/__init__.py index 7b1b884ab297b5..c58375e2861c62 100644 --- a/Lib/test/test_multiprocessing_forkserver/__init__.py +++ b/Lib/test/test_multiprocessing_forkserver/__init__.py @@ -6,7 +6,7 @@ if support.PGO: raise unittest.SkipTest("test is not helpful for PGO") -if sys.platform == "win32": +if sys.platform in ("win32", "cygwin"): raise unittest.SkipTest("forkserver is not available on Windows") if not support.has_fork_support: From 9770e32ce07110f0c8c7a381604ec9a490028eed Mon Sep 17 00:00:00 2001 From: nessita <124304+nessita@users.noreply.github.com> Date: Mon, 18 May 2026 20:00:27 -0300 Subject: [PATCH 5/8] gh-86533: Restore os.makedirs() ability to apply *mode* recursively (GH-150011) bpo-42367: Restore os.makedirs() and pathlib.mkdir() ability to apply *mode* recursively via a new parent_mode= keyword argument. Co-authored-by: Zackery Spytz Co-authored-by: Erlend E. Aasland Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> Co-authored-by: Gregory P. Smith --- Doc/library/os.rst | 14 ++- Doc/library/pathlib.rst | 12 +- Doc/whatsnew/3.15.rst | 8 ++ Lib/os.py | 15 ++- Lib/pathlib/__init__.py | 8 +- Lib/test/test_os/test_os.py | 100 ++++++++++++++-- Lib/test/test_pathlib/test_pathlib.py | 107 ++++++++++++++++++ ...-08-30-07-44-30.gh-issue-86533.pathlib.rst | 4 + 8 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-08-30-07-44-30.gh-issue-86533.pathlib.rst diff --git a/Doc/library/os.rst b/Doc/library/os.rst index f6be2dec0729f2..1a759fd9e7dc91 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -2549,7 +2549,8 @@ features: Windows now handles a *mode* of ``0o700``. -.. function:: makedirs(name, mode=0o777, exist_ok=False) +.. function:: makedirs(name, mode=0o777, exist_ok=False, *, \ + parent_mode=None) .. index:: single: directory; creating @@ -2567,6 +2568,12 @@ features: If *exist_ok* is ``False`` (the default), a :exc:`FileExistsError` is raised if the target directory already exists. + If *parent_mode* is not ``None``, it is used as the mode for any + newly-created, intermediate-level directories. Like *mode*, it is + combined with the process's umask value; see :ref:`the mkdir() + description `. Otherwise, intermediate directories are + created with the default mode, which is also subject to the umask. + .. note:: :func:`makedirs` will become confused if the path elements to create @@ -2593,6 +2600,11 @@ features: The *mode* argument no longer affects the file permission bits of newly created intermediate-level directories. + .. versionadded:: 3.15 + The *parent_mode* parameter. To match the behavior from Python 3.6 and + earlier (where *mode* was applied to all created directories), pass + ``parent_mode=mode``. + .. function:: mkfifo(path, mode=0o666, *, dir_fd=None) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 923cd4a2c4c1f1..ab92c142c37a4f 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1518,7 +1518,8 @@ Creating files and directories :meth:`~Path.write_bytes` methods are often used to create files. -.. method:: Path.mkdir(mode=0o777, parents=False, exist_ok=False) +.. method:: Path.mkdir(mode=0o777, parents=False, exist_ok=False, *, \ + parent_mode=None) Create a new directory at this given path. If *mode* is given, it is combined with the process's ``umask`` value to determine the file mode @@ -1529,6 +1530,12 @@ Creating files and directories as needed; they are created with the default permissions without taking *mode* into account (mimicking the POSIX ``mkdir -p`` command). + If *parent_mode* is not ``None``, it is used as the mode for any + newly-created, intermediate-level directories when *parents* is true. + Like *mode*, it is combined with the process's ``umask`` value. + Otherwise, intermediate directories are created with the default + permissions (also subject to the umask). + If *parents* is false (the default), a missing parent raises :exc:`FileNotFoundError`. @@ -1542,6 +1549,9 @@ Creating files and directories .. versionchanged:: 3.5 The *exist_ok* parameter was added. + .. versionadded:: 3.15 + The *parent_mode* parameter. + .. method:: Path.symlink_to(target, target_is_directory=False) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index a048e51512d592..93d9b18d585634 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1285,6 +1285,10 @@ os glibc versions 2.28 and later. (Contributed by Jeffrey Bosboom and Victor Stinner in :gh:`83714`.) +* :func:`os.makedirs` function now has a *parent_mode* parameter that allows + specifying the mode for intermediate directories. This can be used to match + the behavior from Python 3.6 and earlier by passing ``parent_mode=mode``. + (Contributed by Zackery Spytz and Gregory P. Smith in :gh:`86533`.) os.path ------- @@ -2057,6 +2061,10 @@ importlib.resources pathlib ------- +* :meth:`pathlib.Path.mkdir` now has a *parent_mode* parameter that allows + specifying the mode for intermediate directories when ``parents=True``. + (Contributed by Gregory P. Smith in :gh:`86533`.) + * Removed deprecated :meth:`!pathlib.PurePath.is_reserved`. Use :func:`os.path.isreserved` to detect reserved paths on Windows. (Contributed by Nikita Sobolev in :gh:`133875`.) diff --git a/Lib/os.py b/Lib/os.py index 52cbc5bc85864e..1ca4648cc95c3e 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -219,14 +219,17 @@ def _add(str, fn): # Super directory utilities. # (Inspired by Eric Raymond; the doc strings are mostly his) -def makedirs(name, mode=0o777, exist_ok=False): - """makedirs(name [, mode=0o777][, exist_ok=False]) +def makedirs(name, mode=0o777, exist_ok=False, *, parent_mode=None): + """makedirs(name [, mode=0o777][, exist_ok=False][, parent_mode=None]) Super-mkdir; create a leaf directory and all intermediate ones. Works like mkdir, except that any intermediate path segment (not just the rightmost) will be created if it does not exist. If the target directory already exists, raise an OSError if exist_ok is False. Otherwise no exception is - raised. This is recursive. + raised. If parent_mode is not None, it will be used as the mode for any + newly-created, intermediate-level directories. Otherwise, intermediate + directories are created with the default permissions (respecting umask). + This is recursive. """ head, tail = path.split(name) @@ -234,7 +237,11 @@ def makedirs(name, mode=0o777, exist_ok=False): head, tail = path.split(head) if head and tail and not path.exists(head): try: - makedirs(head, exist_ok=exist_ok) + if parent_mode is not None: + makedirs(head, mode=parent_mode, exist_ok=exist_ok, + parent_mode=parent_mode) + else: + makedirs(head, exist_ok=exist_ok) except FileExistsError: # Defeats race condition when another thread created the path pass diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 295f633824a6ef..ffec9c545ee11f 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -1204,7 +1204,7 @@ def touch(self, mode=0o666, exist_ok=True): fd = os.open(self, flags, mode) os.close(fd) - def mkdir(self, mode=0o777, parents=False, exist_ok=False): + def mkdir(self, mode=0o777, parents=False, exist_ok=False, *, parent_mode=None): """ Create a new directory at this given path. """ @@ -1213,7 +1213,11 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False): except FileNotFoundError: if not parents or self.parent == self: raise - self.parent.mkdir(parents=True, exist_ok=True) + if parent_mode is not None: + self.parent.mkdir(mode=parent_mode, parents=True, exist_ok=True, + parent_mode=parent_mode) + else: + self.parent.mkdir(parents=True, exist_ok=True) self.mkdir(mode, parents=False, exist_ok=exist_ok) except OSError: # Cannot rely on checking for EEXIST, since the operating system diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index e71c28424e095f..68cb32cd40be30 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -2139,6 +2139,94 @@ def test_mode(self): self.assertEqual(os.stat(path).st_mode & 0o777, 0o555) self.assertEqual(os.stat(parent).st_mode & 0o777, 0o775) + @unittest.skipIf( + support.is_emscripten or support.is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_mode_with_parent_mode(self): + # Test the parent_mode parameter + parent = os.path.join(os_helper.TESTFN, 'dir1') + path = os.path.join(parent, 'dir2') + with os_helper.temp_umask(0o002): + # Specify mode for both leaf and parent directories + os.makedirs(path, 0o770, parent_mode=0o750) + self.assertTrue(os.path.exists(path)) + self.assertTrue(os.path.isdir(path)) + if os.name != 'nt': + # Leaf directory gets the mode parameter + self.assertEqual(os.stat(path).st_mode & 0o777, 0o770) + # Parent directory gets the parent_mode parameter + self.assertEqual(os.stat(parent).st_mode & 0o777, 0o750) + + @unittest.skipIf( + support.is_emscripten or support.is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_parent_mode_deep_hierarchy(self): + # Test parent_mode with deep directory hierarchy + base = os.path.join(os_helper.TESTFN, 'dir1', 'dir2', 'dir3') + with os_helper.temp_umask(0o002): + os.makedirs(base, 0o755, parent_mode=0o700) + self.assertTrue(os.path.exists(base)) + if os.name != 'nt': + # Check that all parent directories have parent_mode + level1 = os.path.join(os_helper.TESTFN, 'dir1') + level2 = os.path.join(level1, 'dir2') + self.assertEqual(os.stat(level1).st_mode & 0o777, 0o700) + self.assertEqual(os.stat(level2).st_mode & 0o777, 0o700) + # Leaf directory has the regular mode + self.assertEqual(os.stat(base).st_mode & 0o777, 0o755) + + @unittest.skipIf( + support.is_emscripten or support.is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_parent_mode_same_as_mode(self): + # Test emulating Python 3.6 behavior by setting parent_mode=mode + parent = os.path.join(os_helper.TESTFN, 'dir1') + path = os.path.join(parent, 'dir2') + with os_helper.temp_umask(0o002): + os.makedirs(path, 0o705, parent_mode=0o705) + self.assertTrue(os.path.exists(path)) + if os.name != 'nt': + # Both directories should have the same mode + self.assertEqual(os.stat(path).st_mode & 0o777, 0o705) + self.assertEqual(os.stat(parent).st_mode & 0o777, 0o705) + + @unittest.skipIf( + support.is_emscripten or support.is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_parent_mode_combined_with_umask(self): + # parent_mode, like mode, is combined with the process umask; it does + # not bypass it. + parent = os.path.join(os_helper.TESTFN, 'dir1') + path = os.path.join(parent, 'dir2') + with os_helper.temp_umask(0o022): + os.makedirs(path, 0o777, parent_mode=0o777) + self.assertTrue(os.path.isdir(path)) + if os.name != 'nt': + # 0o777 is masked down to 0o755 by the 0o022 umask, for both + # the leaf (mode) and the parent (parent_mode). + self.assertEqual(os.stat(path).st_mode & 0o777, 0o755) + self.assertEqual(os.stat(parent).st_mode & 0o777, 0o755) + @unittest.skipIf( support.is_wasi, "WASI's umask is a stub." @@ -2212,15 +2300,9 @@ def test_win32_mkdir_700(self): ) def tearDown(self): - path = os.path.join(os_helper.TESTFN, 'dir1', 'dir2', 'dir3', - 'dir4', 'dir5', 'dir6') - # If the tests failed, the bottom-most directory ('../dir6') - # may not have been created, so we look for the outermost directory - # that exists. - while not os.path.exists(path) and path != os_helper.TESTFN: - path = os.path.dirname(path) - - os.removedirs(path) + # Remove the whole tree regardless of which sub-directories a test + # created and regardless of their permission bits. + os_helper.rmtree(os_helper.TESTFN) @unittest.skipUnless(hasattr(os, "chown"), "requires os.chown()") diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 09d1b5d725e5ba..5d13d5aadc7412 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -2492,6 +2492,113 @@ def my_mkdir(path, mode=0o777): self.assertNotIn(str(p12), concurrently_created) self.assertTrue(p.exists()) + @unittest.skipIf( + is_emscripten or is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_mkdir_parents_umask(self): + # Test that parent directories respect umask when parent_mode is not set + p = self.cls(self.base, 'umasktest', 'child') + self.assertFalse(p.exists()) + if os.name != 'nt': + with os_helper.temp_umask(0o002): + p.mkdir(0o755, parents=True) + self.assertTrue(p.exists()) + # Leaf directory gets the specified mode + self.assertEqual(p.stat().st_mode & 0o777, 0o755) + # Parent directory respects umask (0o777 & ~0o002 = 0o775) + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o775) + + @unittest.skipIf( + is_emscripten or is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_mkdir_with_parent_mode(self): + # Test the parent_mode parameter + p = self.cls(self.base, 'newdirPM', 'subdirPM') + self.assertFalse(p.exists()) + if os.name != 'nt': + # Specify different modes for parent and leaf directories + p.mkdir(0o755, parents=True, parent_mode=0o750) + self.assertTrue(p.exists()) + self.assertTrue(p.is_dir()) + # Leaf directory gets the mode parameter + self.assertEqual(p.stat().st_mode & 0o777, 0o755) + # Parent directory gets the parent_mode parameter + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o750) + + @unittest.skipIf( + is_emscripten or is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_mkdir_parent_mode_deep_hierarchy(self): + # Test parent_mode with deep directory hierarchy + p = self.cls(self.base, 'level1PM', 'level2PM', 'level3PM') + self.assertFalse(p.exists()) + if os.name != 'nt': + p.mkdir(0o755, parents=True, parent_mode=0o700) + self.assertTrue(p.exists()) + # Check that all parent directories have parent_mode + level1 = self.cls(self.base, 'level1PM') + level2 = level1 / 'level2PM' + self.assertEqual(level1.stat().st_mode & 0o777, 0o700) + self.assertEqual(level2.stat().st_mode & 0o777, 0o700) + # Leaf directory has the regular mode + self.assertEqual(p.stat().st_mode & 0o777, 0o755) + + @unittest.skipIf( + is_emscripten or is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_mkdir_parent_mode_combined_with_umask(self): + # parent_mode, like mode, is combined with the process umask; it does + # not bypass it. + p = self.cls(self.base, 'umaskPM', 'child') + self.assertFalse(p.exists()) + if os.name != 'nt': + with os_helper.temp_umask(0o022): + p.mkdir(0o777, parents=True, parent_mode=0o777) + self.assertTrue(p.exists()) + # 0o777 is masked down to 0o755 by the 0o022 umask, for both + # the leaf (mode) and the parent (parent_mode). + self.assertEqual(p.stat().st_mode & 0o777, 0o755) + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o755) + + @unittest.skipIf( + is_emscripten or is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + @unittest.skipIf( + sys.platform == "android", + "Android filesystem may not honor requested permissions." + ) + def test_mkdir_parent_mode_same_as_mode(self): + # Test setting parent_mode same as mode + p = self.cls(self.base, 'samedirPM', 'subdirPM') + self.assertFalse(p.exists()) + if os.name != 'nt': + p.mkdir(0o705, parents=True, parent_mode=0o705) + self.assertTrue(p.exists()) + # Both directories should have the same mode + self.assertEqual(p.stat().st_mode & 0o777, 0o705) + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o705) + @needs_symlinks def test_symlink_to(self): P = self.cls(self.base) diff --git a/Misc/NEWS.d/next/Library/2025-08-30-07-44-30.gh-issue-86533.pathlib.rst b/Misc/NEWS.d/next/Library/2025-08-30-07-44-30.gh-issue-86533.pathlib.rst new file mode 100644 index 00000000000000..9c32671173e0ad --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-30-07-44-30.gh-issue-86533.pathlib.rst @@ -0,0 +1,4 @@ +The :func:`os.makedirs` function and :meth:`pathlib.Path.mkdir` method now have +a *parent_mode* parameter to specify the mode for intermediate directories when +creating parent directories. This allows one to match the behavior from Python +3.6 and earlier for :func:`os.makedirs`. From 56737483c2ffdaadfec648fd38d409c6b10941c0 Mon Sep 17 00:00:00 2001 From: Armaan Vakharia <43391096+armaan-v924@users.noreply.github.com> Date: Mon, 18 May 2026 16:00:59 -0700 Subject: [PATCH 6/8] gh-149590: Remove faulthandler_traverse (#150023) `faulthandler_traverse` visits Python objects owned by `_PyRuntime`, not by the module instance. With multi-phase init allowing multiple module instances, each instance's GC traversal decrements `gc_refs` on the same runtime-owned objects, driving it negative when two instances are collected simultaneously. --- ...026-05-18-13-47-17.gh-issue-149590.IPBeQx.rst | 1 + Modules/faulthandler.c | 16 ---------------- 2 files changed, 1 insertion(+), 16 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-13-47-17.gh-issue-149590.IPBeQx.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-13-47-17.gh-issue-149590.IPBeQx.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-13-47-17.gh-issue-149590.IPBeQx.rst new file mode 100644 index 00000000000000..8d3b29d69cc857 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-13-47-17.gh-issue-149590.IPBeQx.rst @@ -0,0 +1 @@ +Fix crash when faulthandler is imported more than once. diff --git a/Modules/faulthandler.c b/Modules/faulthandler.c index 1b4f0c2302daae..fa7fb7085d7e8b 100644 --- a/Modules/faulthandler.c +++ b/Modules/faulthandler.c @@ -1349,21 +1349,6 @@ faulthandler__stack_overflow_impl(PyObject *module) #endif /* defined(FAULTHANDLER_USE_ALT_STACK) && defined(HAVE_SIGACTION) */ -static int -faulthandler_traverse(PyObject *module, visitproc visit, void *arg) -{ - Py_VISIT(thread.file); -#ifdef FAULTHANDLER_USER - if (user_signals != NULL) { - for (size_t signum=0; signum < Py_NSIG; signum++) - Py_VISIT(user_signals[signum].file); - } -#endif - Py_VISIT(fatal_error.file); - return 0; -} - - #ifdef MS_WINDOWS /*[clinic input] faulthandler._raise_exception @@ -1459,7 +1444,6 @@ static struct PyModuleDef module_def = { .m_name = "faulthandler", .m_doc = module_doc, .m_methods = module_methods, - .m_traverse = faulthandler_traverse, .m_slots = faulthandler_slots }; From 57a0e570d36f41b953a91bbaf4262a5d05d0391b Mon Sep 17 00:00:00 2001 From: Saul Cooperman <58375603+scopreon@users.noreply.github.com> Date: Mon, 18 May 2026 16:26:08 -0700 Subject: [PATCH 7/8] gh-146452: Improve locking granularity in pickle's batch_dict_exact and fix race condition (#150025) Remove assertion that could fail in rare race condition. Replace the coarse critical section wrapping the entire function with fine-grained sections covering only PyDict_Next + Py_INCREF. Also handle PyDict_Next returning 0 in the single-item fast path. --- ...-05-18-15-30-34.gh-issue-146452.RM0EVJ.rst | 2 + Modules/_pickle.c | 49 +++++++++++-------- 2 files changed, 31 insertions(+), 20 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-18-15-30-34.gh-issue-146452.RM0EVJ.rst diff --git a/Misc/NEWS.d/next/Library/2026-05-18-15-30-34.gh-issue-146452.RM0EVJ.rst b/Misc/NEWS.d/next/Library/2026-05-18-15-30-34.gh-issue-146452.RM0EVJ.rst new file mode 100644 index 00000000000000..66f9acf6c710a7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-18-15-30-34.gh-issue-146452.RM0EVJ.rst @@ -0,0 +1,2 @@ +Fix race condition when pickling dictionaries in free threaded builds. Also +reduce critical section cover. diff --git a/Modules/_pickle.c b/Modules/_pickle.c index 7b87be23269d40..15d95c658d6f90 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -3450,9 +3450,12 @@ batch_dict(PickleState *state, PicklerObject *self, PyObject *iter, PyObject *or * Returns 0 on success, -1 on error. * * Note that this currently doesn't work for protocol 0. + + * gh-146452: Wrap the dict iteration in a critical sections to prevent + * concurrent mutation from invalidating PyDict_Next() iteration state. */ static int -batch_dict_exact_impl(PickleState *state, PicklerObject *self, PyObject *obj) +batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj) { PyObject *key = NULL, *value = NULL; int i; @@ -3466,15 +3469,24 @@ batch_dict_exact_impl(PickleState *state, PicklerObject *self, PyObject *obj) assert(self->proto > 0); dict_size = PyDict_GET_SIZE(obj); - assert(dict_size); /* Write in batches of BATCHSIZE. */ Py_ssize_t total = 0; do { if (dict_size - total == 1) { - PyDict_Next(obj, &ppos, &key, &value); - Py_INCREF(key); - Py_INCREF(value); + int next; + Py_BEGIN_CRITICAL_SECTION(obj); + next = PyDict_Next(obj, &ppos, &key, &value); + if (next) { + Py_INCREF(key); + Py_INCREF(value); + } + Py_END_CRITICAL_SECTION(); + if (!next) { + PyErr_SetString(PyExc_RuntimeError, + "dictionary changed size during iteration"); + goto error; + } if (save(state, self, key, 0) < 0) { goto error; } @@ -3492,9 +3504,18 @@ batch_dict_exact_impl(PickleState *state, PicklerObject *self, PyObject *obj) i = 0; if (_Pickler_Write(self, &mark_op, 1) < 0) return -1; - while (PyDict_Next(obj, &ppos, &key, &value)) { - Py_INCREF(key); - Py_INCREF(value); + int next; + while (1) { + Py_BEGIN_CRITICAL_SECTION(obj); + next = PyDict_Next(obj, &ppos, &key, &value); + if (next) { + Py_INCREF(key); + Py_INCREF(value); + } + Py_END_CRITICAL_SECTION(); + if (!next) { + break; + } if (save(state, self, key, 0) < 0) { goto error; } @@ -3525,18 +3546,6 @@ batch_dict_exact_impl(PickleState *state, PicklerObject *self, PyObject *obj) return -1; } -/* gh-146452: Wrap the dict iteration in a critical section to prevent - concurrent mutation from invalidating PyDict_Next() iteration state. */ -static int -batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj) -{ - int ret; - Py_BEGIN_CRITICAL_SECTION(obj); - ret = batch_dict_exact_impl(state, self, obj); - Py_END_CRITICAL_SECTION(); - return ret; -} - static int save_dict(PickleState *state, PicklerObject *self, PyObject *obj) { From 398d7e1d130e0dd5c79b9b9cc4535d698c7a06cd Mon Sep 17 00:00:00 2001 From: Dan Shernicoff Date: Mon, 18 May 2026 16:55:47 -0700 Subject: [PATCH 8/8] gh-79413: Add `qualname` parameter to `dataclass.make_dataclass`. (GH-150026) Added `qualname` parameter to `dataclasses.make_dataclass` in order to allow user to set `__qualname__` for the generated class. --- Doc/library/dataclasses.rst | 7 ++++++- Lib/dataclasses.py | 8 +++++++- Lib/test/test_dataclasses/__init__.py | 9 +++++++++ .../2026-05-18-13-43-06.gh-issue-79413.OpTXbV.rst | 3 +++ 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-18-13-43-06.gh-issue-79413.OpTXbV.rst diff --git a/Doc/library/dataclasses.rst b/Doc/library/dataclasses.rst index a09c28ad979158..954edc4506df1a 100644 --- a/Doc/library/dataclasses.rst +++ b/Doc/library/dataclasses.rst @@ -418,7 +418,7 @@ Module contents :func:`!astuple` raises :exc:`TypeError` if *obj* is not a dataclass instance. -.. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False, module=None, decorator=dataclass) +.. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False, module=None, qualname=None, decorator=dataclass) Creates a new dataclass with name *cls_name*, fields as defined in *fields*, base classes as given in *bases*, and initialized @@ -434,6 +434,9 @@ Module contents of the dataclass is set to that value. By default, it is set to the module name of the caller. + If *qualname* is defined, the :attr:`~type.__qualname__` attribute of the dataclass + is set to that value. By default, it is set to the value passed to *cls_name*. + The *decorator* parameter is a callable that will be used to create the dataclass. It should take the class object as a first argument and the same keyword arguments as :deco:`dataclass`. By default, the :deco:`dataclass` @@ -464,6 +467,8 @@ Module contents .. versionadded:: 3.14 Added the *decorator* parameter. + .. versionadded:: next + Added the *qualname* parameter. .. function:: replace(obj, /, **changes) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index dbfabded2e47aa..035678d902adaf 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1644,7 +1644,7 @@ def _astuple_inner(obj, tuple_factory): def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, - weakref_slot=False, module=None, decorator=dataclass): + weakref_slot=False, module=None, qualname=None, decorator=dataclass): """Return a new dynamically created dataclass. The dataclass name will be 'cls_name'. 'fields' is an iterable @@ -1669,6 +1669,9 @@ class C(Base): If module parameter is defined, the '__module__' attribute of the dataclass is set to that value. + + If qualname parameter is defined, the '__qualname__' attribute of the dataclass is set + to that value. """ if namespace is None: @@ -1758,6 +1761,9 @@ def exec_body_callback(ns): if module is not None: cls.__module__ = module + if qualname: + cls.__qualname__ = qualname + # Apply the normal provided decorator. cls = decorator(cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen, diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index dcd6a3ef9abfab..2468e3e64dd621 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -5289,6 +5289,15 @@ class A: self.assertEqual(len(fs), 1) self.assertEqual(fs[0].name, 'x') + def test_makedataclass_with_qualname(self): + A = make_dataclass("A", ['a'], qualname='ClassA') + self.assertEqual(A.__qualname__, 'ClassA') + + B = make_dataclass("B", ['b'], qualname='module1.ClassB') + self.assertEqual(B.__qualname__, 'module1.ClassB') + + C = make_dataclass("C", ['c']) + self.assertEqual(C.__qualname__, 'C') class TestZeroArgumentSuperWithSlots(unittest.TestCase): def test_zero_argument_super(self): diff --git a/Misc/NEWS.d/next/Library/2026-05-18-13-43-06.gh-issue-79413.OpTXbV.rst b/Misc/NEWS.d/next/Library/2026-05-18-13-43-06.gh-issue-79413.OpTXbV.rst new file mode 100644 index 00000000000000..8d62fda8b2b67f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-18-13-43-06.gh-issue-79413.OpTXbV.rst @@ -0,0 +1,3 @@ +Update :func:`dataclasses.make_dataclass` to add a *qualname* parameter. The +*qualname* parameter will be used to set the :attr:`!__qualname__` of the +created ``dataclass``.