Skip to content

Commit 9e00d9e

Browse files
jabgiampaolo
authored andcommitted
bpo-20849: add dirs_exist_ok arg to shutil.copytree (patch by Josh Bronson)
1 parent ed57e13 commit 9e00d9e

File tree

5 files changed

+60
-17
lines changed

5 files changed

+60
-17
lines changed

Doc/library/shutil.rst

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,16 @@ Directory and files operations
209209

210210

211211
.. function:: copytree(src, dst, symlinks=False, ignore=None, \
212-
copy_function=copy2, ignore_dangling_symlinks=False)
212+
copy_function=copy2, ignore_dangling_symlinks=False, \
213+
dirs_exist_ok=False)
213214

214-
Recursively copy an entire directory tree rooted at *src*, returning the
215-
destination directory. The destination
216-
directory, named by *dst*, must not already exist; it will be created as
217-
well as missing parent directories. Permissions and times of directories
218-
are copied with :func:`copystat`, individual files are copied using
219-
:func:`shutil.copy2`.
215+
Recursively copy an entire directory tree rooted at *src* to a directory
216+
named *dst* and return the destination directory. *dirs_exist_ok* dictates
217+
whether to raise an exception in case *dst* or any missing parent directory
218+
already exists.
219+
220+
Permissions and times of directories are copied with :func:`copystat`,
221+
individual files are copied using :func:`shutil.copy2`.
220222

221223
If *symlinks* is true, symbolic links in the source tree are represented as
222224
symbolic links in the new tree and the metadata of the original links will
@@ -262,6 +264,9 @@ Directory and files operations
262264
copy the file more efficiently. See
263265
:ref:`shutil-platform-dependent-efficient-copy-operations` section.
264266

267+
.. versionadded:: 3.8
268+
The *dirs_exist_ok* parameter.
269+
265270
.. function:: rmtree(path, ignore_errors=False, onerror=None)
266271

267272
.. index:: single: directory; deleting

Doc/whatsnew/3.8.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ pathlib
196196
contain characters unrepresentable at the OS level.
197197
(Contributed by Serhiy Storchaka in :issue:`33721`.)
198198

199+
200+
shutil
201+
------
202+
203+
:func:`shutil.copytree` now accepts a new ``dirs_exist_ok`` keyword argument.
204+
(Contributed by Josh Bronson in :issue:`20849`.)
205+
206+
199207
ssl
200208
---
201209

@@ -284,7 +292,6 @@ Optimizations
284292
syscalls is reduced by 38% making :func:`shutil.copytree` especially faster
285293
on network filesystems. (Contributed by Giampaolo Rodola' in :issue:`33695`.)
286294

287-
288295
* The default protocol in the :mod:`pickle` module is now Protocol 4,
289296
first introduced in Python 3.4. It offers better performance and smaller
290297
size compared to Protocol 3 available since Python 3.0.

Lib/shutil.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -432,13 +432,13 @@ def _ignore_patterns(path, names):
432432
return _ignore_patterns
433433

434434
def _copytree(entries, src, dst, symlinks, ignore, copy_function,
435-
ignore_dangling_symlinks):
435+
ignore_dangling_symlinks, dirs_exist_ok=False):
436436
if ignore is not None:
437437
ignored_names = ignore(src, set(os.listdir(src)))
438438
else:
439439
ignored_names = set()
440440

441-
os.makedirs(dst)
441+
os.makedirs(dst, exist_ok=dirs_exist_ok)
442442
errors = []
443443
use_srcentry = copy_function is copy2 or copy_function is copy
444444

@@ -461,14 +461,15 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
461461
# ignore dangling symlink if the flag is on
462462
if not os.path.exists(linkto) and ignore_dangling_symlinks:
463463
continue
464-
# otherwise let the copy occurs. copy2 will raise an error
464+
# otherwise let the copy occur. copy2 will raise an error
465465
if srcentry.is_dir():
466466
copytree(srcobj, dstname, symlinks, ignore,
467-
copy_function)
467+
copy_function, dirs_exist_ok=dirs_exist_ok)
468468
else:
469469
copy_function(srcobj, dstname)
470470
elif srcentry.is_dir():
471-
copytree(srcobj, dstname, symlinks, ignore, copy_function)
471+
copytree(srcobj, dstname, symlinks, ignore, copy_function,
472+
dirs_exist_ok=dirs_exist_ok)
472473
else:
473474
# Will raise a SpecialFileError for unsupported file types
474475
copy_function(srcentry, dstname)
@@ -489,10 +490,12 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
489490
return dst
490491

491492
def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
492-
ignore_dangling_symlinks=False):
493-
"""Recursively copy a directory tree.
493+
ignore_dangling_symlinks=False, dirs_exist_ok=False):
494+
"""Recursively copy a directory tree and return the destination directory.
495+
496+
dirs_exist_ok dictates whether to raise an exception in case dst or any
497+
missing parent directory already exists.
494498
495-
The destination directory must not already exist.
496499
If exception(s) occur, an Error is raised with a list of reasons.
497500
498501
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,
527530
with os.scandir(src) as entries:
528531
return _copytree(entries=entries, src=src, dst=dst, symlinks=symlinks,
529532
ignore=ignore, copy_function=copy_function,
530-
ignore_dangling_symlinks=ignore_dangling_symlinks)
533+
ignore_dangling_symlinks=ignore_dangling_symlinks,
534+
dirs_exist_ok=dirs_exist_ok)
531535

532536
# version vulnerable to race conditions
533537
def _rmtree_unsafe(path, onerror):

Lib/test/test_shutil.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,31 @@ def test_copytree_simple(self):
691691
actual = read_file((dst_dir, 'test_dir', 'test.txt'))
692692
self.assertEqual(actual, '456')
693693

694+
def test_copytree_dirs_exist_ok(self):
695+
src_dir = tempfile.mkdtemp()
696+
dst_dir = tempfile.mkdtemp()
697+
self.addCleanup(shutil.rmtree, src_dir)
698+
self.addCleanup(shutil.rmtree, dst_dir)
699+
700+
write_file((src_dir, 'nonexisting.txt'), '123')
701+
os.mkdir(os.path.join(src_dir, 'existing_dir'))
702+
os.mkdir(os.path.join(dst_dir, 'existing_dir'))
703+
write_file((dst_dir, 'existing_dir', 'existing.txt'), 'will be replaced')
704+
write_file((src_dir, 'existing_dir', 'existing.txt'), 'has been replaced')
705+
706+
shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True)
707+
self.assertTrue(os.path.isfile(os.path.join(dst_dir, 'nonexisting.txt')))
708+
self.assertTrue(os.path.isdir(os.path.join(dst_dir, 'existing_dir')))
709+
self.assertTrue(os.path.isfile(os.path.join(dst_dir, 'existing_dir',
710+
'existing.txt')))
711+
actual = read_file((dst_dir, 'nonexisting.txt'))
712+
self.assertEqual(actual, '123')
713+
actual = read_file((dst_dir, 'existing_dir', 'existing.txt'))
714+
self.assertEqual(actual, 'has been replaced')
715+
716+
with self.assertRaises(FileExistsError):
717+
shutil.copytree(src_dir, dst_dir, dirs_exist_ok=False)
718+
694719
@support.skip_unless_symlink
695720
def test_copytree_symlinks(self):
696721
tmp_dir = self.mkdtemp()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
shutil.copytree now accepts a new ``dirs_exist_ok`` keyword argument.
2+
Patch by Josh Bronson.

0 commit comments

Comments
 (0)