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

This commit is contained in:
jab 2018-12-28 13:03:40 -05:00 committed by Giampaolo Rodola
parent ed57e13df6
commit 9e00d9e88f
5 changed files with 60 additions and 17 deletions

View File

@ -209,14 +209,16 @@ Directory and files operations
.. function:: copytree(src, dst, symlinks=False, ignore=None, \ .. 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 Recursively copy an entire directory tree rooted at *src* to a directory
destination directory. The destination named *dst* and return the destination directory. *dirs_exist_ok* dictates
directory, named by *dst*, must not already exist; it will be created as whether to raise an exception in case *dst* or any missing parent directory
well as missing parent directories. Permissions and times of directories already exists.
are copied with :func:`copystat`, individual files are copied using
:func:`shutil.copy2`. 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 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 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 copy the file more efficiently. See
:ref:`shutil-platform-dependent-efficient-copy-operations` section. :ref:`shutil-platform-dependent-efficient-copy-operations` section.
.. versionadded:: 3.8
The *dirs_exist_ok* parameter.
.. function:: rmtree(path, ignore_errors=False, onerror=None) .. function:: rmtree(path, ignore_errors=False, onerror=None)
.. index:: single: directory; deleting .. index:: single: directory; deleting

View File

@ -196,6 +196,14 @@ pathlib
contain characters unrepresentable at the OS level. contain characters unrepresentable at the OS level.
(Contributed by Serhiy Storchaka in :issue:`33721`.) (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 ssl
--- ---
@ -284,7 +292,6 @@ Optimizations
syscalls is reduced by 38% making :func:`shutil.copytree` especially faster syscalls is reduced by 38% making :func:`shutil.copytree` especially faster
on network filesystems. (Contributed by Giampaolo Rodola' in :issue:`33695`.) on network filesystems. (Contributed by Giampaolo Rodola' in :issue:`33695`.)
* The default protocol in the :mod:`pickle` module is now Protocol 4, * The default protocol in the :mod:`pickle` module is now Protocol 4,
first introduced in Python 3.4. It offers better performance and smaller first introduced in Python 3.4. It offers better performance and smaller
size compared to Protocol 3 available since Python 3.0. size compared to Protocol 3 available since Python 3.0.

View File

@ -432,13 +432,13 @@ def ignore_patterns(*patterns):
return _ignore_patterns return _ignore_patterns
def _copytree(entries, src, dst, symlinks, ignore, copy_function, def _copytree(entries, src, dst, symlinks, ignore, copy_function,
ignore_dangling_symlinks): ignore_dangling_symlinks, dirs_exist_ok=False):
if ignore is not None: if ignore is not None:
ignored_names = ignore(src, set(os.listdir(src))) ignored_names = ignore(src, set(os.listdir(src)))
else: else:
ignored_names = set() ignored_names = set()
os.makedirs(dst) os.makedirs(dst, exist_ok=dirs_exist_ok)
errors = [] errors = []
use_srcentry = copy_function is copy2 or copy_function is copy 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 # ignore dangling symlink if the flag is on
if not os.path.exists(linkto) and ignore_dangling_symlinks: if not os.path.exists(linkto) and ignore_dangling_symlinks:
continue 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(): if srcentry.is_dir():
copytree(srcobj, dstname, symlinks, ignore, copytree(srcobj, dstname, symlinks, ignore,
copy_function) copy_function, dirs_exist_ok=dirs_exist_ok)
else: else:
copy_function(srcobj, dstname) copy_function(srcobj, dstname)
elif srcentry.is_dir(): 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: else:
# Will raise a SpecialFileError for unsupported file types # Will raise a SpecialFileError for unsupported file types
copy_function(srcentry, dstname) copy_function(srcentry, dstname)
@ -489,10 +490,12 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
return dst return dst
def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
ignore_dangling_symlinks=False): ignore_dangling_symlinks=False, dirs_exist_ok=False):
"""Recursively copy a directory tree. """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 exception(s) occur, an Error is raised with a list of reasons.
If the optional symlinks flag is true, symbolic links in the 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: with os.scandir(src) as entries:
return _copytree(entries=entries, src=src, dst=dst, symlinks=symlinks, return _copytree(entries=entries, src=src, dst=dst, symlinks=symlinks,
ignore=ignore, copy_function=copy_function, 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 # version vulnerable to race conditions
def _rmtree_unsafe(path, onerror): def _rmtree_unsafe(path, onerror):

View File

@ -691,6 +691,31 @@ class TestShutil(unittest.TestCase):
actual = read_file((dst_dir, 'test_dir', 'test.txt')) actual = read_file((dst_dir, 'test_dir', 'test.txt'))
self.assertEqual(actual, '456') 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 @support.skip_unless_symlink
def test_copytree_symlinks(self): def test_copytree_symlinks(self):
tmp_dir = self.mkdtemp() tmp_dir = self.mkdtemp()

View File

@ -0,0 +1,2 @@
shutil.copytree now accepts a new ``dirs_exist_ok`` keyword argument.
Patch by Josh Bronson.