mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +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
|
@ -2,7 +2,7 @@
|
|||
Low-level OS functionality wrappers used by pathlib.
|
||||
"""
|
||||
|
||||
from errno import EBADF, EOPNOTSUPP, ETXTBSY, EXDEV
|
||||
from errno import *
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
|
@ -178,3 +178,100 @@ def copyfileobj(source_f, target_f):
|
|||
write_target = target_f.write
|
||||
while buf := read_source(1024 * 1024):
|
||||
write_target(buf)
|
||||
|
||||
|
||||
# Kinds of metadata supported by the operating system.
|
||||
file_metadata_keys = {'mode', 'times_ns'}
|
||||
if hasattr(os.stat_result, 'st_flags'):
|
||||
file_metadata_keys.add('flags')
|
||||
if hasattr(os, 'listxattr'):
|
||||
file_metadata_keys.add('xattrs')
|
||||
file_metadata_keys = frozenset(file_metadata_keys)
|
||||
|
||||
|
||||
def read_file_metadata(path, keys=None, *, follow_symlinks=True):
|
||||
"""
|
||||
Returns local path metadata as a dict with string keys.
|
||||
"""
|
||||
if keys is None:
|
||||
keys = file_metadata_keys
|
||||
assert keys.issubset(file_metadata_keys)
|
||||
result = {}
|
||||
for key in keys:
|
||||
if key == 'xattrs':
|
||||
try:
|
||||
result['xattrs'] = [
|
||||
(attr, os.getxattr(path, attr, follow_symlinks=follow_symlinks))
|
||||
for attr in os.listxattr(path, follow_symlinks=follow_symlinks)]
|
||||
except OSError as err:
|
||||
if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
|
||||
raise
|
||||
continue
|
||||
st = os.stat(path, follow_symlinks=follow_symlinks)
|
||||
if key == 'mode':
|
||||
result['mode'] = stat.S_IMODE(st.st_mode)
|
||||
elif key == 'times_ns':
|
||||
result['times_ns'] = st.st_atime_ns, st.st_mtime_ns
|
||||
elif key == 'flags':
|
||||
result['flags'] = st.st_flags
|
||||
return result
|
||||
|
||||
|
||||
def write_file_metadata(path, metadata, *, follow_symlinks=True):
|
||||
"""
|
||||
Sets local path metadata from the given dict with string keys.
|
||||
"""
|
||||
assert frozenset(metadata.keys()).issubset(file_metadata_keys)
|
||||
|
||||
def _nop(*args, ns=None, follow_symlinks=None):
|
||||
pass
|
||||
|
||||
if follow_symlinks:
|
||||
# use the real function if it exists
|
||||
def lookup(name):
|
||||
return getattr(os, name, _nop)
|
||||
else:
|
||||
# use the real function only if it exists
|
||||
# *and* it supports follow_symlinks
|
||||
def lookup(name):
|
||||
fn = getattr(os, name, _nop)
|
||||
if fn in os.supports_follow_symlinks:
|
||||
return fn
|
||||
return _nop
|
||||
|
||||
times_ns = metadata.get('times_ns')
|
||||
if times_ns is not None:
|
||||
lookup("utime")(path, ns=times_ns, follow_symlinks=follow_symlinks)
|
||||
# We must copy extended attributes before the file is (potentially)
|
||||
# chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
|
||||
xattrs = metadata.get('xattrs')
|
||||
if xattrs is not None:
|
||||
for attr, value in xattrs:
|
||||
try:
|
||||
os.setxattr(path, attr, value, follow_symlinks=follow_symlinks)
|
||||
except OSError as e:
|
||||
if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
|
||||
raise
|
||||
mode = metadata.get('mode')
|
||||
if mode is not None:
|
||||
try:
|
||||
lookup("chmod")(path, mode, follow_symlinks=follow_symlinks)
|
||||
except NotImplementedError:
|
||||
# if we got a NotImplementedError, it's because
|
||||
# * follow_symlinks=False,
|
||||
# * lchown() is unavailable, and
|
||||
# * either
|
||||
# * fchownat() is unavailable or
|
||||
# * fchownat() doesn't implement AT_SYMLINK_NOFOLLOW.
|
||||
# (it returned ENOSUP.)
|
||||
# therefore we're out of options--we simply cannot chown the
|
||||
# symlink. give up, suppress the error.
|
||||
# (which is what shutil always did in this circumstance.)
|
||||
pass
|
||||
flags = metadata.get('flags')
|
||||
if flags is not None:
|
||||
try:
|
||||
lookup("chflags")(path, flags, follow_symlinks=follow_symlinks)
|
||||
except OSError as why:
|
||||
if why.errno not in (EOPNOTSUPP, ENOTSUP):
|
||||
raise
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue