diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 0a2a4e3be0e..67ed914e9f8 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -791,7 +791,7 @@ call fails (for example because the path doesn't exist): the symbolic link's information rather than its target's. -.. method:: Path.mkdir(mode=0o777, parents=False) +.. method:: Path.mkdir(mode=0o777, parents=False, exist_ok=False) Create a new directory at this given path. If *mode* is given, it is combined with the process' ``umask`` value to determine the file mode @@ -805,6 +805,16 @@ call fails (for example because the path doesn't exist): If *parents* is false (the default), a missing parent raises :exc:`FileNotFoundError`. + If *exist_ok* is false (the default), an :exc:`FileExistsError` is + raised if the target directory already exists. + + If *exist_ok* is true, :exc:`FileExistsError` exceptions will be + ignored (same behavior as the POSIX ``mkdir -p`` command), but only if the + last path component is not an existing non-directory file. + + .. versionchanged:: 3.5 + The *exist_ok* parameter was added. + .. method:: Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 428de39f964..eff6ae3f0c9 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1106,14 +1106,21 @@ class Path(PurePath): fd = self._raw_open(flags, mode) os.close(fd) - def mkdir(self, mode=0o777, parents=False): + def mkdir(self, mode=0o777, parents=False, exist_ok=False): if self._closed: self._raise_closed() if not parents: - self._accessor.mkdir(self, mode) + try: + self._accessor.mkdir(self, mode) + except FileExistsError: + if not exist_ok or not self.is_dir(): + raise else: try: self._accessor.mkdir(self, mode) + except FileExistsError: + if not exist_ok or not self.is_dir(): + raise except OSError as e: if e.errno != ENOENT: raise diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index a45cf7e5887..4f762176ba9 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -4,13 +4,10 @@ import os import errno import pathlib import pickle -import shutil import socket import stat -import sys import tempfile import unittest -from contextlib import contextmanager from test import support TESTFN = support.TESTFN @@ -743,7 +740,6 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase): self.assertEqual(P('//Some/SHARE/a/B'), P('//somE/share/A/b')) def test_as_uri(self): - from urllib.parse import quote_from_bytes P = self.cls with self.assertRaises(ValueError): P('/a/b').as_uri() @@ -1617,6 +1613,59 @@ class _BasePathTest(object): # the parent's permissions follow the default process settings self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), mode) + def test_mkdir_exist_ok(self): + p = self.cls(BASE, 'dirB') + st_ctime_first = p.stat().st_ctime + self.assertTrue(p.exists()) + self.assertTrue(p.is_dir()) + with self.assertRaises(FileExistsError) as cm: + p.mkdir() + self.assertEqual(cm.exception.errno, errno.EEXIST) + p.mkdir(exist_ok=True) + self.assertTrue(p.exists()) + self.assertEqual(p.stat().st_ctime, st_ctime_first) + + def test_mkdir_exist_ok_with_parent(self): + p = self.cls(BASE, 'dirC') + self.assertTrue(p.exists()) + with self.assertRaises(FileExistsError) as cm: + p.mkdir() + self.assertEqual(cm.exception.errno, errno.EEXIST) + p = p / 'newdirC' + p.mkdir(parents=True) + st_ctime_first = p.stat().st_ctime + self.assertTrue(p.exists()) + with self.assertRaises(FileExistsError) as cm: + p.mkdir(parents=True) + self.assertEqual(cm.exception.errno, errno.EEXIST) + p.mkdir(parents=True, exist_ok=True) + self.assertTrue(p.exists()) + self.assertEqual(p.stat().st_ctime, st_ctime_first) + + def test_mkdir_with_child_file(self): + p = self.cls(BASE, 'dirB', 'fileB') + self.assertTrue(p.exists()) + # An exception is raised when the last path component is an existing + # regular file, regardless of whether exist_ok is true or not. + with self.assertRaises(FileExistsError) as cm: + p.mkdir(parents=True) + self.assertEqual(cm.exception.errno, errno.EEXIST) + with self.assertRaises(FileExistsError) as cm: + p.mkdir(parents=True, exist_ok=True) + self.assertEqual(cm.exception.errno, errno.EEXIST) + + def test_mkdir_no_parents_file(self): + p = self.cls(BASE, 'fileA') + self.assertTrue(p.exists()) + # An exception is raised when the last path component is an existing + # regular file, regardless of whether exist_ok is true or not. + with self.assertRaises(FileExistsError) as cm: + p.mkdir() + self.assertEqual(cm.exception.errno, errno.EEXIST) + with self.assertRaises(FileExistsError) as cm: + p.mkdir(exist_ok=True) + self.assertEqual(cm.exception.errno, errno.EEXIST) + @with_symlinks def test_symlink_to(self): P = self.cls(BASE) @@ -1852,7 +1901,6 @@ class PosixPathTest(_BasePathTest, unittest.TestCase): @with_symlinks def test_resolve_loop(self): # Loop detection for broken symlinks under POSIX - P = self.cls # Loops with relative symlinks os.symlink('linkX/inside', join('linkX')) self._check_symlink_loop(BASE, 'linkX') diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index b3de43b7abb..ededbdba421 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -307,7 +307,7 @@ class PlatformTest(unittest.TestCase): with mock.patch('platform._UNIXCONFDIR', tempdir): distname, version, distid = platform.linux_distribution() - self.assertEqual(distname, 'Fedora') + self.assertEqual(distname, 'Fedora') self.assertEqual(version, '19') self.assertEqual(distid, 'Schr\xf6dinger\u2019s Cat') diff --git a/Misc/NEWS b/Misc/NEWS index f0687e45bb8..99a50a5ba71 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -123,6 +123,10 @@ Core and Builtins Library ------- +- Issue #21539: Add a *exists_ok* argument to `Pathlib.mkdir()` to mimic + `mkdir -p` and `os.makedirs()` functionality. When true, ignore + FileExistsErrors. Patch by Berker Peksag. + - Issue #21047: set the default value for the *convert_charrefs* argument of HTMLParser to True. Patch by Berker Peksag.