Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions Doc/library/shutil.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion Doc/whatsnew/3.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

Expand Down Expand Up @@ -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.
Expand Down
22 changes: 13 additions & 9 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down
25 changes: 25 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
shutil.copytree now accepts a new ``dirs_exist_ok`` keyword argument.
Patch by Josh Bronson.