Skip to content

Commit d054024

Browse files
committed
bpo-45558: shutil.copytree: Allow disabling copystat
1 parent 311910b commit d054024

3 files changed

Lines changed: 27 additions & 14 deletions

File tree

‎Doc/library/shutil.rst‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,16 +226,19 @@ Directory and files operations
226226

227227

228228
.. function:: copytree(src, dst, symlinks=False, ignore=None, \
229-
copy_function=copy2, ignore_dangling_symlinks=False, \
230-
dirs_exist_ok=False)
229+
copy_stat=True, copy_function=copy2, \
230+
ignore_dangling_symlinks=False, dirs_exist_ok=False)
231231

232232
Recursively copy an entire directory tree rooted at *src* to a directory
233233
named *dst* and return the destination directory. *dirs_exist_ok* dictates
234234
whether to raise an exception in case *dst* or any missing parent directory
235235
already exists.
236236

237237
Permissions and times of directories are copied with :func:`copystat`,
238-
individual files are copied using :func:`~shutil.copy2`.
238+
unless *copy_stat* is `False`. Individual files are copied using
239+
:func:`~shutil.copy2` by default. Using `copy_stat=False` along with
240+
`copy_function=shutil.copyfile` is useful when you wish to not use the
241+
source permissions at all.
239242

240243
If *symlinks* is true, symbolic links in the source tree are represented as
241244
symbolic links in the new tree and the metadata of the original links will

‎Lib/shutil.py‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ def _ignore_patterns(path, names):
450450
return _ignore_patterns
451451

452452
def _copytree(entries, src, dst, symlinks, ignore, copy_function,
453-
ignore_dangling_symlinks, dirs_exist_ok=False):
453+
copy_stat, ignore_dangling_symlinks, dirs_exist_ok=False):
454454
if ignore is not None:
455455
ignored_names = ignore(os.fspath(src), [x.name for x in entries])
456456
else:
@@ -481,20 +481,22 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
481481
# code with a custom `copy_function` may rely on copytree
482482
# doing the right thing.
483483
os.symlink(linkto, dstname)
484-
copystat(srcobj, dstname, follow_symlinks=not symlinks)
484+
if copy_stat:
485+
copystat(srcobj, dstname, follow_symlinks=not symlinks)
485486
else:
486487
# ignore dangling symlink if the flag is on
487488
if not os.path.exists(linkto) and ignore_dangling_symlinks:
488489
continue
489490
# otherwise let the copy occur. copy2 will raise an error
490491
if srcentry.is_dir():
491492
copytree(srcobj, dstname, symlinks, ignore,
492-
copy_function, dirs_exist_ok=dirs_exist_ok)
493+
copy_function, copy_stat,
494+
dirs_exist_ok=dirs_exist_ok)
493495
else:
494496
copy_function(srcobj, dstname)
495497
elif srcentry.is_dir():
496498
copytree(srcobj, dstname, symlinks, ignore, copy_function,
497-
dirs_exist_ok=dirs_exist_ok)
499+
copy_stat, dirs_exist_ok=dirs_exist_ok)
498500
else:
499501
# Will raise a SpecialFileError for unsupported file types
500502
copy_function(srcobj, dstname)
@@ -504,18 +506,20 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
504506
errors.extend(err.args[0])
505507
except OSError as why:
506508
errors.append((srcname, dstname, str(why)))
507-
try:
508-
copystat(src, dst)
509-
except OSError as why:
510-
# Copying file access times may fail on Windows
511-
if getattr(why, 'winerror', None) is None:
512-
errors.append((src, dst, str(why)))
509+
if copy_stat:
510+
try:
511+
copystat(src, dst)
512+
except OSError as why:
513+
# Copying file access times may fail on Windows
514+
if getattr(why, 'winerror', None) is None:
515+
errors.append((src, dst, str(why)))
513516
if errors:
514517
raise Error(errors)
515518
return dst
516519

517520
def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
518-
ignore_dangling_symlinks=False, dirs_exist_ok=False):
521+
copy_stat=True, ignore_dangling_symlinks=False,
522+
dirs_exist_ok=False):
519523
"""Recursively copy a directory tree and return the destination directory.
520524
521525
dirs_exist_ok dictates whether to raise an exception in case dst or any
@@ -551,12 +555,17 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
551555
destination path as arguments. By default, copy2() is used, but any
552556
function that supports the same signature (like copy()) can be used.
553557
558+
The optional copy_stat argument controls whether permissions will be
559+
copied from the source to the destination. This can be used along with
560+
copy_function=copyfile when you wish to not use the source permissions
561+
at all.
554562
"""
555563
sys.audit("shutil.copytree", src, dst)
556564
with os.scandir(src) as itr:
557565
entries = list(itr)
558566
return _copytree(entries=entries, src=src, dst=dst, symlinks=symlinks,
559567
ignore=ignore, copy_function=copy_function,
568+
copy_stat=copy_stat,
560569
ignore_dangling_symlinks=ignore_dangling_symlinks,
561570
dirs_exist_ok=dirs_exist_ok)
562571

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:func:`shutil.copytree`: Add a ``copy_stat`` boolean option that controls whether permissions will be copied as well from the source directory.

0 commit comments

Comments
 (0)