https://github.com/python/cpython/commit/c4c7097e64b0c9cb0081de8872b90594865c892b
commit: c4c7097e64b0c9cb0081de8872b90594865c892b
branch: main
author: Barney Gale <[email protected]>
committer: barneygale <[email protected]>
date: 2024-07-20T23:32:52+01:00
summary:
GH-73991: Support preserving metadata in `pathlib.Path.copytree()` (#121438)
Add *preserve_metadata* keyword-only argument to `pathlib.Path.copytree()`,
defaulting to false. When set to true, we copy timestamps, permissions,
extended attributes and flags where available, like `shutil.copystat()`.
files:
M Doc/library/pathlib.rst
M Lib/pathlib/_abc.py
M Lib/test/test_pathlib/test_pathlib.py
diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst
index a74b1321cb4b1d..496a12a296e443 100644
--- a/Doc/library/pathlib.rst
+++ b/Doc/library/pathlib.rst
@@ -1557,7 +1557,8 @@ Copying, renaming and deleting
.. versionadded:: 3.14
-.. method:: Path.copytree(target, *, follow_symlinks=True,
dirs_exist_ok=False, \
+.. method:: Path.copytree(target, *, follow_symlinks=True, \
+ preserve_metadata=False, dirs_exist_ok=False, \
ignore=None, on_error=None)
Recursively copy this directory tree to the given destination.
@@ -1566,6 +1567,13 @@ Copying, renaming and deleting
true (the default), the symlink's target is copied. Otherwise, the symlink
is recreated in the destination tree.
+ If *preserve_metadata* is false (the default), only the directory structure
+ and file data are guaranteed to be copied. Set *preserve_metadata* to true
+ to ensure that file and directory permissions, flags, last access and
+ modification times, and extended attributes are copied where supported.
+ This argument has no effect on Windows, where metadata is always preserved
+ when copying.
+
If the destination is an existing directory and *dirs_exist_ok* is false
(the default), a :exc:`FileExistsError` is raised. Otherwise, the copying
operation will continue if it encounters existing directories, and files
diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py
index 49e8e4ca13782c..c32e7762cefea3 100644
--- a/Lib/pathlib/_abc.py
+++ b/Lib/pathlib/_abc.py
@@ -835,7 +835,8 @@ def copy(self, target, *, follow_symlinks=True,
preserve_metadata=False):
if preserve_metadata:
self._copy_metadata(target)
- def copytree(self, target, *, follow_symlinks=True, dirs_exist_ok=False,
+ def copytree(self, target, *, follow_symlinks=True,
+ preserve_metadata=False, dirs_exist_ok=False,
ignore=None, on_error=None):
"""
Recursively copy this directory tree to the given destination.
@@ -851,6 +852,8 @@ def on_error(err):
try:
sources = source_dir.iterdir()
target_dir.mkdir(exist_ok=dirs_exist_ok)
+ if preserve_metadata:
+ source_dir._copy_metadata(target_dir)
for source in sources:
if ignore and ignore(source):
continue
@@ -859,7 +862,8 @@ def on_error(err):
stack.append((source,
target_dir.joinpath(source.name)))
else:
source.copy(target_dir.joinpath(source.name),
- follow_symlinks=follow_symlinks)
+ follow_symlinks=follow_symlinks,
+ preserve_metadata=preserve_metadata)
except OSError as err:
on_error(err)
except OSError as err:
diff --git a/Lib/test/test_pathlib/test_pathlib.py
b/Lib/test/test_pathlib/test_pathlib.py
index e17e7d71b6ab46..5293b5c84cda14 100644
--- a/Lib/test/test_pathlib/test_pathlib.py
+++ b/Lib/test/test_pathlib/test_pathlib.py
@@ -721,6 +721,36 @@ def test_copytree_no_read_permission(self):
self.assertIsInstance(errors[0], PermissionError)
self.assertFalse(target.exists())
+ def test_copytree_preserve_metadata(self):
+ base = self.cls(self.base)
+ source = base / 'dirC'
+ if hasattr(os, 'chmod'):
+ os.chmod(source / 'dirD', stat.S_IRWXU | stat.S_IRWXO)
+ if hasattr(os, 'chflags') and hasattr(stat, 'UF_NODUMP'):
+ os.chflags(source / 'fileC', stat.UF_NODUMP)
+ target = base / 'copyA'
+ source.copytree(target, preserve_metadata=True)
+
+ for subpath in ['.', 'fileC', 'dirD', 'dirD/fileD']:
+ source_st = source.joinpath(subpath).stat()
+ target_st = target.joinpath(subpath).stat()
+ self.assertLessEqual(source_st.st_atime, target_st.st_atime)
+ self.assertLessEqual(source_st.st_mtime, target_st.st_mtime)
+ self.assertEqual(source_st.st_mode, target_st.st_mode)
+ if hasattr(source_st, 'st_flags'):
+ self.assertEqual(source_st.st_flags, target_st.st_flags)
+
+ @os_helper.skip_unless_xattr
+ def test_copytree_preserve_metadata_xattrs(self):
+ base = self.cls(self.base)
+ source = base / 'dirC'
+ source_file = source.joinpath('dirD', 'fileD')
+ os.setxattr(source_file, b'user.foo', b'42')
+ target = base / 'copyA'
+ source.copytree(target, preserve_metadata=True)
+ target_file = target.joinpath('dirD', 'fileD')
+ self.assertEqual(os.getxattr(target_file, b'user.foo'), b'42')
+
def test_resolve_nonexist_relative_issue38671(self):
p = self.cls('non', 'exist')
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]