Skip to content

Commit 04a73cd

Browse files
gpsheadclaude
andcommitted
Modernize os.makedirs for Python 3.15: Add parent_mode parameter
- Replace recursive_mode with more intuitive parent_mode parameter - parent_mode=None (default): intermediate dirs use default permissions - parent_mode=<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 <[email protected]>
1 parent 710665a commit 04a73cd

File tree

5 files changed

+91
-24
lines changed

5 files changed

+91
-24
lines changed

Doc/library/os.rst

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2524,7 +2524,7 @@ features:
25242524

25252525

25262526
.. function:: makedirs(name, mode=0o777, exist_ok=False, *, \
2527-
recursive_mode=False)
2527+
parent_mode=None)
25282528

25292529
.. index::
25302530
single: directory; creating
@@ -2542,8 +2542,9 @@ features:
25422542
If *exist_ok* is ``False`` (the default), a :exc:`FileExistsError` is
25432543
raised if the target directory already exists.
25442544

2545-
If *recursive_mode* is ``True``, the *mode* argument will affect the file
2546-
permission bits of any newly-created, intermediate-level directories.
2545+
If *parent_mode* is not ``None``, it will be used as the mode for any
2546+
newly-created intermediate-level directories. Otherwise, intermediate
2547+
directories are created with the default permissions (respecting umask).
25472548

25482549
.. note::
25492550

@@ -2571,8 +2572,10 @@ features:
25712572
The *mode* argument no longer affects the file permission bits of
25722573
newly created intermediate-level directories.
25732574

2574-
.. versionadded:: 3.10
2575-
The *recursive_mode* parameter.
2575+
.. versionadded:: next
2576+
The *parent_mode* parameter. To match the behavior from Python 3.6 and
2577+
earlier (where *mode* was applied to all created directories), pass
2578+
``parent_mode=mode``.
25762579

25772580

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

Doc/whatsnew/3.15.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,15 @@ math
358358
(Contributed by Bénédikt Tran in :gh:`135853`.)
359359

360360

361+
os
362+
--
363+
364+
* :func:`os.makedirs` function now has a *parent_mode* parameter that allows
365+
specifying the mode for intermediate directories. This can be used to match
366+
the behavior from Python 3.6 and earlier by passing ``parent_mode=mode``.
367+
(Contributed by Zackery Spytz in :gh:`86533`.)
368+
369+
361370
os.path
362371
-------
363372

Lib/os.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,26 +208,27 @@ def _add(str, fn):
208208
# Super directory utilities.
209209
# (Inspired by Eric Raymond; the doc strings are mostly his)
210210

211-
def makedirs(name, mode=0o777, exist_ok=False, *, recursive_mode=False):
212-
"""makedirs(name [, mode=0o777][, exist_ok=False][, recursive_mode=False])
211+
def makedirs(name, mode=0o777, exist_ok=False, *, parent_mode=None):
212+
"""makedirs(name [, mode=0o777][, exist_ok=False][, parent_mode=None])
213213
214214
Super-mkdir; create a leaf directory and all intermediate ones. Works like
215215
mkdir, except that any intermediate path segment (not just the rightmost)
216216
will be created if it does not exist. If the target directory already
217217
exists, raise an OSError if exist_ok is False. Otherwise no exception is
218-
raised. If recursive_mode is True, the mode argument will affect the file
219-
permission bits of any newly-created, intermediate-level directories. This
220-
is recursive.
218+
raised. If parent_mode is not None, it will be used as the mode for any
219+
newly-created, intermediate-level directories. Otherwise, intermediate
220+
directories are created with the default permissions (respecting umask).
221+
This is recursive.
221222
222223
"""
223224
head, tail = path.split(name)
224225
if not tail:
225226
head, tail = path.split(head)
226227
if head and tail and not path.exists(head):
227228
try:
228-
if recursive_mode:
229-
makedirs(head, mode=mode, exist_ok=exist_ok,
230-
recursive_mode=True)
229+
if parent_mode is not None:
230+
makedirs(head, mode=parent_mode, exist_ok=exist_ok,
231+
parent_mode=parent_mode)
231232
else:
232233
makedirs(head, exist_ok=exist_ok)
233234
except FileExistsError:

Lib/test/test_os.py

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1924,16 +1924,68 @@ def test_mode(self):
19241924
parent = os.path.join(os_helper.TESTFN, 'dir1')
19251925
path = os.path.join(parent, 'dir2')
19261926
with os_helper.temp_umask(0o002):
1927-
for mode, recursive_mode, path_mode, parent_mode in \
1928-
(0o555, False, 0o555, 0o775), (0o770, True, 0o770, 0o770):
1929-
os.makedirs(path, mode, recursive_mode=recursive_mode)
1930-
self.assertTrue(os.path.exists(path))
1931-
self.assertTrue(os.path.isdir(path))
1932-
if os.name != 'nt':
1933-
self.assertEqual(os.stat(path).st_mode & 0o777, path_mode)
1934-
self.assertEqual(os.stat(parent).st_mode & 0o777,
1935-
parent_mode)
1936-
shutil.rmtree(parent)
1927+
os.makedirs(path, 0o705)
1928+
self.assertTrue(os.path.exists(path))
1929+
self.assertTrue(os.path.isdir(path))
1930+
if os.name != 'nt':
1931+
# Leaf directory gets the specified mode
1932+
self.assertEqual(os.stat(path).st_mode & 0o777, 0o705)
1933+
# Parent directory uses default permissions (respecting umask)
1934+
self.assertEqual(os.stat(parent).st_mode & 0o777, 0o775)
1935+
1936+
@unittest.skipIf(
1937+
support.is_wasi,
1938+
"WASI's umask is a stub."
1939+
)
1940+
def test_mode_with_parent_mode(self):
1941+
# Test the parent_mode parameter
1942+
parent = os.path.join(os_helper.TESTFN, 'dir1')
1943+
path = os.path.join(parent, 'dir2')
1944+
with os_helper.temp_umask(0o002):
1945+
# Specify mode for both leaf and parent directories
1946+
os.makedirs(path, 0o770, parent_mode=0o750)
1947+
self.assertTrue(os.path.exists(path))
1948+
self.assertTrue(os.path.isdir(path))
1949+
if os.name != 'nt':
1950+
# Leaf directory gets the mode parameter
1951+
self.assertEqual(os.stat(path).st_mode & 0o777, 0o770)
1952+
# Parent directory gets the parent_mode parameter
1953+
self.assertEqual(os.stat(parent).st_mode & 0o777, 0o750)
1954+
1955+
@unittest.skipIf(
1956+
support.is_wasi,
1957+
"WASI's umask is a stub."
1958+
)
1959+
def test_parent_mode_deep_hierarchy(self):
1960+
# Test parent_mode with deep directory hierarchy
1961+
base = os.path.join(os_helper.TESTFN, 'dir1', 'dir2', 'dir3')
1962+
with os_helper.temp_umask(0o002):
1963+
os.makedirs(base, 0o755, parent_mode=0o700)
1964+
self.assertTrue(os.path.exists(base))
1965+
if os.name != 'nt':
1966+
# Check that all parent directories have parent_mode
1967+
level1 = os.path.join(os_helper.TESTFN, 'dir1')
1968+
level2 = os.path.join(level1, 'dir2')
1969+
self.assertEqual(os.stat(level1).st_mode & 0o777, 0o700)
1970+
self.assertEqual(os.stat(level2).st_mode & 0o777, 0o700)
1971+
# Leaf directory has the regular mode
1972+
self.assertEqual(os.stat(base).st_mode & 0o777, 0o755)
1973+
1974+
@unittest.skipIf(
1975+
support.is_wasi,
1976+
"WASI's umask is a stub."
1977+
)
1978+
def test_parent_mode_same_as_mode(self):
1979+
# Test emulating Python 3.6 behavior by setting parent_mode=mode
1980+
parent = os.path.join(os_helper.TESTFN, 'dir1')
1981+
path = os.path.join(parent, 'dir2')
1982+
with os_helper.temp_umask(0o002):
1983+
os.makedirs(path, 0o705, parent_mode=0o705)
1984+
self.assertTrue(os.path.exists(path))
1985+
if os.name != 'nt':
1986+
# Both directories should have the same mode
1987+
self.assertEqual(os.stat(path).st_mode & 0o777, 0o705)
1988+
self.assertEqual(os.stat(parent).st_mode & 0o777, 0o705)
19371989

19381990
@unittest.skipIf(
19391991
support.is_wasi,
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
The :func:`os.makedirs` function now has a *recursive_mode* parameter.
1+
The :func:`os.makedirs` function now has a *parent_mode* parameter to specify
2+
the mode for intermediate directories, allowing one to match the behavior from
3+
Python 3.6 and earlier.

0 commit comments

Comments
 (0)