bpo-20849: add dirs_exist_ok arg to shutil.copytree (patch by Josh Bronson)
This commit is contained in:
parent
ed57e13df6
commit
9e00d9e88f
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
shutil.copytree now accepts a new ``dirs_exist_ok`` keyword argument.
|
||||||
|
Patch by Josh Bronson.
|
Loading…
Reference in New Issue