Skip to content

bpo-45558: shutil.copytree: Allow disabling copystat #29130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
9 changes: 6 additions & 3 deletions Doc/library/shutil.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,19 @@ Directory and files operations


.. function:: copytree(src, dst, symlinks=False, ignore=None, \
copy_function=copy2, ignore_dangling_symlinks=False, \
dirs_exist_ok=False)
copy_stat=True, copy_function=copy2, \
ignore_dangling_symlinks=False, dirs_exist_ok=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inserting this new parameter before existing ones will break existing uses if passed positionally. Was this position chosen for a reason?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The position was chosen so it'd be near the copy_function parameter.


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`.
unless *copy_stat* is `False`. Individual files are copied using
:func:`~shutil.copy2` by default. Using `copy_stat=False` along with
`copy_function=shutil.copyfile` is useful when you wish to not use the
source permissions at all.

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
31 changes: 20 additions & 11 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ def _ignore_patterns(path, names):
return _ignore_patterns

def _copytree(entries, src, dst, symlinks, ignore, copy_function,
ignore_dangling_symlinks, dirs_exist_ok=False):
copy_stat, ignore_dangling_symlinks, dirs_exist_ok=False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy_stat should be added to the docs too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy_stat should be added to the docs too.

Do you mean to add this to the docs of _copytree? I thought it is a "hidden" function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, sorry, I commented on the wrong location. This should be added to the docs of shutil.copytree, it still does not list the new copy_stat argument in the function definition (should be line 228), it only mentions it in the description.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I think understood you correctly.

if ignore is not None:
ignored_names = ignore(os.fspath(src), [x.name for x in entries])
else:
Expand Down Expand Up @@ -481,20 +481,22 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
# code with a custom `copy_function` may rely on copytree
# doing the right thing.
os.symlink(linkto, dstname)
copystat(srcobj, dstname, follow_symlinks=not symlinks)
if copy_stat:
copystat(srcobj, dstname, follow_symlinks=not symlinks)
else:
# ignore dangling symlink if the flag is on
if not os.path.exists(linkto) and ignore_dangling_symlinks:
continue
# otherwise let the copy occur. copy2 will raise an error
if srcentry.is_dir():
copytree(srcobj, dstname, symlinks, ignore,
copy_function, dirs_exist_ok=dirs_exist_ok)
copy_function, copy_stat,
dirs_exist_ok=dirs_exist_ok)
else:
copy_function(srcobj, dstname)
elif srcentry.is_dir():
copytree(srcobj, dstname, symlinks, ignore, copy_function,
dirs_exist_ok=dirs_exist_ok)
copy_stat, dirs_exist_ok=dirs_exist_ok)
else:
# Will raise a SpecialFileError for unsupported file types
copy_function(srcobj, dstname)
Expand All @@ -504,18 +506,20 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
errors.extend(err.args[0])
except OSError as why:
errors.append((srcname, dstname, str(why)))
try:
copystat(src, dst)
except OSError as why:
# Copying file access times may fail on Windows
if getattr(why, 'winerror', None) is None:
errors.append((src, dst, str(why)))
if copy_stat:
try:
copystat(src, dst)
except OSError as why:
# Copying file access times may fail on Windows
if getattr(why, 'winerror', None) is None:
errors.append((src, dst, str(why)))
if errors:
raise Error(errors)
return dst

def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
ignore_dangling_symlinks=False, dirs_exist_ok=False):
copy_stat=True, 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
Expand Down Expand Up @@ -551,12 +555,17 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
destination path as arguments. By default, copy2() is used, but any
function that supports the same signature (like copy()) can be used.

The optional copy_stat argument controls whether permissions will be
copied from the source to the destination. This can be used along with
copy_function=copyfile when you wish to not use the source permissions
at all.
"""
sys.audit("shutil.copytree", src, dst)
with os.scandir(src) as itr:
entries = list(itr)
return _copytree(entries=entries, src=src, dst=dst, symlinks=symlinks,
ignore=ignore, copy_function=copy_function,
copy_stat=copy_stat,
ignore_dangling_symlinks=ignore_dangling_symlinks,
dirs_exist_ok=dirs_exist_ok)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:func:`shutil.copytree`: Add a ``copy_stat`` boolean option that controls whether permissions will be copied as well from the source directory.