[Python-checkins] bpo-20849: add dirs_exist_ok arg to shutil.copytree (patch by Josh Bronson)

Giampaolo Rodola webhook-mailer at python.org
Fri Dec 28 13:03:45 EST 2018


https://github.com/python/cpython/commit/9e00d9e88fbf943987e4771c753f5ca8f794103e
commit: 9e00d9e88fbf943987e4771c753f5ca8f794103e
branch: master
author: jab <jab at users.noreply.github.com>
committer: Giampaolo Rodola <g.rodola at gmail.com>
date: 2018-12-28T19:03:40+01:00
summary:

bpo-20849: add dirs_exist_ok arg to shutil.copytree (patch by Josh Bronson)

files:
A Misc/NEWS.d/next/Library/2018-08-16-16-47-15.bpo-20849.YWJECC.rst
M Doc/library/shutil.rst
M Doc/whatsnew/3.8.rst
M Lib/shutil.py
M Lib/test/test_shutil.py

diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst
index 7a596eeff682..427a12015963 100644
--- a/Doc/library/shutil.rst
+++ b/Doc/library/shutil.rst
@@ -209,14 +209,16 @@ Directory and files operations
 
 
 .. function:: copytree(src, dst, symlinks=False, ignore=None, \
-              copy_function=copy2, ignore_dangling_symlinks=False)
+              copy_function=copy2, ignore_dangling_symlinks=False, \
+              dirs_exist_ok=False)
 
-   Recursively copy an entire directory tree rooted at *src*, returning the
-   destination directory.  The destination
-   directory, named by *dst*, must not already exist; it will be created as
-   well as missing parent directories.  Permissions and times of directories
-   are copied with :func:`copystat`, individual files are copied using
-   :func:`shutil.copy2`.
+   Recursively copy an entire directory tree rooted at *src* to a directory
+   named *dst* and return the destination directory. *dirs_exist_ok* dictates
+   whether to raise an exception in case *dst* or any missing parent directory
+   already exists.
+
+   Permissions and times of directories are copied with :func:`copystat`,
+   individual files are copied using :func:`shutil.copy2`.
 
    If *symlinks* is true, symbolic links in the source tree are represented as
    symbolic links in the new tree and the metadata of the original links will
@@ -262,6 +264,9 @@ Directory and files operations
       copy the file more efficiently. See
       :ref:`shutil-platform-dependent-efficient-copy-operations` section.
 
+   .. versionadded:: 3.8
+      The *dirs_exist_ok* parameter.
+
 .. function:: rmtree(path, ignore_errors=False, onerror=None)
 
    .. index:: single: directory; deleting
diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst
index 2d45e7e94deb..c592f00d2d9d 100644
--- a/Doc/whatsnew/3.8.rst
+++ b/Doc/whatsnew/3.8.rst
@@ -196,6 +196,14 @@ pathlib
 contain characters unrepresentable at the OS level.
 (Contributed by Serhiy Storchaka in :issue:`33721`.)
 
+
+shutil
+------
+
+:func:`shutil.copytree` now accepts a new ``dirs_exist_ok`` keyword argument.
+(Contributed by Josh Bronson in :issue:`20849`.)
+
+
 ssl
 ---
 
@@ -284,7 +292,6 @@ Optimizations
   syscalls is reduced by 38% making :func:`shutil.copytree` especially faster
   on network filesystems. (Contributed by Giampaolo Rodola' in :issue:`33695`.)
 
-
 * The default protocol in the :mod:`pickle` module is now Protocol 4,
   first introduced in Python 3.4.  It offers better performance and smaller
   size compared to Protocol 3 available since Python 3.0.
diff --git a/Lib/shutil.py b/Lib/shutil.py
index 74348ba62ef7..8d0de72b44a3 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -432,13 +432,13 @@ def _ignore_patterns(path, names):
     return _ignore_patterns
 
 def _copytree(entries, src, dst, symlinks, ignore, copy_function,
-              ignore_dangling_symlinks):
+              ignore_dangling_symlinks, dirs_exist_ok=False):
     if ignore is not None:
         ignored_names = ignore(src, set(os.listdir(src)))
     else:
         ignored_names = set()
 
-    os.makedirs(dst)
+    os.makedirs(dst, exist_ok=dirs_exist_ok)
     errors = []
     use_srcentry = copy_function is copy2 or copy_function is copy
 
@@ -461,14 +461,15 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
                     # ignore dangling symlink if the flag is on
                     if not os.path.exists(linkto) and ignore_dangling_symlinks:
                         continue
-                    # otherwise let the copy occurs. copy2 will raise an error
+                    # otherwise let the copy occur. copy2 will raise an error
                     if srcentry.is_dir():
                         copytree(srcobj, dstname, symlinks, ignore,
-                                 copy_function)
+                                 copy_function, dirs_exist_ok=dirs_exist_ok)
                     else:
                         copy_function(srcobj, dstname)
             elif srcentry.is_dir():
-                copytree(srcobj, dstname, symlinks, ignore, copy_function)
+                copytree(srcobj, dstname, symlinks, ignore, copy_function,
+                         dirs_exist_ok=dirs_exist_ok)
             else:
                 # Will raise a SpecialFileError for unsupported file types
                 copy_function(srcentry, dstname)
@@ -489,10 +490,12 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
     return dst
 
 def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
-             ignore_dangling_symlinks=False):
-    """Recursively copy a directory tree.
+             ignore_dangling_symlinks=False, dirs_exist_ok=False):
+    """Recursively copy a directory tree and return the destination directory.
+
+    dirs_exist_ok dictates whether to raise an exception in case dst or any
+    missing parent directory already exists.
 
-    The destination directory must not already exist.
     If exception(s) occur, an Error is raised with a list of reasons.
 
     If the optional symlinks flag is true, symbolic links in the
@@ -527,7 +530,8 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
     with os.scandir(src) as entries:
         return _copytree(entries=entries, src=src, dst=dst, symlinks=symlinks,
                          ignore=ignore, copy_function=copy_function,
-                         ignore_dangling_symlinks=ignore_dangling_symlinks)
+                         ignore_dangling_symlinks=ignore_dangling_symlinks,
+                         dirs_exist_ok=dirs_exist_ok)
 
 # version vulnerable to race conditions
 def _rmtree_unsafe(path, onerror):
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
index ec8fcc3eef01..6f22e5378ff2 100644
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -691,6 +691,31 @@ def test_copytree_simple(self):
         actual = read_file((dst_dir, 'test_dir', 'test.txt'))
         self.assertEqual(actual, '456')
 
+    def test_copytree_dirs_exist_ok(self):
+        src_dir = tempfile.mkdtemp()
+        dst_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, src_dir)
+        self.addCleanup(shutil.rmtree, dst_dir)
+
+        write_file((src_dir, 'nonexisting.txt'), '123')
+        os.mkdir(os.path.join(src_dir, 'existing_dir'))
+        os.mkdir(os.path.join(dst_dir, 'existing_dir'))
+        write_file((dst_dir, 'existing_dir', 'existing.txt'), 'will be replaced')
+        write_file((src_dir, 'existing_dir', 'existing.txt'), 'has been replaced')
+
+        shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True)
+        self.assertTrue(os.path.isfile(os.path.join(dst_dir, 'nonexisting.txt')))
+        self.assertTrue(os.path.isdir(os.path.join(dst_dir, 'existing_dir')))
+        self.assertTrue(os.path.isfile(os.path.join(dst_dir, 'existing_dir',
+                                                    'existing.txt')))
+        actual = read_file((dst_dir, 'nonexisting.txt'))
+        self.assertEqual(actual, '123')
+        actual = read_file((dst_dir, 'existing_dir', 'existing.txt'))
+        self.assertEqual(actual, 'has been replaced')
+
+        with self.assertRaises(FileExistsError):
+            shutil.copytree(src_dir, dst_dir, dirs_exist_ok=False)
+
     @support.skip_unless_symlink
     def test_copytree_symlinks(self):
         tmp_dir = self.mkdtemp()
diff --git a/Misc/NEWS.d/next/Library/2018-08-16-16-47-15.bpo-20849.YWJECC.rst b/Misc/NEWS.d/next/Library/2018-08-16-16-47-15.bpo-20849.YWJECC.rst
new file mode 100644
index 000000000000..8ef544ba1e34
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-08-16-16-47-15.bpo-20849.YWJECC.rst
@@ -0,0 +1,2 @@
+shutil.copytree now accepts a new ``dirs_exist_ok`` keyword argument.
+Patch by Josh Bronson.



More information about the Python-checkins mailing list