Skip to content

Commit 28f275f

Browse files
miss-islingtonnessitaZackerySpytzerlend-aaslandgpshead
authored
[3.15] gh-86533: Restore os.makedirs() ability to apply *mode* recursively (GH-150011) (#150036)
bpo-42367: Restore os.makedirs() and pathlib.mkdir() ability to apply *mode* recursively via a new parent_mode= keyword argument. (cherry picked from commit 9770e32) + Make Path.mkdir parent_mode tests umask-independent test_mkdir_with_parent_mode, test_mkdir_parent_mode_deep_hierarchy and test_mkdir_parent_mode_same_as_mode assert exact directory mode bits but did not pin the process umask. On buildbots running with a restrictive umask (e.g. 0o077) the 0o755 leaf was masked down to 0o700, failing the assertions. Wrap them in os_helper.temp_umask(0o022), matching the other umask-aware mkdir tests in this file. --------- Co-authored-by: nessita <124304+nessita@users.noreply.github.com> Co-authored-by: Zackery Spytz <zspytz@gmail.com> Co-authored-by: Erlend E. Aasland <erlend@python.org> Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent d36e080 commit 28f275f

8 files changed

Lines changed: 254 additions & 17 deletions

File tree

Doc/library/os.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2549,7 +2549,8 @@ features:
25492549
Windows now handles a *mode* of ``0o700``.
25502550

25512551

2552-
.. function:: makedirs(name, mode=0o777, exist_ok=False)
2552+
.. function:: makedirs(name, mode=0o777, exist_ok=False, *, \
2553+
parent_mode=None)
25532554

25542555
.. index::
25552556
single: directory; creating
@@ -2567,6 +2568,12 @@ features:
25672568
If *exist_ok* is ``False`` (the default), a :exc:`FileExistsError` is
25682569
raised if the target directory already exists.
25692570

2571+
If *parent_mode* is not ``None``, it is used as the mode for any
2572+
newly-created, intermediate-level directories. Like *mode*, it is
2573+
combined with the process's umask value; see :ref:`the mkdir()
2574+
description <mkdir_modebits>`. Otherwise, intermediate directories are
2575+
created with the default mode, which is also subject to the umask.
2576+
25702577
.. note::
25712578

25722579
:func:`makedirs` will become confused if the path elements to create
@@ -2593,6 +2600,11 @@ features:
25932600
The *mode* argument no longer affects the file permission bits of
25942601
newly created intermediate-level directories.
25952602

2603+
.. versionadded:: 3.15
2604+
The *parent_mode* parameter. To match the behavior from Python 3.6 and
2605+
earlier (where *mode* was applied to all created directories), pass
2606+
``parent_mode=mode``.
2607+
25962608

25972609
.. function:: mkfifo(path, mode=0o666, *, dir_fd=None)
25982610

Doc/library/pathlib.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1514,7 +1514,8 @@ Creating files and directories
15141514
:meth:`~Path.write_bytes` methods are often used to create files.
15151515

15161516

1517-
.. method:: Path.mkdir(mode=0o777, parents=False, exist_ok=False)
1517+
.. method:: Path.mkdir(mode=0o777, parents=False, exist_ok=False, *, \
1518+
parent_mode=None)
15181519

15191520
Create a new directory at this given path. If *mode* is given, it is
15201521
combined with the process's ``umask`` value to determine the file mode
@@ -1525,6 +1526,12 @@ Creating files and directories
15251526
as needed; they are created with the default permissions without taking
15261527
*mode* into account (mimicking the POSIX ``mkdir -p`` command).
15271528

1529+
If *parent_mode* is not ``None``, it is used as the mode for any
1530+
newly-created, intermediate-level directories when *parents* is true.
1531+
Like *mode*, it is combined with the process's ``umask`` value.
1532+
Otherwise, intermediate directories are created with the default
1533+
permissions (also subject to the umask).
1534+
15281535
If *parents* is false (the default), a missing parent raises
15291536
:exc:`FileNotFoundError`.
15301537

@@ -1538,6 +1545,9 @@ Creating files and directories
15381545
.. versionchanged:: 3.5
15391546
The *exist_ok* parameter was added.
15401547

1548+
.. versionadded:: 3.15
1549+
The *parent_mode* parameter.
1550+
15411551

15421552
.. method:: Path.symlink_to(target, target_is_directory=False)
15431553

Doc/whatsnew/3.15.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,10 @@ os
12851285
glibc versions 2.28 and later.
12861286
(Contributed by Jeffrey Bosboom and Victor Stinner in :gh:`83714`.)
12871287

1288+
* :func:`os.makedirs` function now has a *parent_mode* parameter that allows
1289+
specifying the mode for intermediate directories. This can be used to match
1290+
the behavior from Python 3.6 and earlier by passing ``parent_mode=mode``.
1291+
(Contributed by Zackery Spytz and Gregory P. Smith in :gh:`86533`.)
12881292

12891293
os.path
12901294
-------
@@ -2057,6 +2061,10 @@ importlib.resources
20572061
pathlib
20582062
-------
20592063

2064+
* :meth:`pathlib.Path.mkdir` now has a *parent_mode* parameter that allows
2065+
specifying the mode for intermediate directories when ``parents=True``.
2066+
(Contributed by Gregory P. Smith in :gh:`86533`.)
2067+
20602068
* Removed deprecated :meth:`!pathlib.PurePath.is_reserved`.
20612069
Use :func:`os.path.isreserved` to detect reserved paths on Windows.
20622070
(Contributed by Nikita Sobolev in :gh:`133875`.)

Lib/os.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,22 +219,29 @@ def _add(str, fn):
219219
# Super directory utilities.
220220
# (Inspired by Eric Raymond; the doc strings are mostly his)
221221

222-
def makedirs(name, mode=0o777, exist_ok=False):
223-
"""makedirs(name [, mode=0o777][, exist_ok=False])
222+
def makedirs(name, mode=0o777, exist_ok=False, *, parent_mode=None):
223+
"""makedirs(name [, mode=0o777][, exist_ok=False][, parent_mode=None])
224224
225225
Super-mkdir; create a leaf directory and all intermediate ones. Works like
226226
mkdir, except that any intermediate path segment (not just the rightmost)
227227
will be created if it does not exist. If the target directory already
228228
exists, raise an OSError if exist_ok is False. Otherwise no exception is
229-
raised. This is recursive.
229+
raised. If parent_mode is not None, it will be used as the mode for any
230+
newly-created, intermediate-level directories. Otherwise, intermediate
231+
directories are created with the default permissions (respecting umask).
232+
This is recursive.
230233
231234
"""
232235
head, tail = path.split(name)
233236
if not tail:
234237
head, tail = path.split(head)
235238
if head and tail and not path.exists(head):
236239
try:
237-
makedirs(head, exist_ok=exist_ok)
240+
if parent_mode is not None:
241+
makedirs(head, mode=parent_mode, exist_ok=exist_ok,
242+
parent_mode=parent_mode)
243+
else:
244+
makedirs(head, exist_ok=exist_ok)
238245
except FileExistsError:
239246
# Defeats race condition when another thread created the path
240247
pass

Lib/pathlib/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,7 +1202,7 @@ def touch(self, mode=0o666, exist_ok=True):
12021202
fd = os.open(self, flags, mode)
12031203
os.close(fd)
12041204

1205-
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
1205+
def mkdir(self, mode=0o777, parents=False, exist_ok=False, *, parent_mode=None):
12061206
"""
12071207
Create a new directory at this given path.
12081208
"""
@@ -1211,7 +1211,11 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
12111211
except FileNotFoundError:
12121212
if not parents or self.parent == self:
12131213
raise
1214-
self.parent.mkdir(parents=True, exist_ok=True)
1214+
if parent_mode is not None:
1215+
self.parent.mkdir(mode=parent_mode, parents=True, exist_ok=True,
1216+
parent_mode=parent_mode)
1217+
else:
1218+
self.parent.mkdir(parents=True, exist_ok=True)
12151219
self.mkdir(mode, parents=False, exist_ok=exist_ok)
12161220
except OSError:
12171221
# Cannot rely on checking for EEXIST, since the operating system

Lib/test/test_os/test_os.py

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2137,6 +2137,94 @@ def test_mode(self):
21372137
self.assertEqual(os.stat(path).st_mode & 0o777, 0o555)
21382138
self.assertEqual(os.stat(parent).st_mode & 0o777, 0o775)
21392139

2140+
@unittest.skipIf(
2141+
support.is_emscripten or support.is_wasi,
2142+
"umask is not implemented on Emscripten/WASI."
2143+
)
2144+
@unittest.skipIf(
2145+
sys.platform == "android",
2146+
"Android filesystem may not honor requested permissions."
2147+
)
2148+
def test_mode_with_parent_mode(self):
2149+
# Test the parent_mode parameter
2150+
parent = os.path.join(os_helper.TESTFN, 'dir1')
2151+
path = os.path.join(parent, 'dir2')
2152+
with os_helper.temp_umask(0o002):
2153+
# Specify mode for both leaf and parent directories
2154+
os.makedirs(path, 0o770, parent_mode=0o750)
2155+
self.assertTrue(os.path.exists(path))
2156+
self.assertTrue(os.path.isdir(path))
2157+
if os.name != 'nt':
2158+
# Leaf directory gets the mode parameter
2159+
self.assertEqual(os.stat(path).st_mode & 0o777, 0o770)
2160+
# Parent directory gets the parent_mode parameter
2161+
self.assertEqual(os.stat(parent).st_mode & 0o777, 0o750)
2162+
2163+
@unittest.skipIf(
2164+
support.is_emscripten or support.is_wasi,
2165+
"umask is not implemented on Emscripten/WASI."
2166+
)
2167+
@unittest.skipIf(
2168+
sys.platform == "android",
2169+
"Android filesystem may not honor requested permissions."
2170+
)
2171+
def test_parent_mode_deep_hierarchy(self):
2172+
# Test parent_mode with deep directory hierarchy
2173+
base = os.path.join(os_helper.TESTFN, 'dir1', 'dir2', 'dir3')
2174+
with os_helper.temp_umask(0o002):
2175+
os.makedirs(base, 0o755, parent_mode=0o700)
2176+
self.assertTrue(os.path.exists(base))
2177+
if os.name != 'nt':
2178+
# Check that all parent directories have parent_mode
2179+
level1 = os.path.join(os_helper.TESTFN, 'dir1')
2180+
level2 = os.path.join(level1, 'dir2')
2181+
self.assertEqual(os.stat(level1).st_mode & 0o777, 0o700)
2182+
self.assertEqual(os.stat(level2).st_mode & 0o777, 0o700)
2183+
# Leaf directory has the regular mode
2184+
self.assertEqual(os.stat(base).st_mode & 0o777, 0o755)
2185+
2186+
@unittest.skipIf(
2187+
support.is_emscripten or support.is_wasi,
2188+
"umask is not implemented on Emscripten/WASI."
2189+
)
2190+
@unittest.skipIf(
2191+
sys.platform == "android",
2192+
"Android filesystem may not honor requested permissions."
2193+
)
2194+
def test_parent_mode_same_as_mode(self):
2195+
# Test emulating Python 3.6 behavior by setting parent_mode=mode
2196+
parent = os.path.join(os_helper.TESTFN, 'dir1')
2197+
path = os.path.join(parent, 'dir2')
2198+
with os_helper.temp_umask(0o002):
2199+
os.makedirs(path, 0o705, parent_mode=0o705)
2200+
self.assertTrue(os.path.exists(path))
2201+
if os.name != 'nt':
2202+
# Both directories should have the same mode
2203+
self.assertEqual(os.stat(path).st_mode & 0o777, 0o705)
2204+
self.assertEqual(os.stat(parent).st_mode & 0o777, 0o705)
2205+
2206+
@unittest.skipIf(
2207+
support.is_emscripten or support.is_wasi,
2208+
"umask is not implemented on Emscripten/WASI."
2209+
)
2210+
@unittest.skipIf(
2211+
sys.platform == "android",
2212+
"Android filesystem may not honor requested permissions."
2213+
)
2214+
def test_parent_mode_combined_with_umask(self):
2215+
# parent_mode, like mode, is combined with the process umask; it does
2216+
# not bypass it.
2217+
parent = os.path.join(os_helper.TESTFN, 'dir1')
2218+
path = os.path.join(parent, 'dir2')
2219+
with os_helper.temp_umask(0o022):
2220+
os.makedirs(path, 0o777, parent_mode=0o777)
2221+
self.assertTrue(os.path.isdir(path))
2222+
if os.name != 'nt':
2223+
# 0o777 is masked down to 0o755 by the 0o022 umask, for both
2224+
# the leaf (mode) and the parent (parent_mode).
2225+
self.assertEqual(os.stat(path).st_mode & 0o777, 0o755)
2226+
self.assertEqual(os.stat(parent).st_mode & 0o777, 0o755)
2227+
21402228
@unittest.skipIf(
21412229
support.is_wasi,
21422230
"WASI's umask is a stub."
@@ -2210,15 +2298,9 @@ def test_win32_mkdir_700(self):
22102298
)
22112299

22122300
def tearDown(self):
2213-
path = os.path.join(os_helper.TESTFN, 'dir1', 'dir2', 'dir3',
2214-
'dir4', 'dir5', 'dir6')
2215-
# If the tests failed, the bottom-most directory ('../dir6')
2216-
# may not have been created, so we look for the outermost directory
2217-
# that exists.
2218-
while not os.path.exists(path) and path != os_helper.TESTFN:
2219-
path = os.path.dirname(path)
2220-
2221-
os.removedirs(path)
2301+
# Remove the whole tree regardless of which sub-directories a test
2302+
# created and regardless of their permission bits.
2303+
os_helper.rmtree(os_helper.TESTFN)
22222304

22232305

22242306
@unittest.skipUnless(hasattr(os, "chown"), "requires os.chown()")

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2492,6 +2492,116 @@ def my_mkdir(path, mode=0o777):
24922492
self.assertNotIn(str(p12), concurrently_created)
24932493
self.assertTrue(p.exists())
24942494

2495+
@unittest.skipIf(
2496+
is_emscripten or is_wasi,
2497+
"umask is not implemented on Emscripten/WASI."
2498+
)
2499+
@unittest.skipIf(
2500+
sys.platform == "android",
2501+
"Android filesystem may not honor requested permissions."
2502+
)
2503+
def test_mkdir_parents_umask(self):
2504+
# Test that parent directories respect umask when parent_mode is not set
2505+
p = self.cls(self.base, 'umasktest', 'child')
2506+
self.assertFalse(p.exists())
2507+
if os.name != 'nt':
2508+
with os_helper.temp_umask(0o002):
2509+
p.mkdir(0o755, parents=True)
2510+
self.assertTrue(p.exists())
2511+
# Leaf directory gets the specified mode
2512+
self.assertEqual(p.stat().st_mode & 0o777, 0o755)
2513+
# Parent directory respects umask (0o777 & ~0o002 = 0o775)
2514+
self.assertEqual(p.parent.stat().st_mode & 0o777, 0o775)
2515+
2516+
@unittest.skipIf(
2517+
is_emscripten or is_wasi,
2518+
"umask is not implemented on Emscripten/WASI."
2519+
)
2520+
@unittest.skipIf(
2521+
sys.platform == "android",
2522+
"Android filesystem may not honor requested permissions."
2523+
)
2524+
def test_mkdir_with_parent_mode(self):
2525+
# Test the parent_mode parameter
2526+
p = self.cls(self.base, 'newdirPM', 'subdirPM')
2527+
self.assertFalse(p.exists())
2528+
if os.name != 'nt':
2529+
with os_helper.temp_umask(0o022):
2530+
# Specify different modes for parent and leaf directories
2531+
p.mkdir(0o755, parents=True, parent_mode=0o750)
2532+
self.assertTrue(p.exists())
2533+
self.assertTrue(p.is_dir())
2534+
# Leaf directory gets the mode parameter
2535+
self.assertEqual(p.stat().st_mode & 0o777, 0o755)
2536+
# Parent directory gets the parent_mode parameter
2537+
self.assertEqual(p.parent.stat().st_mode & 0o777, 0o750)
2538+
2539+
@unittest.skipIf(
2540+
is_emscripten or is_wasi,
2541+
"umask is not implemented on Emscripten/WASI."
2542+
)
2543+
@unittest.skipIf(
2544+
sys.platform == "android",
2545+
"Android filesystem may not honor requested permissions."
2546+
)
2547+
def test_mkdir_parent_mode_deep_hierarchy(self):
2548+
# Test parent_mode with deep directory hierarchy
2549+
p = self.cls(self.base, 'level1PM', 'level2PM', 'level3PM')
2550+
self.assertFalse(p.exists())
2551+
if os.name != 'nt':
2552+
with os_helper.temp_umask(0o022):
2553+
p.mkdir(0o755, parents=True, parent_mode=0o700)
2554+
self.assertTrue(p.exists())
2555+
# Check that all parent directories have parent_mode
2556+
level1 = self.cls(self.base, 'level1PM')
2557+
level2 = level1 / 'level2PM'
2558+
self.assertEqual(level1.stat().st_mode & 0o777, 0o700)
2559+
self.assertEqual(level2.stat().st_mode & 0o777, 0o700)
2560+
# Leaf directory has the regular mode
2561+
self.assertEqual(p.stat().st_mode & 0o777, 0o755)
2562+
2563+
@unittest.skipIf(
2564+
is_emscripten or is_wasi,
2565+
"umask is not implemented on Emscripten/WASI."
2566+
)
2567+
@unittest.skipIf(
2568+
sys.platform == "android",
2569+
"Android filesystem may not honor requested permissions."
2570+
)
2571+
def test_mkdir_parent_mode_combined_with_umask(self):
2572+
# parent_mode, like mode, is combined with the process umask; it does
2573+
# not bypass it.
2574+
p = self.cls(self.base, 'umaskPM', 'child')
2575+
self.assertFalse(p.exists())
2576+
if os.name != 'nt':
2577+
with os_helper.temp_umask(0o022):
2578+
p.mkdir(0o777, parents=True, parent_mode=0o777)
2579+
self.assertTrue(p.exists())
2580+
# 0o777 is masked down to 0o755 by the 0o022 umask, for both
2581+
# the leaf (mode) and the parent (parent_mode).
2582+
self.assertEqual(p.stat().st_mode & 0o777, 0o755)
2583+
self.assertEqual(p.parent.stat().st_mode & 0o777, 0o755)
2584+
2585+
@unittest.skipIf(
2586+
is_emscripten or is_wasi,
2587+
"umask is not implemented on Emscripten/WASI."
2588+
)
2589+
@unittest.skipIf(
2590+
sys.platform == "android",
2591+
"Android filesystem may not honor requested permissions."
2592+
)
2593+
def test_mkdir_parent_mode_same_as_mode(self):
2594+
# Test setting parent_mode same as mode
2595+
p = self.cls(self.base, 'samedirPM', 'subdirPM')
2596+
self.assertFalse(p.exists())
2597+
if os.name != 'nt':
2598+
with os_helper.temp_umask(0o022):
2599+
p.mkdir(0o705, parents=True, parent_mode=0o705)
2600+
self.assertTrue(p.exists())
2601+
# Both directories should have the same mode
2602+
self.assertEqual(p.stat().st_mode & 0o777, 0o705)
2603+
self.assertEqual(p.parent.stat().st_mode & 0o777, 0o705)
2604+
24952605
@needs_symlinks
24962606
def test_symlink_to(self):
24972607
P = self.cls(self.base)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The :func:`os.makedirs` function and :meth:`pathlib.Path.mkdir` method now have
2+
a *parent_mode* parameter to specify the mode for intermediate directories when
3+
creating parent directories. This allows one to match the behavior from Python
4+
3.6 and earlier for :func:`os.makedirs`.

0 commit comments

Comments
 (0)