diff --git a/Doc/library/os.rst b/Doc/library/os.rst --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -1548,7 +1548,7 @@ The *dir_fd* argument. -.. function:: makedirs(path, mode=0o777, exist_ok=False) +.. function:: makedirs(path, mode=0o777, exist_ok=False, on_wrong_mode=os.FAIL) .. index:: single: directory; creating @@ -1562,9 +1562,17 @@ If *exists_ok* is ``False`` (the default), an :exc:`OSError` is raised if the target directory already exists. If *exists_ok* is ``True`` an - :exc:`OSError` is still raised if the umask-masked *mode* is different from - the existing mode, on systems where the mode is used. :exc:`OSError` will - also be raised if the directory creation fails. + :exc:`OSError` is still raised if *on_wrong_mode* is :data:`FAIL` (the + default) and the umask-masked *mode* is different from the existing mode, on + systems where the mode is used. To avoid this, it's possible to either + supply :data:`IGNORE` or a callable for *on_wrong_mode*. In the first case, + mode mismatches are simply ignored. In the second case the callable is + invoked with the path of the directory in question and its conflicting mode. + It's up to the callable to decide whether this conflict is acceptable (do + nothing), fix it, or abort (raise an exception itself). If the callable + returns, makedirs continues as if :data:`IGNORE` has been supplied. + + :exc:`OSError` will also be raised if the directory creation fails. .. note:: @@ -1576,6 +1584,30 @@ .. versionadded:: 3.2 The *exist_ok* parameter. + .. versionadded:: 3.4 + The *on_wrong_mode* parameter. + + +.. data:: FAIL + + If supplied as *on_wrong_mode* for :func:`makedirs` together with + *exist_ok=True*, calling it on an already existing directory with + a conflicting mode will raise a :exc:`OSError`. This is the + default. + + .. versionadded:: 3.4 + + +.. data:: IGNORE + + If supplied as *on_wrong_mode* for :func:`makedirs` together with + ``exist_ok=True``, calling it on an already existing directory will succeed + regardless whether the actual mode of the directory conflicts with the + *mode* supplied. + + .. versionadded:: 3.4 + + .. function:: mkfifo(path, mode=0o666, *, dir_fd=None) diff --git a/Lib/os.py b/Lib/os.py --- a/Lib/os.py +++ b/Lib/os.py @@ -215,15 +215,27 @@ # Super directory utilities. # (Inspired by Eric Raymond; the doc strings are mostly his) -def makedirs(name, mode=0o777, exist_ok=False): - """makedirs(path [, mode=0o777][, exist_ok=False]) +FAIL = 0 +IGNORE = 1 + +def makedirs(name, mode=0o777, exist_ok=False, on_wrong_mode=FAIL): + """makedirs(path [, mode=0o777][, exist_ok=False, [on_wrong_mode=FAIL]]) 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 with the same mode as we specified already exists, - raises an OSError if exist_ok is False, otherwise no exception is - raised. This is recursive. + just the rightmost) will be created if it does not exist. + + If the target directory already exists, the behavior of makedirs depends on + the mode of the directory and the values of exist_ok and on_wrong_mode: + + - If exist_ok is False, FileExistsError is raised. + - If exist_ok is True and the mode matches, makedirs succeeds. + - If exist_ok is True and the mode doesn't match, on_wrong_mode + determines further action: os.FAIL (default) raises FileExistsError, + os.IGNORE succeeds. If a callable is passed instead, it gets called + with the directory name in question and the mode of it as arguments. + + This is recursive. """ head, tail = path.split(name) @@ -253,9 +265,18 @@ if not (e.errno == errno.EEXIST and exist_ok and dir_exists and actual_mode == expected_mode): if dir_exists and actual_mode != expected_mode: - e.strerror += ' (mode %o != expected mode %o)' % ( + if on_wrong_mode == FAIL: + e.strerror += ' (mode %o != expected mode %o)' % ( actual_mode, expected_mode) - raise + raise + elif on_wrong_mode == IGNORE: + pass + elif callable(on_wrong_mode): + on_wrong_mode(name, actual_mode) + else: + raise ValueError('Invalid value for on_wrong_mode.') + else: + raise def removedirs(name): """removedirs(path) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -2,6 +2,9 @@ # does add tests for a few functions which have been determined to be more # portable than they had been thought to be. +from test import support +from unittest import mock + import os import errno import unittest @@ -11,7 +14,6 @@ import subprocess import time import shutil -from test import support import contextlib import mmap import platform @@ -894,6 +896,19 @@ self.assertRaises(OSError, os.makedirs, path, exist_ok=True) os.remove(path) + def test_on_wrong_mode(self): + d = os.path.join(support.TESTFN, 'dir1') + os.makedirs(d, mode=0o333) + self.assertRaises(FileExistsError, os.makedirs, d, mode=0o777) + self.assertRaises(FileExistsError, os.makedirs, d, mode=0o777, + exist_ok=True) + self.assertRaises(FileExistsError, os.makedirs, d, mode=0o777, + exist_ok=True, on_wrong_mode=os.FAIL) + os.makedirs(d, mode=0o777, exist_ok=True, on_wrong_mode=os.IGNORE) + m = mock.MagicMock(name='on_wrong_mode') + os.makedirs(d, mode=0o777, exist_ok=True, on_wrong_mode=m) + m.assert_called_once_with(d, 0o311) + def tearDown(self): path = os.path.join(support.TESTFN, 'dir1', 'dir2', 'dir3', 'dir4', 'dir5', 'dir6')