Skip to content

Commit 67ae3af

Browse files
gpsheadclaude
andcommitted
Add parent_mode parameter to pathlib.Path.mkdir
- 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 <[email protected]>
1 parent 04a73cd commit 67ae3af

File tree

5 files changed

+103
-4
lines changed

5 files changed

+103
-4
lines changed

Doc/library/pathlib.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1492,7 +1492,7 @@ Creating files and directories
14921492
:meth:`~Path.write_bytes` methods are often used to create files.
14931493

14941494

1495-
.. method:: Path.mkdir(mode=0o777, parents=False, exist_ok=False)
1495+
.. method:: Path.mkdir(mode=0o777, parents=False, exist_ok=False, *, parent_mode=None)
14961496

14971497
Create a new directory at this given path. If *mode* is given, it is
14981498
combined with the process's ``umask`` value to determine the file mode
@@ -1503,6 +1503,11 @@ Creating files and directories
15031503
as needed; they are created with the default permissions without taking
15041504
*mode* into account (mimicking the POSIX ``mkdir -p`` command).
15051505

1506+
If *parent_mode* is not ``None``, it will be used as the mode for any
1507+
newly-created intermediate-level directories when *parents* is true.
1508+
Otherwise, intermediate directories are created with the default
1509+
permissions (respecting umask).
1510+
15061511
If *parents* is false (the default), a missing parent raises
15071512
:exc:`FileNotFoundError`.
15081513

@@ -1516,6 +1521,9 @@ Creating files and directories
15161521
.. versionchanged:: 3.5
15171522
The *exist_ok* parameter was added.
15181523

1524+
.. versionadded:: next
1525+
The *parent_mode* parameter.
1526+
15191527

15201528
.. method:: Path.symlink_to(target, target_is_directory=False)
15211529

Doc/whatsnew/3.15.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ os
364364
* :func:`os.makedirs` function now has a *parent_mode* parameter that allows
365365
specifying the mode for intermediate directories. This can be used to match
366366
the behavior from Python 3.6 and earlier by passing ``parent_mode=mode``.
367-
(Contributed by Zackery Spytz in :gh:`86533`.)
367+
(Contributed by Zackery Spytz and Gregory P. Smith in :gh:`86533`.)
368368

369369

370370
os.path
@@ -550,6 +550,10 @@ http.server
550550
pathlib
551551
-------
552552

553+
* :meth:`pathlib.Path.mkdir` now has a *parent_mode* parameter that allows
554+
specifying the mode for intermediate directories when ``parents=True``.
555+
(Contributed by Gregory P. Smith in :gh:`86533`.)
556+
553557
* Removed deprecated :meth:`!pathlib.PurePath.is_reserved`.
554558
Use :func:`os.path.isreserved` to detect reserved paths on Windows.
555559
(Contributed by Nikita Sobolev in :gh:`133875`.)

Lib/pathlib/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -997,7 +997,7 @@ def touch(self, mode=0o666, exist_ok=True):
997997
fd = os.open(self, flags, mode)
998998
os.close(fd)
999999

1000-
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
1000+
def mkdir(self, mode=0o777, parents=False, exist_ok=False, *, parent_mode=None):
10011001
"""
10021002
Create a new directory at this given path.
10031003
"""
@@ -1006,7 +1006,10 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
10061006
except FileNotFoundError:
10071007
if not parents or self.parent == self:
10081008
raise
1009-
self.parent.mkdir(parents=True, exist_ok=True)
1009+
if parent_mode is not None:
1010+
self.parent.mkdir(mode=parent_mode, parents=True, exist_ok=True, parent_mode=parent_mode)
1011+
else:
1012+
self.parent.mkdir(parents=True, exist_ok=True)
10101013
self.mkdir(mode, parents=False, exist_ok=exist_ok)
10111014
except OSError:
10121015
# Cannot rely on checking for EEXIST, since the operating system

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2489,6 +2489,87 @@ def my_mkdir(path, mode=0o777):
24892489
self.assertNotIn(str(p12), concurrently_created)
24902490
self.assertTrue(p.exists())
24912491

2492+
@unittest.skipIf(
2493+
is_emscripten or is_wasi,
2494+
"umask is not implemented on Emscripten/WASI."
2495+
)
2496+
def test_mkdir_parents_umask(self):
2497+
# Test that parent directories respect umask when parent_mode is not set
2498+
p = self.cls(self.base, 'umasktest', 'child')
2499+
self.assertFalse(p.exists())
2500+
if os.name != 'nt':
2501+
old_mask = os.umask(0o002)
2502+
try:
2503+
p.mkdir(0o755, parents=True)
2504+
self.assertTrue(p.exists())
2505+
# Leaf directory gets the specified mode
2506+
self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755)
2507+
# Parent directory respects umask (0o777 & ~0o002 = 0o775)
2508+
self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 0o775)
2509+
finally:
2510+
os.umask(old_mask)
2511+
2512+
def test_mkdir_with_parent_mode(self):
2513+
# Test the parent_mode parameter
2514+
p = self.cls(self.base, 'newdirPM', 'subdirPM')
2515+
self.assertFalse(p.exists())
2516+
if os.name != 'nt':
2517+
# Specify different modes for parent and leaf directories
2518+
p.mkdir(0o755, parents=True, parent_mode=0o750)
2519+
self.assertTrue(p.exists())
2520+
self.assertTrue(p.is_dir())
2521+
# Leaf directory gets the mode parameter
2522+
self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755)
2523+
# Parent directory gets the parent_mode parameter
2524+
self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 0o750)
2525+
2526+
def test_mkdir_parent_mode_deep_hierarchy(self):
2527+
# Test parent_mode with deep directory hierarchy
2528+
p = self.cls(self.base, 'level1PM', 'level2PM', 'level3PM')
2529+
self.assertFalse(p.exists())
2530+
if os.name != 'nt':
2531+
p.mkdir(0o755, parents=True, parent_mode=0o700)
2532+
self.assertTrue(p.exists())
2533+
# Check that all parent directories have parent_mode
2534+
level1 = self.cls(self.base, 'level1PM')
2535+
level2 = level1 / 'level2PM'
2536+
self.assertEqual(stat.S_IMODE(level1.stat().st_mode), 0o700)
2537+
self.assertEqual(stat.S_IMODE(level2.stat().st_mode), 0o700)
2538+
# Leaf directory has the regular mode
2539+
self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755)
2540+
2541+
@unittest.skipIf(
2542+
is_emscripten or is_wasi,
2543+
"umask is not implemented on Emscripten/WASI."
2544+
)
2545+
def test_mkdir_parent_mode_overrides_umask(self):
2546+
# Test that parent_mode overrides umask for parent directories
2547+
p = self.cls(self.base, 'overridetest', 'child')
2548+
self.assertFalse(p.exists())
2549+
if os.name != 'nt':
2550+
old_mask = os.umask(0o022) # Restrictive umask
2551+
try:
2552+
# parent_mode should override umask for parents
2553+
p.mkdir(0o755, parents=True, parent_mode=0o700)
2554+
self.assertTrue(p.exists())
2555+
# Leaf directory gets the specified mode
2556+
self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o755)
2557+
# Parent directory gets parent_mode, not affected by umask
2558+
self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 0o700)
2559+
finally:
2560+
os.umask(old_mask)
2561+
2562+
def test_mkdir_parent_mode_same_as_mode(self):
2563+
# Test setting parent_mode same as mode
2564+
p = self.cls(self.base, 'samedirPM', 'subdirPM')
2565+
self.assertFalse(p.exists())
2566+
if os.name != 'nt':
2567+
p.mkdir(0o705, parents=True, parent_mode=0o705)
2568+
self.assertTrue(p.exists())
2569+
# Both directories should have the same mode
2570+
self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o705)
2571+
self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), 0o705)
2572+
24922573
@needs_symlinks
24932574
def test_symlink_to(self):
24942575
P = self.cls(self.base)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The :meth:`pathlib.Path.mkdir` method now has a *parent_mode* parameter to
2+
specify the mode for intermediate directories when creating parent directories,
3+
providing consistency with the new :func:`os.makedirs` *parent_mode* parameter.

0 commit comments

Comments
 (0)