From 128ff8b46696c26e2cea5609cf9840b9425dcccf Mon Sep 17 00:00:00 2001 From: Zackery Spytz Date: Tue, 22 Dec 2020 16:56:30 -0700 Subject: [PATCH 01/10] bpo-42367: Restore os.makedirs() ability to apply *mode* recursively Allow os.makedirs() to apply the *mode* argument to any intermediate directories that are created. --- Doc/library/os.rst | 9 +++++++- Doc/whatsnew/3.10.rst | 3 +++ Lib/os.py | 14 +++++++++---- Lib/test/test_os.py | 21 +++++++++++-------- .../2020-12-22-16-55-59.bpo-42367.IGICm2.rst | 1 + 5 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 35cf7c0a0ba5c3..a2af75a6117825 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -2055,7 +2055,8 @@ features: Accepts a :term:`path-like object`. -.. function:: makedirs(name, mode=0o777, exist_ok=False) +.. function:: makedirs(name, mode=0o777, exist_ok=False, *, \ + recursive_mode=False) .. index:: single: directory; creating @@ -2073,6 +2074,9 @@ features: If *exist_ok* is ``False`` (the default), an :exc:`FileExistsError` is raised if the target directory already exists. + If *recursive_mode* is ``True``, the *mode* argument will affect the file + permission bits of any newly-created, intermediate-level directories. + .. note:: :func:`makedirs` will become confused if the path elements to create @@ -2099,6 +2103,9 @@ features: The *mode* argument no longer affects the file permission bits of newly-created intermediate-level directories. + .. versionadded:: 3.10 + The *recursive_mode* parameter. + .. function:: mkfifo(path, mode=0o666, *, dir_fd=None) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index a6f9b0b1754d29..20302b4f0e6c44 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -259,6 +259,9 @@ descriptors without copying between kernel address space and user address space, where one of the file descriptors must refer to a pipe. (Contributed by Pablo Galindo in :issue:`41625`.) +The :func:`os.makedirs` function now has a *recursive_mode* parameter. +(Contributed by Zackery Spytz in :issue:`42367`.) + pathlib ------- diff --git a/Lib/os.py b/Lib/os.py index 05e9c32c5a7117..b01f7c674eb0bb 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -197,14 +197,16 @@ 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, *, recursive_mode=False): + """makedirs(name [, mode=0o777][, exist_ok=False][, recursive_mode=False]) 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 recursive_mode is True, the mode argument will affect the file + permission bits of any newly-created, intermediate-level directories. This + is recursive. """ head, tail = path.split(name) @@ -212,7 +214,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 recursive_mode: + makedirs(head, mode=mode, exist_ok=exist_ok, + recursive_mode=True) + else: + makedirs(head, exist_ok=exist_ok) except FileExistsError: # Defeats race condition when another thread created the path pass diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 08d7ab8a30ba7e..6e3d924b891844 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1572,16 +1572,19 @@ def test_makedir(self): os.makedirs(path) def test_mode(self): + parent = os.path.join(os_helper.TESTFN, 'dir1') + path = os.path.join(parent, 'dir2') with os_helper.temp_umask(0o002): - base = os_helper.TESTFN - parent = os.path.join(base, 'dir1') - path = os.path.join(parent, 'dir2') - os.makedirs(path, 0o555) - self.assertTrue(os.path.exists(path)) - self.assertTrue(os.path.isdir(path)) - if os.name != 'nt': - self.assertEqual(os.stat(path).st_mode & 0o777, 0o555) - self.assertEqual(os.stat(parent).st_mode & 0o777, 0o775) + for mode, recursive_mode, path_mode, parent_mode in \ + (0o555, False, 0o555, 0o775), (0o770, True, 0o770, 0o770): + os.makedirs(path, mode, recursive_mode=recursive_mode) + self.assertTrue(os.path.exists(path)) + self.assertTrue(os.path.isdir(path)) + if os.name != 'nt': + self.assertEqual(os.stat(path).st_mode & 0o777, path_mode) + self.assertEqual(os.stat(parent).st_mode & 0o777, + parent_mode) + shutil.rmtree(parent) def test_exist_ok_existing_directory(self): path = os.path.join(os_helper.TESTFN, 'dir1') diff --git a/Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst b/Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst new file mode 100644 index 00000000000000..7bb5ea8737f1fe --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst @@ -0,0 +1 @@ +The :func:`os.makedirs` function now has a *recursive_mode* parameter. From 04a73cd36e55df11c3298ee457a85bcfb15c0da7 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sat, 30 Aug 2025 07:24:19 +0000 Subject: [PATCH 02/10] Modernize os.makedirs for Python 3.15: Add parent_mode parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace recursive_mode with more intuitive parent_mode parameter - parent_mode=None (default): intermediate dirs use default permissions - parent_mode=: intermediate dirs use specified permissions - parent_mode=mode: matches Python 3.6 behavior - Update documentation with version markers and usage examples - Add comprehensive test coverage with separate focused test functions - Fix test permissions to avoid cleanup issues (0o555 → 0o705) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Doc/library/os.rst | 13 ++-- Doc/whatsnew/3.15.rst | 9 +++ Lib/os.py | 17 ++--- Lib/test/test_os.py | 72 ++++++++++++++++--- .../2020-12-22-16-55-59.bpo-42367.IGICm2.rst | 4 +- 5 files changed, 91 insertions(+), 24 deletions(-) diff --git a/Doc/library/os.rst b/Doc/library/os.rst index f627841e543904..b2a3a45c02aba8 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -2524,7 +2524,7 @@ features: .. function:: makedirs(name, mode=0o777, exist_ok=False, *, \ - recursive_mode=False) + parent_mode=None) .. index:: single: directory; creating @@ -2542,8 +2542,9 @@ features: If *exist_ok* is ``False`` (the default), a :exc:`FileExistsError` is raised if the target directory already exists. - If *recursive_mode* is ``True``, the *mode* argument will affect the file - permission bits of any newly-created, intermediate-level directories. + 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). .. note:: @@ -2571,8 +2572,10 @@ features: The *mode* argument no longer affects the file permission bits of newly created intermediate-level directories. - .. versionadded:: 3.10 - The *recursive_mode* parameter. + .. versionadded:: next + 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/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index c38c83d9f45d4e..06c5b02accedfb 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -358,6 +358,15 @@ math (Contributed by Bénédikt Tran in :gh:`135853`.) +os +-- + +* :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 in :gh:`86533`.) + + os.path ------- diff --git a/Lib/os.py b/Lib/os.py index 65f537ec2288c1..59d1afc43c1b58 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -208,16 +208,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, *, recursive_mode=False): - """makedirs(name [, mode=0o777][, exist_ok=False][, recursive_mode=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. If recursive_mode is True, the mode argument will affect the file - permission bits of any newly-created, intermediate-level directories. 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) @@ -225,9 +226,9 @@ def makedirs(name, mode=0o777, exist_ok=False, *, recursive_mode=False): head, tail = path.split(head) if head and tail and not path.exists(head): try: - if recursive_mode: - makedirs(head, mode=mode, exist_ok=exist_ok, - recursive_mode=True) + 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: diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 60c209d9610939..9f607808ee3477 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1924,16 +1924,68 @@ def test_mode(self): parent = os.path.join(os_helper.TESTFN, 'dir1') path = os.path.join(parent, 'dir2') with os_helper.temp_umask(0o002): - for mode, recursive_mode, path_mode, parent_mode in \ - (0o555, False, 0o555, 0o775), (0o770, True, 0o770, 0o770): - os.makedirs(path, mode, recursive_mode=recursive_mode) - self.assertTrue(os.path.exists(path)) - self.assertTrue(os.path.isdir(path)) - if os.name != 'nt': - self.assertEqual(os.stat(path).st_mode & 0o777, path_mode) - self.assertEqual(os.stat(parent).st_mode & 0o777, - parent_mode) - shutil.rmtree(parent) + os.makedirs(path, 0o705) + self.assertTrue(os.path.exists(path)) + self.assertTrue(os.path.isdir(path)) + if os.name != 'nt': + # Leaf directory gets the specified mode + self.assertEqual(os.stat(path).st_mode & 0o777, 0o705) + # Parent directory uses default permissions (respecting umask) + self.assertEqual(os.stat(parent).st_mode & 0o777, 0o775) + + @unittest.skipIf( + support.is_wasi, + "WASI's umask is a stub." + ) + 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_wasi, + "WASI's umask is a stub." + ) + 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_wasi, + "WASI's umask is a stub." + ) + 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_wasi, diff --git a/Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst b/Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst index 7bb5ea8737f1fe..5863785d9f6367 100644 --- a/Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst +++ b/Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst @@ -1 +1,3 @@ -The :func:`os.makedirs` function now has a *recursive_mode* parameter. +The :func:`os.makedirs` function now has a *parent_mode* parameter to specify +the mode for intermediate directories, allowing one to match the behavior from +Python 3.6 and earlier. From 67ae3afc7b313e5c6d2679b8dd5b449f8c2c5565 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sat, 30 Aug 2025 07:58:41 +0000 Subject: [PATCH 03/10] Add parent_mode parameter to pathlib.Path.mkdir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add parent_mode parameter to Path.mkdir() for specifying intermediate directory permissions when parents=True - Maintain pathlib's independence by using recursive implementation rather than delegating to os.makedirs - Add comprehensive tests including umask behavior verification - Update documentation and whatsnew entries - Provides consistency with os.makedirs parent_mode parameter 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Doc/library/pathlib.rst | 10 ++- Doc/whatsnew/3.15.rst | 6 +- Lib/pathlib/__init__.py | 7 +- Lib/test/test_pathlib/test_pathlib.py | 81 +++++++++++++++++++ ...-08-30-07-44-30.gh-issue-86533.pathlib.rst | 3 + 5 files changed, 103 insertions(+), 4 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/pathlib.rst b/Doc/library/pathlib.rst index ebf5756146df92..3d6ca606f9215b 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1492,7 +1492,7 @@ 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 @@ -1503,6 +1503,11 @@ 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 will be used as the mode for any + newly-created intermediate-level directories when *parents* is true. + Otherwise, intermediate directories are created with the default + permissions (respecting umask). + If *parents* is false (the default), a missing parent raises :exc:`FileNotFoundError`. @@ -1516,6 +1521,9 @@ Creating files and directories .. versionchanged:: 3.5 The *exist_ok* parameter was added. + .. versionadded:: next + 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 06c5b02accedfb..31f8f18284080f 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -364,7 +364,7 @@ os * :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 in :gh:`86533`.) + (Contributed by Zackery Spytz and Gregory P. Smith in :gh:`86533`.) os.path @@ -550,6 +550,10 @@ http.server 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/pathlib/__init__.py b/Lib/pathlib/__init__.py index cea1a9fe57eedf..e869258ddf6552 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -997,7 +997,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. """ @@ -1006,7 +1006,10 @@ 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_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index a1105aae6351b6..becc0bbf222d03 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -2489,6 +2489,87 @@ 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." + ) + 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': + old_mask = os.umask(0o002) + try: + p.mkdir(0o755, parents=True) + self.assertTrue(p.exists()) + # Leaf directory gets the specified mode + self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755) + # Parent directory respects umask (0o777 & ~0o002 = 0o775) + self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 0o775) + finally: + os.umask(old_mask) + + 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(stat.S_IMODE(p.stat().st_mode), 0o755) + # Parent directory gets the parent_mode parameter + self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 0o750) + + 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(stat.S_IMODE(level1.stat().st_mode), 0o700) + self.assertEqual(stat.S_IMODE(level2.stat().st_mode), 0o700) + # Leaf directory has the regular mode + self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755) + + @unittest.skipIf( + is_emscripten or is_wasi, + "umask is not implemented on Emscripten/WASI." + ) + def test_mkdir_parent_mode_overrides_umask(self): + # Test that parent_mode overrides umask for parent directories + p = self.cls(self.base, 'overridetest', 'child') + self.assertFalse(p.exists()) + if os.name != 'nt': + old_mask = os.umask(0o022) # Restrictive umask + try: + # parent_mode should override umask for parents + p.mkdir(0o755, parents=True, parent_mode=0o700) + self.assertTrue(p.exists()) + # Leaf directory gets the specified mode + self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755) + # Parent directory gets parent_mode, not affected by umask + self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 0o700) + finally: + os.umask(old_mask) + + 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(stat.S_IMODE(p.stat().st_mode), 0o705) + self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 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..764659f72d39a3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-30-07-44-30.gh-issue-86533.pathlib.rst @@ -0,0 +1,3 @@ +The :meth:`pathlib.Path.mkdir` method now has a *parent_mode* parameter to +specify the mode for intermediate directories when creating parent directories, +providing consistency with the new :func:`os.makedirs` *parent_mode* parameter. From 0a8c61e6f8c6de1fc4b0ee36431cbd11f25cecec Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sat, 30 Aug 2025 08:09:31 +0000 Subject: [PATCH 04/10] merge the NEWS entries --- .../next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst | 3 --- .../Library/2025-08-30-07-44-30.gh-issue-86533.pathlib.rst | 7 ++++--- 2 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst diff --git a/Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst b/Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst deleted file mode 100644 index 5863785d9f6367..00000000000000 --- a/Misc/NEWS.d/next/Library/2020-12-22-16-55-59.bpo-42367.IGICm2.rst +++ /dev/null @@ -1,3 +0,0 @@ -The :func:`os.makedirs` function now has a *parent_mode* parameter to specify -the mode for intermediate directories, allowing one to match the behavior from -Python 3.6 and earlier. 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 index 764659f72d39a3..9c32671173e0ad 100644 --- 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 @@ -1,3 +1,4 @@ -The :meth:`pathlib.Path.mkdir` method now has a *parent_mode* parameter to -specify the mode for intermediate directories when creating parent directories, -providing consistency with the new :func:`os.makedirs` *parent_mode* parameter. +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 9371670ab0fbb0f7877267f944fa36445d2845de Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sat, 30 Aug 2025 08:47:03 +0000 Subject: [PATCH 05/10] Fix pathlib parent_mode tests for Android filesystem compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use st_mode & 0o777 masking instead of stat.S_IMODE() to ignore filesystem-specific bits like setgid, matching the approach used in os.makedirs tests. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Lib/test/test_pathlib/test_pathlib.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index becc0bbf222d03..6ffceb533c11b6 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -2503,9 +2503,9 @@ def test_mkdir_parents_umask(self): p.mkdir(0o755, parents=True) self.assertTrue(p.exists()) # Leaf directory gets the specified mode - self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755) + self.assertEqual(p.stat().st_mode & 0o777, 0o755) # Parent directory respects umask (0o777 & ~0o002 = 0o775) - self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 0o775) + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o775) finally: os.umask(old_mask) @@ -2519,9 +2519,9 @@ def test_mkdir_with_parent_mode(self): self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) # Leaf directory gets the mode parameter - self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755) + self.assertEqual(p.stat().st_mode & 0o777, 0o755) # Parent directory gets the parent_mode parameter - self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 0o750) + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o750) def test_mkdir_parent_mode_deep_hierarchy(self): # Test parent_mode with deep directory hierarchy @@ -2533,10 +2533,10 @@ def test_mkdir_parent_mode_deep_hierarchy(self): # Check that all parent directories have parent_mode level1 = self.cls(self.base, 'level1PM') level2 = level1 / 'level2PM' - self.assertEqual(stat.S_IMODE(level1.stat().st_mode), 0o700) - self.assertEqual(stat.S_IMODE(level2.stat().st_mode), 0o700) + self.assertEqual(level1.stat().st_mode & 0o777, 0o700) + self.assertEqual(level2.stat().st_mode & 0o777, 0o700) # Leaf directory has the regular mode - self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755) + self.assertEqual(p.stat().st_mode & 0o777, 0o755) @unittest.skipIf( is_emscripten or is_wasi, @@ -2553,9 +2553,9 @@ def test_mkdir_parent_mode_overrides_umask(self): p.mkdir(0o755, parents=True, parent_mode=0o700) self.assertTrue(p.exists()) # Leaf directory gets the specified mode - self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755) + self.assertEqual(p.stat().st_mode & 0o777, 0o755) # Parent directory gets parent_mode, not affected by umask - self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 0o700) + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o700) finally: os.umask(old_mask) @@ -2567,8 +2567,8 @@ def test_mkdir_parent_mode_same_as_mode(self): p.mkdir(0o705, parents=True, parent_mode=0o705) self.assertTrue(p.exists()) # Both directories should have the same mode - self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o705) - self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 0o705) + self.assertEqual(p.stat().st_mode & 0o777, 0o705) + self.assertEqual(p.parent.stat().st_mode & 0o777, 0o705) @needs_symlinks def test_symlink_to(self): From 988d1c437f9ca18edb1995fb2bd7cb343b4107e2 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sun, 31 Aug 2025 01:20:56 +0000 Subject: [PATCH 06/10] add test skips for WASI and android where file modes don't work well enough --- Lib/test/test_pathlib/test_pathlib.py | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 6ffceb533c11b6..55a2292f9b82f0 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -2493,6 +2493,10 @@ def my_mkdir(path, mode=0o777): 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') @@ -2509,6 +2513,14 @@ def test_mkdir_parents_umask(self): finally: os.umask(old_mask) + @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') @@ -2523,6 +2535,14 @@ def test_mkdir_with_parent_mode(self): # 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') @@ -2542,6 +2562,10 @@ def test_mkdir_parent_mode_deep_hierarchy(self): 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_overrides_umask(self): # Test that parent_mode overrides umask for parent directories p = self.cls(self.base, 'overridetest', 'child') @@ -2559,6 +2583,14 @@ def test_mkdir_parent_mode_overrides_umask(self): finally: os.umask(old_mask) + @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') From ccce06394a5edd43eb08738b22428ee04702cdfb Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sun, 31 Aug 2025 01:22:12 +0000 Subject: [PATCH 07/10] doc line length --- Doc/library/pathlib.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 3d6ca606f9215b..f51eb79363b5e0 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1492,7 +1492,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, *, parent_mode=None) +.. 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 From bdf5c8c6728f8d82a5280307911110072c2f1e50 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> Date: Sat, 30 Aug 2025 18:25:47 -0700 Subject: [PATCH 08/10] add comma to docs Co-authored-by: Zackery Spytz --- Doc/library/pathlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index f51eb79363b5e0..78e3b119b10717 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1505,7 +1505,7 @@ Creating files and directories *mode* into account (mimicking the POSIX ``mkdir -p`` command). If *parent_mode* is not ``None``, it will be used as the mode for any - newly-created intermediate-level directories when *parents* is true. + newly-created, intermediate-level directories when *parents* is true. Otherwise, intermediate directories are created with the default permissions (respecting umask). From de7139c70c61eaef11df301d4fb2f5ef43a2c4b9 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> Date: Sat, 30 Aug 2025 18:26:05 -0700 Subject: [PATCH 09/10] add comma to docs Co-authored-by: Zackery Spytz --- Doc/library/os.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/os.rst b/Doc/library/os.rst index b2a3a45c02aba8..d51d058fb496cd 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -2543,7 +2543,7 @@ features: raised if the target directory already exists. If *parent_mode* is not ``None``, it will be used as the mode for any - newly-created intermediate-level directories. Otherwise, intermediate + newly-created, intermediate-level directories. Otherwise, intermediate directories are created with the default permissions (respecting umask). .. note:: From 59c982f5697062c1b60622cb0bcb5937cad7216c Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sun, 31 Aug 2025 01:29:36 +0000 Subject: [PATCH 10/10] line length --- Lib/pathlib/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index e869258ddf6552..624567c0f0f34f 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -1007,7 +1007,8 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False, *, parent_mode=None): if not parents or self.parent == self: raise if parent_mode is not None: - self.parent.mkdir(mode=parent_mode, parents=True, exist_ok=True, parent_mode=parent_mode) + 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)