mirror of
https://github.com/python/cpython.git
synced 2025-08-30 13:38:43 +00:00
GH-73991: Support preserving metadata in pathlib.Path.copy()
(#120806)
Add *preserve_metadata* keyword-only argument to `pathlib.Path.copy()`, defaulting to false. When set to true, we copy timestamps, permissions, extended attributes and flags where available, like `shutil.copystat()`. The argument has no effect on Windows, where metadata is always copied. Internally (in the pathlib ABCs), path types gain `_readable_metadata` and `_writable_metadata` attributes. These sets of strings describe what kinds of metadata can be retrieved and stored. We take an intersection of `source._readable_metadata` and `target._writable_metadata` to minimise reads/writes. A new `_read_metadata()` method accepts a set of metadata keys and returns a dict with those keys, and a new `_write_metadata()` method accepts a dict of metadata. We *might* make these public in future, but it's hard to justify while the ABCs are still private.
This commit is contained in:
parent
6239d41527
commit
88fc0655d4
5 changed files with 187 additions and 11 deletions
|
@ -653,6 +653,50 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
|||
self.assertIsInstance(f, io.RawIOBase)
|
||||
self.assertEqual(f.read().strip(), b"this is file A")
|
||||
|
||||
def test_copy_file_preserve_metadata(self):
|
||||
base = self.cls(self.base)
|
||||
source = base / 'fileA'
|
||||
if hasattr(os, 'setxattr'):
|
||||
os.setxattr(source, b'user.foo', b'42')
|
||||
if hasattr(os, 'chmod'):
|
||||
os.chmod(source, stat.S_IRWXU | stat.S_IRWXO)
|
||||
if hasattr(os, 'chflags') and hasattr(stat, 'UF_NODUMP'):
|
||||
os.chflags(source, stat.UF_NODUMP)
|
||||
source_st = source.stat()
|
||||
target = base / 'copyA'
|
||||
source.copy(target, preserve_metadata=True)
|
||||
self.assertTrue(target.exists())
|
||||
self.assertEqual(source.read_text(), target.read_text())
|
||||
target_st = target.stat()
|
||||
self.assertLessEqual(source_st.st_atime, target_st.st_atime)
|
||||
self.assertLessEqual(source_st.st_mtime, target_st.st_mtime)
|
||||
if hasattr(os, 'getxattr'):
|
||||
self.assertEqual(os.getxattr(target, b'user.foo'), b'42')
|
||||
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)
|
||||
|
||||
@needs_symlinks
|
||||
def test_copy_link_preserve_metadata(self):
|
||||
base = self.cls(self.base)
|
||||
source = base / 'linkA'
|
||||
if hasattr(os, 'lchmod'):
|
||||
os.lchmod(source, stat.S_IRWXU | stat.S_IRWXO)
|
||||
if hasattr(os, 'lchflags') and hasattr(stat, 'UF_NODUMP'):
|
||||
os.lchflags(source, stat.UF_NODUMP)
|
||||
source_st = source.lstat()
|
||||
target = base / 'copyA'
|
||||
source.copy(target, follow_symlinks=False, preserve_metadata=True)
|
||||
self.assertTrue(target.exists())
|
||||
self.assertTrue(target.is_symlink())
|
||||
self.assertEqual(source.readlink(), target.readlink())
|
||||
target_st = target.lstat()
|
||||
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)
|
||||
|
||||
@unittest.skipIf(sys.platform == "win32" or sys.platform == "wasi", "directories are always readable on Windows and WASI")
|
||||
@unittest.skipIf(root_in_posix, "test fails with root privilege")
|
||||
def test_copytree_no_read_permission(self):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue