summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/shutil.rst19
-rw-r--r--Doc/whatsnew/3.8.rst9
-rw-r--r--Lib/shutil.py22
-rw-r--r--Lib/test/test_shutil.py25
-rw-r--r--Misc/NEWS.d/next/Library/2018-08-16-16-47-15.bpo-20849.YWJECC.rst2
5 files changed, 60 insertions, 17 deletions
diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst
index 7a596ee..427a120 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 2d45e7e..c592f00 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 74348ba..8d0de72 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -432,13 +432,13 @@ def ignore_patterns(*patterns):
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 ec8fcc3..6f22e53 100644
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -691,6 +691,31 @@ class TestShutil(unittest.TestCase):
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 0000000..8ef544b
--- /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.