From 4dd7c21b8f5d0a8045da36a4402e926300140b09 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 9 Feb 2025 13:10:15 +0000 Subject: [PATCH 01/13] GH-125413: Add private metadata methods to `pathlib.Path.info` Add the following private methods to `pathlib.Path.info`: - `_get_mode()`: returns the POSIX file mode (`st_mode`), or zero if `os.stat()` fails. - `_get_times_ns()`: returns the access and modify times in nanoseconds (`st_atime_ns` and `st_mtime_ns`), or zeroes if `os.stat()` fails. - `_get_flags()`: returns the BSD file flags (`st_flags`), or zero if `os.stat()` fails. - `_get_xattrs()`: returns the file extended attributes as a list of key, value pairs, or an empty list if `listxattr()` or `getattr()` fail. These methods replace `LocalCopyReader.read_metadata()`, and so we can delete the `CopyReader` and `LocalCopyReader` classes. Rather than reading metadata via `source._copy_reader.read_metadata()`, we instead call `source.info._get_mode()`, `_get_times_ns()`, etc. Copying metadata is only supported for local-to-local copies at the moment. To support copying between arbitrary `ReadablePath` and `WritablePath` objects, we'd need to make the new methods public and documented. --- Lib/pathlib/_abc.py | 4 +- Lib/pathlib/_local.py | 3 +- Lib/pathlib/_os.py | 386 +++++++++++++++++++----------------------- 3 files changed, 177 insertions(+), 216 deletions(-) diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 800d1b4503d78d..68215f900f50f0 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -14,7 +14,7 @@ import functools import posixpath from glob import _PathGlobber, _no_recurse_symlinks -from pathlib._os import magic_open, CopyReader, CopyWriter +from pathlib._os import magic_open, CopyWriter @functools.cache @@ -354,8 +354,6 @@ def readlink(self): """ raise NotImplementedError - _copy_reader = property(CopyReader) - def copy(self, target, follow_symlinks=True, dirs_exist_ok=False, preserve_metadata=False): """ diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 956c1920bf6d78..742358af32aab4 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -19,7 +19,7 @@ except ImportError: grp = None -from pathlib._os import LocalCopyReader, LocalCopyWriter, PathInfo, DirEntryInfo +from pathlib._os import LocalCopyWriter, PathInfo, DirEntryInfo from pathlib._abc import JoinablePath, ReadablePath, WritablePath @@ -1055,7 +1055,6 @@ def replace(self, target): os.replace(self, target) return self.with_segments(target) - _copy_reader = property(LocalCopyReader) _copy_writer = property(LocalCopyWriter) def move(self, target): diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index c0c81ada858cdf..263ddcb3892b49 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -200,26 +200,6 @@ def magic_open(path, mode='r', buffering=-1, encoding=None, errors=None, raise TypeError(f"{cls.__name__} can't be opened with mode {mode!r}") -class CopyReader: - """ - Class that implements the "read" part of copying between path objects. - An instance of this class is available from the ReadablePath._copy_reader - property. - """ - __slots__ = ('_path',) - - def __init__(self, path): - self._path = path - - _readable_metakeys = frozenset() - - def _read_metadata(self, metakeys, *, follow_symlinks=True): - """ - Returns path metadata as a dict with string keys. - """ - raise NotImplementedError - - class CopyWriter: """ Class that implements the "write" part of copying between path objects. An @@ -231,46 +211,33 @@ class CopyWriter: def __init__(self, path): self._path = path - _writable_metakeys = frozenset() - - def _write_metadata(self, metadata, *, follow_symlinks=True): - """ - Sets path metadata from the given dict with string keys. - """ - raise NotImplementedError - def _create(self, source, follow_symlinks, dirs_exist_ok, preserve_metadata): self._ensure_distinct_path(source) - if preserve_metadata: - metakeys = self._writable_metakeys & source._copy_reader._readable_metakeys - else: - metakeys = None if not follow_symlinks and source.is_symlink(): - self._create_symlink(source, metakeys) + self._create_symlink(source, preserve_metadata) elif source.is_dir(): - self._create_dir(source, metakeys, follow_symlinks, dirs_exist_ok) + self._create_dir(source, follow_symlinks, dirs_exist_ok, preserve_metadata) else: - self._create_file(source, metakeys) + self._create_file(source, preserve_metadata) return self._path - def _create_dir(self, source, metakeys, follow_symlinks, dirs_exist_ok): + def _create_dir(self, source, follow_symlinks, dirs_exist_ok, preserve_metadata): """Copy the given directory to our path.""" children = list(source.iterdir()) self._path.mkdir(exist_ok=dirs_exist_ok) for src in children: dst = self._path.joinpath(src.name) if not follow_symlinks and src.is_symlink(): - dst._copy_writer._create_symlink(src, metakeys) + dst._copy_writer._create_symlink(src, preserve_metadata) elif src.is_dir(): - dst._copy_writer._create_dir(src, metakeys, follow_symlinks, dirs_exist_ok) + dst._copy_writer._create_dir(src, follow_symlinks, dirs_exist_ok, preserve_metadata) else: - dst._copy_writer._create_file(src, metakeys) - if metakeys: - metadata = source._copy_reader._read_metadata(metakeys) - if metadata: - self._write_metadata(metadata) + dst._copy_writer._create_file(src, preserve_metadata) + + if preserve_metadata: + self._create_metadata(source) - def _create_file(self, source, metakeys): + def _create_file(self, source, preserve_metadata): """Copy the given file to our path.""" self._ensure_different_file(source) with magic_open(source, 'rb') as source_f: @@ -283,18 +250,22 @@ def _create_file(self, source, metakeys): raise FileNotFoundError( f'Directory does not exist: {self._path}') from e raise - if metakeys: - metadata = source._copy_reader._read_metadata(metakeys) - if metadata: - self._write_metadata(metadata) + if preserve_metadata: + self._create_metadata(source) - def _create_symlink(self, source, metakeys): + def _create_symlink(self, source, preserve_metadata): """Copy the given symbolic link to our path.""" self._path.symlink_to(source.readlink()) - if metakeys: - metadata = source._copy_reader._read_metadata(metakeys, follow_symlinks=False) - if metadata: - self._write_metadata(metadata, follow_symlinks=False) + if preserve_metadata: + self._create_symlink_metadata(source) + + def _create_metadata(self, source): + """Copy metadata from the given path to our path.""" + pass + + def _create_symlink_metadata(self, source): + """Copy metadata from the given symlink to our symlink.""" + pass def _ensure_different_file(self, source): """ @@ -322,123 +293,105 @@ def _ensure_distinct_path(self, source): raise err -class LocalCopyReader(CopyReader): - """This object implements the "read" part of copying local paths. Don't - try to construct it yourself. - """ - __slots__ = () - - _readable_metakeys = {'mode', 'times_ns'} - if hasattr(os.stat_result, 'st_flags'): - _readable_metakeys.add('flags') - if hasattr(os, 'listxattr'): - _readable_metakeys.add('xattrs') - _readable_metakeys = frozenset(_readable_metakeys) - - def _read_metadata(self, metakeys, *, follow_symlinks=True): - metadata = {} - if 'mode' in metakeys or 'times_ns' in metakeys or 'flags' in metakeys: - st = self._path.stat(follow_symlinks=follow_symlinks) - if 'mode' in metakeys: - metadata['mode'] = S_IMODE(st.st_mode) - if 'times_ns' in metakeys: - metadata['times_ns'] = st.st_atime_ns, st.st_mtime_ns - if 'flags' in metakeys: - metadata['flags'] = st.st_flags - if 'xattrs' in metakeys: - try: - metadata['xattrs'] = [ - (attr, os.getxattr(self._path, attr, follow_symlinks=follow_symlinks)) - for attr in os.listxattr(self._path, follow_symlinks=follow_symlinks)] - except OSError as err: - if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): - raise - return metadata - - class LocalCopyWriter(CopyWriter): """This object implements the "write" part of copying local paths. Don't try to construct it yourself. """ __slots__ = () - _writable_metakeys = LocalCopyReader._readable_metakeys - - def _write_metadata(self, metadata, *, follow_symlinks=True): - 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")(self._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(self._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")(self._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")(self._path, flags, follow_symlinks=follow_symlinks) - except OSError as why: - if why.errno not in (EOPNOTSUPP, ENOTSUP): - raise - if copyfile: # Use fast OS routine for local file copying where available. - def _create_file(self, source, metakeys): + def _create_file(self, source, preserve_metadata): """Copy the given file to the given target.""" try: source = os.fspath(source) except TypeError: - super()._create_file(source, metakeys) + super()._create_file(source, preserve_metadata) else: copyfile(source, os.fspath(self._path)) if os.name == 'nt': # Windows: symlink target might not exist yet if we're copying several # files, so ensure we pass is_dir to os.symlink(). - def _create_symlink(self, source, metakeys): + def _create_symlink(self, source, preserve_metadata): """Copy the given symlink to the given target.""" self._path.symlink_to(source.readlink(), source.is_dir()) - if metakeys: - metadata = source._copy_reader._read_metadata(metakeys, follow_symlinks=False) - if metadata: - self._write_metadata(metadata, follow_symlinks=False) + if preserve_metadata: + self._create_symlink_metadata(source) + + def _create_metadata(self, source): + """Copy metadata from the given path to our path.""" + target = self._path + info = source.info + copy_times_ns = hasattr(info, '_get_times') + copy_xattrs = hasattr(info, '_get_xattrs') and hasattr(os, 'setxattr') + copy_mode = hasattr(info, '_get_mode') + copy_flags = hasattr(info, '_get_flags') and hasattr(os, 'chflags') + + if copy_times_ns: + atime_ns, mtime_ns = info._get_times_ns() + if atime_ns and mtime_ns: + os.utime(target, ns=(atime_ns, mtime_ns)) + if copy_xattrs: + xattrs = info._get_xattrs() + for attr, value in xattrs: + try: + os.setxattr(target, attr, value) + except OSError as e: + if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): + raise + if copy_mode: + mode = info._get_mode() + if mode: + os.chmod(target, S_IMODE(mode)) + if copy_flags: + flags = info._get_flags() + if flags: + try: + os.chflags(target, flags) + except OSError as why: + if why.errno not in (EOPNOTSUPP, ENOTSUP): + raise + + def _create_symlink_metadata(self, source): + """Copy metadata from the given symlink to our symlink.""" + target = self._path + info = source.info + copy_times_ns = (hasattr(info, '_get_times') and + os.utime in os.supports_follow_symlinks) + copy_xattrs = (hasattr(info, '_get_xattrs') and hasattr(os, 'setxattr') and + os.setxattr in os.supports_fd) + copy_mode = hasattr(info, '_get_mode') and hasattr(os, 'lchmod') + copy_flags = (hasattr(info, '_get_flags') and hasattr(os, 'chflags') and + os.chflags in os.supports_follow_symlinks) + + if copy_times_ns: + atime_ns, mtime_ns = info._get_times_ns(follow_symlinks=False) + if atime_ns and mtime_ns: + os.utime(target, ns=(atime_ns, mtime_ns), follow_symlinks=False) + if copy_xattrs: + xattrs = info._get_xattrs(follow_symlinks=False) + for attr, value in xattrs: + try: + os.setxattr(target, attr, value, follow_symlinks=False) + except OSError as e: + if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): + raise + if copy_mode: + mode = info._get_mode(follow_symlinks=False) + if mode: + try: + os.lchmod(target, S_IMODE(mode)) + except NotImplementedError: + pass + if copy_flags: + flags = info._get_flags(follow_symlinks=False) + if flags: + try: + os.chflags(target, flags, follow_symlinks=False) + except OSError as why: + if why.errno not in (EOPNOTSUPP, ENOTSUP): + raise def _ensure_different_file(self, source): """ @@ -456,20 +409,80 @@ def _ensure_different_file(self, source): class _PathInfoBase: - __slots__ = () + __slots__ = ('_path', '_stat_result', '_lstat_result') + + def __init__(self, path): + self._path = str(path) def __repr__(self): path_type = "WindowsPath" if os.name == "nt" else "PosixPath" return f"<{path_type}.info>" + def _raw_stat(self, *, follow_symlinks=True): + return os.stat(self._path, follow_symlinks=follow_symlinks) + + def _stat(self, *, follow_symlinks=True): + """Return the status as an os.stat_result, or None if stat() fails.""" + if follow_symlinks: + try: + return self._stat_result + except AttributeError: + try: + self._stat_result = self._raw_stat(follow_symlinks=True) + except (OSError, ValueError): + self._stat_result = None + return self._stat_result + else: + try: + return self._lstat_result + except AttributeError: + try: + self._lstat_result = self._raw_stat(follow_symlinks=False) + except (OSError, ValueError): + self._lstat_result = None + return self._lstat_result + + def _get_mode(self, *, follow_symlinks=True): + """Return the POSIX file mode, or zero if stat() fails.""" + st = self._stat(follow_symlinks=follow_symlinks) + if st is None: + return 0 + return st.st_mode + + def _get_times_ns(self, *, follow_symlinks=True): + """Return the access and modify times in nanoseconds. If stat() fails, + both values are set to zero.""" + st = self._stat(follow_symlinks=follow_symlinks) + if st is None: + return 0, 0 + return st.st_atime_ns, st.st_mtime_ns + + if hasattr(os.stat_result, 'st_flags'): + def _get_flags(self, *, follow_symlinks=True): + """Return the flags, or zero if stat() fails.""" + st = self._stat(follow_symlinks=follow_symlinks) + if st is None: + return 0 + return st.st_flags + + if hasattr(os, 'listxattr'): + def _get_xattrs(self, *, follow_symlinks=True): + """Return the xattrs as a list of (attr, value) pairs, or an empty + list if extended attributes aren't supported.""" + try: + return [ + (attr, os.getxattr(self._path, attr, follow_symlinks=follow_symlinks)) + for attr in os.listxattr(self._path, follow_symlinks=follow_symlinks)] + except OSError as err: + if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): + raise + return [] + class _WindowsPathInfo(_PathInfoBase): """Implementation of pathlib.types.PathInfo that provides status information for Windows paths. Don't try to construct it yourself.""" - __slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink') - - def __init__(self, path): - self._path = str(path) + __slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink') def exists(self, *, follow_symlinks=True): """Whether this path exists.""" @@ -523,30 +536,7 @@ def is_symlink(self): class _PosixPathInfo(_PathInfoBase): - """Implementation of pathlib.types.PathInfo that provides status - information for POSIX paths. Don't try to construct it yourself.""" - __slots__ = ('_path', '_mode') - - def __init__(self, path): - self._path = str(path) - self._mode = [None, None] - - def _get_mode(self, *, follow_symlinks=True): - idx = bool(follow_symlinks) - mode = self._mode[idx] - if mode is None: - try: - st = os.stat(self._path, follow_symlinks=follow_symlinks) - except (OSError, ValueError): - mode = 0 - else: - mode = st.st_mode - if follow_symlinks or S_ISLNK(mode): - self._mode[idx] = mode - else: - # Not a symlink, so stat() will give the same result - self._mode = [mode, mode] - return mode + __slots__ = () def exists(self, *, follow_symlinks=True): """Whether this path exists.""" @@ -568,47 +558,21 @@ def is_symlink(self): PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo -class DirEntryInfo(_PathInfoBase): +class DirEntryInfo(_PosixPathInfo): """Implementation of pathlib.types.PathInfo that provides status information by querying a wrapped os.DirEntry object. Don't try to construct it yourself.""" __slots__ = ('_entry', '_exists') def __init__(self, entry): + super().__init__(entry.path) self._entry = entry + def _raw_stat(self, *, follow_symlinks=True): + return self._entry.stat(follow_symlinks=follow_symlinks) + def exists(self, *, follow_symlinks=True): """Whether this path exists.""" if not follow_symlinks: return True - try: - return self._exists - except AttributeError: - try: - self._entry.stat() - except OSError: - self._exists = False - else: - self._exists = True - return self._exists - - def is_dir(self, *, follow_symlinks=True): - """Whether this path is a directory.""" - try: - return self._entry.is_dir(follow_symlinks=follow_symlinks) - except OSError: - return False - - def is_file(self, *, follow_symlinks=True): - """Whether this path is a regular file.""" - try: - return self._entry.is_file(follow_symlinks=follow_symlinks) - except OSError: - return False - - def is_symlink(self): - """Whether this path is a symbolic link.""" - try: - return self._entry.is_symlink() - except OSError: - return False + return super().exists() From 30faab7ba5f087edda4aa70ecb744c6dbf0ebe45 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 9 Feb 2025 13:49:51 +0000 Subject: [PATCH 02/13] Fix atime/mtime copying --- Lib/pathlib/_os.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 263ddcb3892b49..e5537ecf3bc70f 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -323,13 +323,14 @@ def _create_metadata(self, source): """Copy metadata from the given path to our path.""" target = self._path info = source.info - copy_times_ns = hasattr(info, '_get_times') + copy_times_ns = hasattr(info, '_get_atime_ns') and hasattr(info, '_get_mtime_ns') copy_xattrs = hasattr(info, '_get_xattrs') and hasattr(os, 'setxattr') copy_mode = hasattr(info, '_get_mode') copy_flags = hasattr(info, '_get_flags') and hasattr(os, 'chflags') if copy_times_ns: - atime_ns, mtime_ns = info._get_times_ns() + atime_ns = info._get_atime_ns() + mtime_ns = info._get_mtime_ns() if atime_ns and mtime_ns: os.utime(target, ns=(atime_ns, mtime_ns)) if copy_xattrs: @@ -357,16 +358,21 @@ def _create_symlink_metadata(self, source): """Copy metadata from the given symlink to our symlink.""" target = self._path info = source.info - copy_times_ns = (hasattr(info, '_get_times') and + copy_times_ns = (hasattr(info, '_get_atime_ns') and + hasattr(info, '_get_mtime_ns') and os.utime in os.supports_follow_symlinks) - copy_xattrs = (hasattr(info, '_get_xattrs') and hasattr(os, 'setxattr') and + copy_xattrs = (hasattr(info, '_get_xattrs') and + hasattr(os, 'setxattr') and os.setxattr in os.supports_fd) - copy_mode = hasattr(info, '_get_mode') and hasattr(os, 'lchmod') - copy_flags = (hasattr(info, '_get_flags') and hasattr(os, 'chflags') and + copy_mode = (hasattr(info, '_get_mode') and + hasattr(os, 'lchmod')) + copy_flags = (hasattr(info, '_get_flags') and + hasattr(os, 'chflags') and os.chflags in os.supports_follow_symlinks) if copy_times_ns: - atime_ns, mtime_ns = info._get_times_ns(follow_symlinks=False) + atime_ns = info._get_atime_ns(follow_symlinks=False) + mtime_ns = info._get_mtime_ns(follow_symlinks=False) if atime_ns and mtime_ns: os.utime(target, ns=(atime_ns, mtime_ns), follow_symlinks=False) if copy_xattrs: @@ -449,13 +455,19 @@ def _get_mode(self, *, follow_symlinks=True): return 0 return st.st_mode - def _get_times_ns(self, *, follow_symlinks=True): - """Return the access and modify times in nanoseconds. If stat() fails, - both values are set to zero.""" + def _get_atime_ns(self, *, follow_symlinks=True): + """Return the access time in nanoseconds, or zero if stat() fails.""" st = self._stat(follow_symlinks=follow_symlinks) if st is None: - return 0, 0 - return st.st_atime_ns, st.st_mtime_ns + return 0 + return st.st_atime_ns + + def _get_mtime_ns(self, *, follow_symlinks=True): + """Return the modify time in nanoseconds, or zero if stat() fails.""" + st = self._stat(follow_symlinks=follow_symlinks) + if st is None: + return 0 + return st.st_mtime_ns if hasattr(os.stat_result, 'st_flags'): def _get_flags(self, *, follow_symlinks=True): From 22e065d6ef376b99e8253e2f00ec305178ac5510 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 9 Feb 2025 14:10:20 +0000 Subject: [PATCH 03/13] Simplify patch --- Lib/pathlib/_os.py | 77 +++++++++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index e5537ecf3bc70f..a3a99305d07bad 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -414,7 +414,7 @@ def _ensure_different_file(self, source): raise err -class _PathInfoBase: +class _PosixPathInfo: __slots__ = ('_path', '_stat_result', '_lstat_result') def __init__(self, path): @@ -448,6 +448,34 @@ def _stat(self, *, follow_symlinks=True): self._lstat_result = None return self._lstat_result + def exists(self, *, follow_symlinks=True): + """Whether this path exists.""" + st = self._stat(follow_symlinks=follow_symlinks) + if st is None: + return False + return True + + def is_dir(self, *, follow_symlinks=True): + """Whether this path is a directory.""" + st = self._stat(follow_symlinks=follow_symlinks) + if st is None: + return False + return S_ISDIR(st.st_mode) + + def is_file(self, *, follow_symlinks=True): + """Whether this path is a regular file.""" + st = self._stat(follow_symlinks=follow_symlinks) + if st is None: + return False + return S_ISREG(st.st_mode) + + def is_symlink(self): + """Whether this path is a symbolic link.""" + st = self._stat(follow_symlinks=False) + if st is None: + return False + return S_ISLNK(st.st_mode) + def _get_mode(self, *, follow_symlinks=True): """Return the POSIX file mode, or zero if stat() fails.""" st = self._stat(follow_symlinks=follow_symlinks) @@ -491,7 +519,7 @@ def _get_xattrs(self, *, follow_symlinks=True): return [] -class _WindowsPathInfo(_PathInfoBase): +class _WindowsPathInfo(_PosixPathInfo): """Implementation of pathlib.types.PathInfo that provides status information for Windows paths. Don't try to construct it yourself.""" __slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink') @@ -547,26 +575,6 @@ def is_symlink(self): return self._is_symlink -class _PosixPathInfo(_PathInfoBase): - __slots__ = () - - def exists(self, *, follow_symlinks=True): - """Whether this path exists.""" - return self._get_mode(follow_symlinks=follow_symlinks) > 0 - - def is_dir(self, *, follow_symlinks=True): - """Whether this path is a directory.""" - return S_ISDIR(self._get_mode(follow_symlinks=follow_symlinks)) - - def is_file(self, *, follow_symlinks=True): - """Whether this path is a regular file.""" - return S_ISREG(self._get_mode(follow_symlinks=follow_symlinks)) - - def is_symlink(self): - """Whether this path is a symbolic link.""" - return S_ISLNK(self._get_mode(follow_symlinks=False)) - - PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo @@ -574,7 +582,7 @@ class DirEntryInfo(_PosixPathInfo): """Implementation of pathlib.types.PathInfo that provides status information by querying a wrapped os.DirEntry object. Don't try to construct it yourself.""" - __slots__ = ('_entry', '_exists') + __slots__ = ('_entry',) def __init__(self, entry): super().__init__(entry.path) @@ -587,4 +595,25 @@ def exists(self, *, follow_symlinks=True): """Whether this path exists.""" if not follow_symlinks: return True - return super().exists() + return self._stat() is not None + + def is_dir(self, *, follow_symlinks=True): + """Whether this path is a directory.""" + try: + return self._entry.is_dir(follow_symlinks=follow_symlinks) + except OSError: + return False + + def is_file(self, *, follow_symlinks=True): + """Whether this path is a regular file.""" + try: + return self._entry.is_file(follow_symlinks=follow_symlinks) + except OSError: + return False + + def is_symlink(self): + """Whether this path is a symbolic link.""" + try: + return self._entry.is_symlink() + except OSError: + return False From cd4a1b51702b2cb26d08e3772caa8725c3f6471a Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 9 Feb 2025 14:41:16 +0000 Subject: [PATCH 04/13] Improve naming --- Lib/pathlib/_os.py | 101 ++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index a3a99305d07bad..6f8a84d08b5b73 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -323,33 +323,35 @@ def _create_metadata(self, source): """Copy metadata from the given path to our path.""" target = self._path info = source.info - copy_times_ns = hasattr(info, '_get_atime_ns') and hasattr(info, '_get_mtime_ns') - copy_xattrs = hasattr(info, '_get_xattrs') and hasattr(os, 'setxattr') - copy_mode = hasattr(info, '_get_mode') - copy_flags = hasattr(info, '_get_flags') and hasattr(os, 'chflags') - + copy_times_ns = hasattr(info, '_access_time_ns') and hasattr(info, '_mod_time_ns') if copy_times_ns: - atime_ns = info._get_atime_ns() - mtime_ns = info._get_mtime_ns() - if atime_ns and mtime_ns: - os.utime(target, ns=(atime_ns, mtime_ns)) + access_time_ns = info._access_time_ns() + mod_time_ns = info._mod_time_ns() + if access_time_ns and mod_time_ns: + os.utime(target, ns=(access_time_ns, mod_time_ns)) + + copy_xattrs = hasattr(info, '_xattrs') and hasattr(os, 'setxattr') if copy_xattrs: - xattrs = info._get_xattrs() + xattrs = info._xattrs() for attr, value in xattrs: try: os.setxattr(target, attr, value) except OSError as e: if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): raise - if copy_mode: - mode = info._get_mode() - if mode: - os.chmod(target, S_IMODE(mode)) - if copy_flags: - flags = info._get_flags() - if flags: + + copy_posix_permissions = hasattr(info, '_posix_permissions') + if copy_posix_permissions: + posix_permissions = info._posix_permissions() + if posix_permissions: + os.chmod(target, posix_permissions) + + copy_bsd_flags = hasattr(info, '_bsd_flags') and hasattr(os, 'chflags') + if copy_bsd_flags: + bsd_flags = info._bsd_flags() + if bsd_flags: try: - os.chflags(target, flags) + os.chflags(target, bsd_flags) except OSError as why: if why.errno not in (EOPNOTSUPP, ENOTSUP): raise @@ -358,43 +360,46 @@ def _create_symlink_metadata(self, source): """Copy metadata from the given symlink to our symlink.""" target = self._path info = source.info - copy_times_ns = (hasattr(info, '_get_atime_ns') and - hasattr(info, '_get_mtime_ns') and + + copy_times_ns = (hasattr(info, '_access_time_ns') and + hasattr(info, '_mod_time_ns') and os.utime in os.supports_follow_symlinks) - copy_xattrs = (hasattr(info, '_get_xattrs') and + if copy_times_ns: + access_time_ns = info._access_time_ns(follow_symlinks=False) + mod_time_ns = info._mod_time_ns(follow_symlinks=False) + if access_time_ns and mod_time_ns: + os.utime(target, ns=(access_time_ns, mod_time_ns), follow_symlinks=False) + + copy_xattrs = (hasattr(info, '_xattrs') and hasattr(os, 'setxattr') and os.setxattr in os.supports_fd) - copy_mode = (hasattr(info, '_get_mode') and - hasattr(os, 'lchmod')) - copy_flags = (hasattr(info, '_get_flags') and - hasattr(os, 'chflags') and - os.chflags in os.supports_follow_symlinks) - - if copy_times_ns: - atime_ns = info._get_atime_ns(follow_symlinks=False) - mtime_ns = info._get_mtime_ns(follow_symlinks=False) - if atime_ns and mtime_ns: - os.utime(target, ns=(atime_ns, mtime_ns), follow_symlinks=False) if copy_xattrs: - xattrs = info._get_xattrs(follow_symlinks=False) + xattrs = info._xattrs(follow_symlinks=False) for attr, value in xattrs: try: os.setxattr(target, attr, value, follow_symlinks=False) except OSError as e: if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): raise - if copy_mode: - mode = info._get_mode(follow_symlinks=False) - if mode: + + copy_posix_permissions = (hasattr(info, '_posix_permissions') and + hasattr(os, 'lchmod')) + if copy_posix_permissions: + posix_permissions = info._posix_permissions(follow_symlinks=False) + if posix_permissions: try: - os.lchmod(target, S_IMODE(mode)) + os.lchmod(target, posix_permissions) except NotImplementedError: pass - if copy_flags: - flags = info._get_flags(follow_symlinks=False) - if flags: + + copy_bsd_flags = (hasattr(info, '_bsd_flags') and + hasattr(os, 'chflags') and + os.chflags in os.supports_follow_symlinks) + if copy_bsd_flags: + bsd_flags = info._bsd_flags(follow_symlinks=False) + if bsd_flags: try: - os.chflags(target, flags, follow_symlinks=False) + os.chflags(target, bsd_flags, follow_symlinks=False) except OSError as why: if why.errno not in (EOPNOTSUPP, ENOTSUP): raise @@ -476,21 +481,21 @@ def is_symlink(self): return False return S_ISLNK(st.st_mode) - def _get_mode(self, *, follow_symlinks=True): - """Return the POSIX file mode, or zero if stat() fails.""" + def _posix_permissions(self, *, follow_symlinks=True): + """Return the POSIX file permissions, or zero if stat() fails.""" st = self._stat(follow_symlinks=follow_symlinks) if st is None: return 0 - return st.st_mode + return S_IMODE(st.st_mode) - def _get_atime_ns(self, *, follow_symlinks=True): + def _access_time_ns(self, *, follow_symlinks=True): """Return the access time in nanoseconds, or zero if stat() fails.""" st = self._stat(follow_symlinks=follow_symlinks) if st is None: return 0 return st.st_atime_ns - def _get_mtime_ns(self, *, follow_symlinks=True): + def _mod_time_ns(self, *, follow_symlinks=True): """Return the modify time in nanoseconds, or zero if stat() fails.""" st = self._stat(follow_symlinks=follow_symlinks) if st is None: @@ -498,7 +503,7 @@ def _get_mtime_ns(self, *, follow_symlinks=True): return st.st_mtime_ns if hasattr(os.stat_result, 'st_flags'): - def _get_flags(self, *, follow_symlinks=True): + def _bsd_flags(self, *, follow_symlinks=True): """Return the flags, or zero if stat() fails.""" st = self._stat(follow_symlinks=follow_symlinks) if st is None: @@ -506,7 +511,7 @@ def _get_flags(self, *, follow_symlinks=True): return st.st_flags if hasattr(os, 'listxattr'): - def _get_xattrs(self, *, follow_symlinks=True): + def _xattrs(self, *, follow_symlinks=True): """Return the xattrs as a list of (attr, value) pairs, or an empty list if extended attributes aren't supported.""" try: From 07f89a4b7132726c5fea394abdfc4ee86da3b892 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 9 Feb 2025 17:34:37 +0000 Subject: [PATCH 05/13] Return `None` if results aren't available. (not 100% sure about this) --- Lib/pathlib/_os.py | 60 ++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 6f8a84d08b5b73..b60c686cbd1cb9 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -327,29 +327,30 @@ def _create_metadata(self, source): if copy_times_ns: access_time_ns = info._access_time_ns() mod_time_ns = info._mod_time_ns() - if access_time_ns and mod_time_ns: + if access_time_ns is not None and mod_time_ns is not None: os.utime(target, ns=(access_time_ns, mod_time_ns)) copy_xattrs = hasattr(info, '_xattrs') and hasattr(os, 'setxattr') if copy_xattrs: xattrs = info._xattrs() - for attr, value in xattrs: - try: - os.setxattr(target, attr, value) - except OSError as e: - if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): - raise + if xattrs is not None: + for attr, value in xattrs: + try: + os.setxattr(target, attr, value) + except OSError as e: + if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): + raise copy_posix_permissions = hasattr(info, '_posix_permissions') if copy_posix_permissions: posix_permissions = info._posix_permissions() - if posix_permissions: + if posix_permissions is not None: os.chmod(target, posix_permissions) copy_bsd_flags = hasattr(info, '_bsd_flags') and hasattr(os, 'chflags') if copy_bsd_flags: bsd_flags = info._bsd_flags() - if bsd_flags: + if bsd_flags is not None: try: os.chflags(target, bsd_flags) except OSError as why: @@ -367,7 +368,7 @@ def _create_symlink_metadata(self, source): if copy_times_ns: access_time_ns = info._access_time_ns(follow_symlinks=False) mod_time_ns = info._mod_time_ns(follow_symlinks=False) - if access_time_ns and mod_time_ns: + if access_time_ns is not None and mod_time_ns is not None: os.utime(target, ns=(access_time_ns, mod_time_ns), follow_symlinks=False) copy_xattrs = (hasattr(info, '_xattrs') and @@ -375,18 +376,19 @@ def _create_symlink_metadata(self, source): os.setxattr in os.supports_fd) if copy_xattrs: xattrs = info._xattrs(follow_symlinks=False) - for attr, value in xattrs: - try: - os.setxattr(target, attr, value, follow_symlinks=False) - except OSError as e: - if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): - raise + if xattrs is not None: + for attr, value in xattrs: + try: + os.setxattr(target, attr, value, follow_symlinks=False) + except OSError as e: + if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): + raise copy_posix_permissions = (hasattr(info, '_posix_permissions') and hasattr(os, 'lchmod')) if copy_posix_permissions: posix_permissions = info._posix_permissions(follow_symlinks=False) - if posix_permissions: + if posix_permissions is not None: try: os.lchmod(target, posix_permissions) except NotImplementedError: @@ -397,7 +399,7 @@ def _create_symlink_metadata(self, source): os.chflags in os.supports_follow_symlinks) if copy_bsd_flags: bsd_flags = info._bsd_flags(follow_symlinks=False) - if bsd_flags: + if bsd_flags is not None: try: os.chflags(target, bsd_flags, follow_symlinks=False) except OSError as why: @@ -482,38 +484,38 @@ def is_symlink(self): return S_ISLNK(st.st_mode) def _posix_permissions(self, *, follow_symlinks=True): - """Return the POSIX file permissions, or zero if stat() fails.""" + """Return the POSIX file permissions, or None if stat() fails.""" st = self._stat(follow_symlinks=follow_symlinks) if st is None: - return 0 + return None return S_IMODE(st.st_mode) def _access_time_ns(self, *, follow_symlinks=True): - """Return the access time in nanoseconds, or zero if stat() fails.""" + """Return the access time in nanoseconds, or None if stat() fails.""" st = self._stat(follow_symlinks=follow_symlinks) if st is None: - return 0 + return None return st.st_atime_ns def _mod_time_ns(self, *, follow_symlinks=True): - """Return the modify time in nanoseconds, or zero if stat() fails.""" + """Return the modify time in nanoseconds, or None if stat() fails.""" st = self._stat(follow_symlinks=follow_symlinks) if st is None: - return 0 + return None return st.st_mtime_ns if hasattr(os.stat_result, 'st_flags'): def _bsd_flags(self, *, follow_symlinks=True): - """Return the flags, or zero if stat() fails.""" + """Return the flags, or None if stat() fails.""" st = self._stat(follow_symlinks=follow_symlinks) if st is None: - return 0 + return None return st.st_flags if hasattr(os, 'listxattr'): def _xattrs(self, *, follow_symlinks=True): - """Return the xattrs as a list of (attr, value) pairs, or an empty - list if extended attributes aren't supported.""" + """Return the xattrs as a list of (attr, value) pairs, or None if + extended attributes aren't supported.""" try: return [ (attr, os.getxattr(self._path, attr, follow_symlinks=follow_symlinks)) @@ -521,7 +523,7 @@ def _xattrs(self, *, follow_symlinks=True): except OSError as err: if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): raise - return [] + return None class _WindowsPathInfo(_PosixPathInfo): From 202688681c46dcc60ac824a65b1bf0fddef4f1d8 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 9 Feb 2025 17:44:15 +0000 Subject: [PATCH 06/13] Merge `_create_metadata()` and `_create_symlink_metadata()` --- Lib/pathlib/_os.py | 98 ++++++++++++++-------------------------------- 1 file changed, 30 insertions(+), 68 deletions(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index b60c686cbd1cb9..26df6654a1151c 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -257,16 +257,12 @@ def _create_symlink(self, source, preserve_metadata): """Copy the given symbolic link to our path.""" self._path.symlink_to(source.readlink()) if preserve_metadata: - self._create_symlink_metadata(source) + self._create_metadata(source, follow_symlinks=False) - def _create_metadata(self, source): + def _create_metadata(self, source, follow_symlinks=True): """Copy metadata from the given path to our path.""" pass - def _create_symlink_metadata(self, source): - """Copy metadata from the given symlink to our symlink.""" - pass - def _ensure_different_file(self, source): """ Raise OSError(EINVAL) if both paths refer to the same file. @@ -317,91 +313,57 @@ def _create_symlink(self, source, preserve_metadata): """Copy the given symlink to the given target.""" self._path.symlink_to(source.readlink(), source.is_dir()) if preserve_metadata: - self._create_symlink_metadata(source) + self._create_metadata(source, follow_symlinks=False) - def _create_metadata(self, source): + def _create_metadata(self, source, follow_symlinks=True): """Copy metadata from the given path to our path.""" target = self._path info = source.info - copy_times_ns = hasattr(info, '_access_time_ns') and hasattr(info, '_mod_time_ns') - if copy_times_ns: - access_time_ns = info._access_time_ns() - mod_time_ns = info._mod_time_ns() - if access_time_ns is not None and mod_time_ns is not None: - os.utime(target, ns=(access_time_ns, mod_time_ns)) - - copy_xattrs = hasattr(info, '_xattrs') and hasattr(os, 'setxattr') - if copy_xattrs: - xattrs = info._xattrs() - if xattrs is not None: - for attr, value in xattrs: - try: - os.setxattr(target, attr, value) - except OSError as e: - if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): - raise - - copy_posix_permissions = hasattr(info, '_posix_permissions') - if copy_posix_permissions: - posix_permissions = info._posix_permissions() - if posix_permissions is not None: - os.chmod(target, posix_permissions) - - copy_bsd_flags = hasattr(info, '_bsd_flags') and hasattr(os, 'chflags') - if copy_bsd_flags: - bsd_flags = info._bsd_flags() - if bsd_flags is not None: - try: - os.chflags(target, bsd_flags) - except OSError as why: - if why.errno not in (EOPNOTSUPP, ENOTSUP): - raise - - def _create_symlink_metadata(self, source): - """Copy metadata from the given symlink to our symlink.""" - target = self._path - info = source.info - copy_times_ns = (hasattr(info, '_access_time_ns') and - hasattr(info, '_mod_time_ns') and - os.utime in os.supports_follow_symlinks) + copy_times_ns = ( + hasattr(info, '_access_time_ns') and + hasattr(info, '_mod_time_ns') and + (follow_symlinks or os.utime in os.supports_follow_symlinks)) if copy_times_ns: - access_time_ns = info._access_time_ns(follow_symlinks=False) - mod_time_ns = info._mod_time_ns(follow_symlinks=False) - if access_time_ns is not None and mod_time_ns is not None: - os.utime(target, ns=(access_time_ns, mod_time_ns), follow_symlinks=False) - - copy_xattrs = (hasattr(info, '_xattrs') and - hasattr(os, 'setxattr') and - os.setxattr in os.supports_fd) + t0 = info._access_time_ns(follow_symlinks=follow_symlinks) + t1 = info._mod_time_ns(follow_symlinks=follow_symlinks) + if t0 is not None and t1 is not None: + os.utime(target, ns=(t0, t1), follow_symlinks=follow_symlinks) + + copy_xattrs = ( + hasattr(info, '_xattrs') and + hasattr(os, 'setxattr') and + (follow_symlinks or os.setxattr in os.supports_follow_symlinks)) if copy_xattrs: - xattrs = info._xattrs(follow_symlinks=False) + xattrs = info._xattrs(follow_symlinks=follow_symlinks) if xattrs is not None: for attr, value in xattrs: try: - os.setxattr(target, attr, value, follow_symlinks=False) + os.setxattr(target, attr, value, follow_symlinks=follow_symlinks) except OSError as e: if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): raise - copy_posix_permissions = (hasattr(info, '_posix_permissions') and - hasattr(os, 'lchmod')) + copy_posix_permissions = ( + hasattr(info, '_posix_permissions') and + (follow_symlinks or os.chmod in os.supports_follow_symlinks)) if copy_posix_permissions: - posix_permissions = info._posix_permissions(follow_symlinks=False) + posix_permissions = info._posix_permissions(follow_symlinks=follow_symlinks) if posix_permissions is not None: try: - os.lchmod(target, posix_permissions) + os.chmod(target, posix_permissions, follow_symlinks=follow_symlinks) except NotImplementedError: pass - copy_bsd_flags = (hasattr(info, '_bsd_flags') and - hasattr(os, 'chflags') and - os.chflags in os.supports_follow_symlinks) + copy_bsd_flags = ( + hasattr(info, '_bsd_flags') and + hasattr(os, 'chflags') and + (follow_symlinks or os.chflags in os.supports_follow_symlinks)) if copy_bsd_flags: - bsd_flags = info._bsd_flags(follow_symlinks=False) + bsd_flags = info._bsd_flags(follow_symlinks=follow_symlinks) if bsd_flags is not None: try: - os.chflags(target, bsd_flags, follow_symlinks=False) + os.chflags(target, bsd_flags, follow_symlinks=follow_symlinks) except OSError as why: if why.errno not in (EOPNOTSUPP, ENOTSUP): raise From ba4d393e547dc8555c4e9e42ee9eea3af45009c1 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 9 Feb 2025 17:52:30 +0000 Subject: [PATCH 07/13] Simplify patch a bit --- Lib/pathlib/_os.py | 68 +++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 26df6654a1151c..832d3ce1aa8f7c 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -383,7 +383,7 @@ def _ensure_different_file(self, source): raise err -class _PosixPathInfo: +class _PathInfoBase: __slots__ = ('_path', '_stat_result', '_lstat_result') def __init__(self, path): @@ -417,34 +417,6 @@ def _stat(self, *, follow_symlinks=True): self._lstat_result = None return self._lstat_result - def exists(self, *, follow_symlinks=True): - """Whether this path exists.""" - st = self._stat(follow_symlinks=follow_symlinks) - if st is None: - return False - return True - - def is_dir(self, *, follow_symlinks=True): - """Whether this path is a directory.""" - st = self._stat(follow_symlinks=follow_symlinks) - if st is None: - return False - return S_ISDIR(st.st_mode) - - def is_file(self, *, follow_symlinks=True): - """Whether this path is a regular file.""" - st = self._stat(follow_symlinks=follow_symlinks) - if st is None: - return False - return S_ISREG(st.st_mode) - - def is_symlink(self): - """Whether this path is a symbolic link.""" - st = self._stat(follow_symlinks=False) - if st is None: - return False - return S_ISLNK(st.st_mode) - def _posix_permissions(self, *, follow_symlinks=True): """Return the POSIX file permissions, or None if stat() fails.""" st = self._stat(follow_symlinks=follow_symlinks) @@ -488,7 +460,7 @@ def _xattrs(self, *, follow_symlinks=True): return None -class _WindowsPathInfo(_PosixPathInfo): +class _WindowsPathInfo(_PathInfoBase): """Implementation of pathlib.types.PathInfo that provides status information for Windows paths. Don't try to construct it yourself.""" __slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink') @@ -544,10 +516,44 @@ def is_symlink(self): return self._is_symlink +class _PosixPathInfo(_PathInfoBase): + """Implementation of pathlib.types.PathInfo that provides status + information for POSIX paths. Don't try to construct it yourself.""" + __slots__ = () + + def exists(self, *, follow_symlinks=True): + """Whether this path exists.""" + st = self._stat(follow_symlinks=follow_symlinks) + if st is None: + return False + return True + + def is_dir(self, *, follow_symlinks=True): + """Whether this path is a directory.""" + st = self._stat(follow_symlinks=follow_symlinks) + if st is None: + return False + return S_ISDIR(st.st_mode) + + def is_file(self, *, follow_symlinks=True): + """Whether this path is a regular file.""" + st = self._stat(follow_symlinks=follow_symlinks) + if st is None: + return False + return S_ISREG(st.st_mode) + + def is_symlink(self): + """Whether this path is a symbolic link.""" + st = self._stat(follow_symlinks=False) + if st is None: + return False + return S_ISLNK(st.st_mode) + + PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo -class DirEntryInfo(_PosixPathInfo): +class DirEntryInfo(_PathInfoBase): """Implementation of pathlib.types.PathInfo that provides status information by querying a wrapped os.DirEntry object. Don't try to construct it yourself.""" From 6cde4fbc6d242a5277d723cf212cf34a8182066b Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 9 Feb 2025 18:02:11 +0000 Subject: [PATCH 08/13] Further reduce diff --- Lib/pathlib/_os.py | 60 +++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 832d3ce1aa8f7c..66817e10e979a5 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -211,6 +211,10 @@ class CopyWriter: def __init__(self, path): self._path = path + def _create_metadata(self, source, follow_symlinks=True): + """Copy metadata from the given path to our path.""" + pass + def _create(self, source, follow_symlinks, dirs_exist_ok, preserve_metadata): self._ensure_distinct_path(source) if not follow_symlinks and source.is_symlink(): @@ -259,10 +263,6 @@ def _create_symlink(self, source, preserve_metadata): if preserve_metadata: self._create_metadata(source, follow_symlinks=False) - def _create_metadata(self, source, follow_symlinks=True): - """Copy metadata from the given path to our path.""" - pass - def _ensure_different_file(self, source): """ Raise OSError(EINVAL) if both paths refer to the same file. @@ -295,26 +295,6 @@ class LocalCopyWriter(CopyWriter): """ __slots__ = () - if copyfile: - # Use fast OS routine for local file copying where available. - def _create_file(self, source, preserve_metadata): - """Copy the given file to the given target.""" - try: - source = os.fspath(source) - except TypeError: - super()._create_file(source, preserve_metadata) - else: - copyfile(source, os.fspath(self._path)) - - if os.name == 'nt': - # Windows: symlink target might not exist yet if we're copying several - # files, so ensure we pass is_dir to os.symlink(). - def _create_symlink(self, source, preserve_metadata): - """Copy the given symlink to the given target.""" - self._path.symlink_to(source.readlink(), source.is_dir()) - if preserve_metadata: - self._create_metadata(source, follow_symlinks=False) - def _create_metadata(self, source, follow_symlinks=True): """Copy metadata from the given path to our path.""" target = self._path @@ -330,6 +310,8 @@ def _create_metadata(self, source, follow_symlinks=True): if t0 is not None and t1 is not None: os.utime(target, ns=(t0, t1), follow_symlinks=follow_symlinks) + # We must copy extended attributes before the file is (potentially) + # chmod()'ed read-only, otherwise setxattr() will error with -EACCES. copy_xattrs = ( hasattr(info, '_xattrs') and hasattr(os, 'setxattr') and @@ -353,6 +335,16 @@ def _create_metadata(self, source, follow_symlinks=True): try: os.chmod(target, posix_permissions, 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 copy_bsd_flags = ( @@ -368,6 +360,26 @@ def _create_metadata(self, source, follow_symlinks=True): if why.errno not in (EOPNOTSUPP, ENOTSUP): raise + if copyfile: + # Use fast OS routine for local file copying where available. + def _create_file(self, source, preserve_metadata): + """Copy the given file to the given target.""" + try: + source = os.fspath(source) + except TypeError: + super()._create_file(source, preserve_metadata) + else: + copyfile(source, os.fspath(self._path)) + + if os.name == 'nt': + # Windows: symlink target might not exist yet if we're copying several + # files, so ensure we pass is_dir to os.symlink(). + def _create_symlink(self, source, preserve_metadata): + """Copy the given symlink to the given target.""" + self._path.symlink_to(source.readlink(), source.is_dir()) + if preserve_metadata: + self._create_metadata(source, follow_symlinks=False) + def _ensure_different_file(self, source): """ Raise OSError(EINVAL) if both paths refer to the same file. From 0871a3960c529aba7e5a886d1b275a767e7e4bb3 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 9 Feb 2025 18:59:48 +0000 Subject: [PATCH 09/13] Stop suppressing stat() errors from new methods --- Lib/pathlib/_os.py | 153 ++++++++++++++++++++++----------------------- 1 file changed, 76 insertions(+), 77 deletions(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 66817e10e979a5..9d69cf13831346 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -307,8 +307,7 @@ def _create_metadata(self, source, follow_symlinks=True): if copy_times_ns: t0 = info._access_time_ns(follow_symlinks=follow_symlinks) t1 = info._mod_time_ns(follow_symlinks=follow_symlinks) - if t0 is not None and t1 is not None: - os.utime(target, ns=(t0, t1), follow_symlinks=follow_symlinks) + os.utime(target, ns=(t0, t1), follow_symlinks=follow_symlinks) # We must copy extended attributes before the file is (potentially) # chmod()'ed read-only, otherwise setxattr() will error with -EACCES. @@ -318,34 +317,32 @@ def _create_metadata(self, source, follow_symlinks=True): (follow_symlinks or os.setxattr in os.supports_follow_symlinks)) if copy_xattrs: xattrs = info._xattrs(follow_symlinks=follow_symlinks) - if xattrs is not None: - for attr, value in xattrs: - try: - os.setxattr(target, attr, value, follow_symlinks=follow_symlinks) - except OSError as e: - if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): - raise + for attr, value in xattrs: + try: + os.setxattr(target, attr, value, follow_symlinks=follow_symlinks) + except OSError as e: + if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): + raise copy_posix_permissions = ( hasattr(info, '_posix_permissions') and (follow_symlinks or os.chmod in os.supports_follow_symlinks)) if copy_posix_permissions: posix_permissions = info._posix_permissions(follow_symlinks=follow_symlinks) - if posix_permissions is not None: - try: - os.chmod(target, posix_permissions, 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 + try: + os.chmod(target, posix_permissions, 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 copy_bsd_flags = ( hasattr(info, '_bsd_flags') and @@ -353,12 +350,11 @@ def _create_metadata(self, source, follow_symlinks=True): (follow_symlinks or os.chflags in os.supports_follow_symlinks)) if copy_bsd_flags: bsd_flags = info._bsd_flags(follow_symlinks=follow_symlinks) - if bsd_flags is not None: - try: - os.chflags(target, bsd_flags, follow_symlinks=follow_symlinks) - except OSError as why: - if why.errno not in (EOPNOTSUPP, ENOTSUP): - raise + try: + os.chflags(target, bsd_flags, follow_symlinks=follow_symlinks) + except OSError as why: + if why.errno not in (EOPNOTSUPP, ENOTSUP): + raise if copyfile: # Use fast OS routine for local file copying where available. @@ -405,63 +401,61 @@ def __repr__(self): path_type = "WindowsPath" if os.name == "nt" else "PosixPath" return f"<{path_type}.info>" - def _raw_stat(self, *, follow_symlinks=True): - return os.stat(self._path, follow_symlinks=follow_symlinks) - - def _stat(self, *, follow_symlinks=True): - """Return the status as an os.stat_result, or None if stat() fails.""" + def _stat(self, *, follow_symlinks=True, ignore_errors=False): + """Return the status as an os.stat_result, or None if stat() fails and + ignore_errors is true.""" if follow_symlinks: try: - return self._stat_result + result = self._stat_result except AttributeError: - try: - self._stat_result = self._raw_stat(follow_symlinks=True) - except (OSError, ValueError): - self._stat_result = None - return self._stat_result + pass + else: + if ignore_errors or result is not None: + return result + try: + self._stat_result = os.stat(self._path) + except (OSError, ValueError): + self._stat_result = None + if not ignore_errors: + raise + return self._stat_result else: try: - return self._lstat_result + result = self._lstat_result except AttributeError: - try: - self._lstat_result = self._raw_stat(follow_symlinks=False) - except (OSError, ValueError): - self._lstat_result = None - return self._lstat_result + pass + else: + if ignore_errors or result is not None: + return result + try: + self._lstat_result = os.lstat(self._path) + except (OSError, ValueError): + self._lstat_result = None + if not ignore_errors: + raise + return self._lstat_result def _posix_permissions(self, *, follow_symlinks=True): - """Return the POSIX file permissions, or None if stat() fails.""" - st = self._stat(follow_symlinks=follow_symlinks) - if st is None: - return None - return S_IMODE(st.st_mode) + """Return the POSIX file permissions.""" + return S_IMODE(self._stat(follow_symlinks=follow_symlinks).st_mode) def _access_time_ns(self, *, follow_symlinks=True): - """Return the access time in nanoseconds, or None if stat() fails.""" - st = self._stat(follow_symlinks=follow_symlinks) - if st is None: - return None - return st.st_atime_ns + """Return the access time in nanoseconds.""" + return self._stat(follow_symlinks=follow_symlinks).st_atime_ns def _mod_time_ns(self, *, follow_symlinks=True): - """Return the modify time in nanoseconds, or None if stat() fails.""" - st = self._stat(follow_symlinks=follow_symlinks) - if st is None: - return None - return st.st_mtime_ns + """Return the modify time in nanoseconds.""" + return self._stat(follow_symlinks=follow_symlinks).st_mtime_ns if hasattr(os.stat_result, 'st_flags'): def _bsd_flags(self, *, follow_symlinks=True): - """Return the flags, or None if stat() fails.""" - st = self._stat(follow_symlinks=follow_symlinks) - if st is None: - return None - return st.st_flags + """Return the flags.""" + return self._stat(follow_symlinks=follow_symlinks).st_flags if hasattr(os, 'listxattr'): def _xattrs(self, *, follow_symlinks=True): - """Return the xattrs as a list of (attr, value) pairs, or None if - extended attributes aren't supported.""" + """Return the xattrs as a list of (attr, value) pairs, or an empty + list if extended attributes aren't supported.""" try: return [ (attr, os.getxattr(self._path, attr, follow_symlinks=follow_symlinks)) @@ -469,7 +463,7 @@ def _xattrs(self, *, follow_symlinks=True): except OSError as err: if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): raise - return None + return [] class _WindowsPathInfo(_PathInfoBase): @@ -535,28 +529,28 @@ class _PosixPathInfo(_PathInfoBase): def exists(self, *, follow_symlinks=True): """Whether this path exists.""" - st = self._stat(follow_symlinks=follow_symlinks) + st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True) if st is None: return False return True def is_dir(self, *, follow_symlinks=True): """Whether this path is a directory.""" - st = self._stat(follow_symlinks=follow_symlinks) + st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True) if st is None: return False return S_ISDIR(st.st_mode) def is_file(self, *, follow_symlinks=True): """Whether this path is a regular file.""" - st = self._stat(follow_symlinks=follow_symlinks) + st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True) if st is None: return False return S_ISREG(st.st_mode) def is_symlink(self): """Whether this path is a symbolic link.""" - st = self._stat(follow_symlinks=False) + st = self._stat(follow_symlinks=False, ignore_errors=True) if st is None: return False return S_ISLNK(st.st_mode) @@ -575,14 +569,19 @@ def __init__(self, entry): super().__init__(entry.path) self._entry = entry - def _raw_stat(self, *, follow_symlinks=True): - return self._entry.stat(follow_symlinks=follow_symlinks) + def _stat(self, *, follow_symlinks=True, ignore_errors=False): + try: + return self._entry.stat(follow_symlinks=follow_symlinks) + except OSError: + if not ignore_errors: + raise + return None def exists(self, *, follow_symlinks=True): """Whether this path exists.""" if not follow_symlinks: return True - return self._stat() is not None + return self._stat(ignore_errors=True) is not None def is_dir(self, *, follow_symlinks=True): """Whether this path is a directory.""" From 89e49bf9a46100982e83f71c9d2b21a2837f3675 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 11 Feb 2025 22:54:56 +0000 Subject: [PATCH 10/13] Stop using `samefile()`, add `_file_id()` and `_device_id()` --- Lib/pathlib/_local.py | 4 +-- Lib/pathlib/_os.py | 83 +++++++++++++++++++++++++------------------ 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 742358af32aab4..5d2033333853a3 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -19,7 +19,7 @@ except ImportError: grp = None -from pathlib._os import LocalCopyWriter, PathInfo, DirEntryInfo +from pathlib._os import LocalCopyWriter, PathInfo, DirEntryInfo, ensure_different_files from pathlib._abc import JoinablePath, ReadablePath, WritablePath @@ -1069,7 +1069,7 @@ def move(self, target): else: if not hasattr(target, '_copy_writer'): target = self.with_segments(target_str) - target._copy_writer._ensure_different_file(self) + ensure_different_files(self, target) try: os.replace(self, target_str) return target diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 9d69cf13831346..38c92a339f3e01 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -216,7 +216,7 @@ def _create_metadata(self, source, follow_symlinks=True): pass def _create(self, source, follow_symlinks, dirs_exist_ok, preserve_metadata): - self._ensure_distinct_path(source) + ensure_distinct_paths(source, self._path) if not follow_symlinks and source.is_symlink(): self._create_symlink(source, preserve_metadata) elif source.is_dir(): @@ -243,7 +243,7 @@ def _create_dir(self, source, follow_symlinks, dirs_exist_ok, preserve_metadata) def _create_file(self, source, preserve_metadata): """Copy the given file to our path.""" - self._ensure_different_file(source) + ensure_different_files(source, self._path) with magic_open(source, 'rb') as source_f: try: with magic_open(self._path, 'wb') as target_f: @@ -263,30 +263,25 @@ def _create_symlink(self, source, preserve_metadata): if preserve_metadata: self._create_metadata(source, follow_symlinks=False) - def _ensure_different_file(self, source): - """ - Raise OSError(EINVAL) if both paths refer to the same file. - """ - pass - def _ensure_distinct_path(self, source): - """ - Raise OSError(EINVAL) if the other path is within this path. - """ - # Note: there is no straightforward, foolproof algorithm to determine - # if one directory is within another (a particularly perverse example - # would be a single network share mounted in one location via NFS, and - # in another location via CIFS), so we simply checks whether the - # other path is lexically equal to, or within, this path. - if source == self._path: - err = OSError(EINVAL, "Source and target are the same path") - elif source in self._path.parents: - err = OSError(EINVAL, "Source path is a parent of target path") - else: - return - err.filename = str(source) - err.filename2 = str(self._path) - raise err +def ensure_distinct_paths(source, target): + """ + Raise OSError(EINVAL) if the other path is within this path. + """ + # Note: there is no straightforward, foolproof algorithm to determine + # if one directory is within another (a particularly perverse example + # would be a single network share mounted in one location via NFS, and + # in another location via CIFS), so we simply checks whether the + # other path is lexically equal to, or within, this path. + if source == target: + err = OSError(EINVAL, "Source and target are the same path") + elif source in target.parents: + err = OSError(EINVAL, "Source path is a parent of target path") + else: + return + err.filename = str(source) + err.filename2 = str(target) + raise err class LocalCopyWriter(CopyWriter): @@ -376,19 +371,31 @@ def _create_symlink(self, source, preserve_metadata): if preserve_metadata: self._create_metadata(source, follow_symlinks=False) - def _ensure_different_file(self, source): - """ - Raise OSError(EINVAL) if both paths refer to the same file. - """ + +def ensure_different_files(source, target): + """ + Raise OSError(EINVAL) if both paths refer to the same file. + """ + try: + source_file_id = source.info._file_id + target_file_id = target.info._file_id + source_device_id = source.info._device_id + target_device_id = target.info._device_id + except AttributeError: + if source != target: + return + else: try: - if not self._path.samefile(source): + if source_file_id() != target_file_id(): + return + if source_device_id() != target_device_id(): return except (OSError, ValueError): return - err = OSError(EINVAL, "Source and target are the same file") - err.filename = str(source) - err.filename2 = str(self._path) - raise err + err = OSError(EINVAL, "Source and target are the same file") + err.filename = str(source) + err.filename2 = str(target) + raise err class _PathInfoBase: @@ -439,6 +446,14 @@ def _posix_permissions(self, *, follow_symlinks=True): """Return the POSIX file permissions.""" return S_IMODE(self._stat(follow_symlinks=follow_symlinks).st_mode) + def _file_id(self, *, follow_symlinks=True): + """Returns the identifier of the file (unique for a device ID).""" + return self._stat(follow_symlinks=follow_symlinks).st_ino + + def _device_id(self, *, follow_symlinks=True): + """Returns the identifier of the device on which the file resides.""" + return self._stat(follow_symlinks=follow_symlinks).st_dev + def _access_time_ns(self, *, follow_symlinks=True): """Return the access time in nanoseconds.""" return self._stat(follow_symlinks=follow_symlinks).st_atime_ns From 86b671f124e91213eb108c0cda1923aaa7781c41 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 12 Feb 2025 00:14:37 +0000 Subject: [PATCH 11/13] Attempt to fix access time test --- Lib/test/test_pathlib/test_pathlib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 31e5306ae60538..104a7adcb8d03a 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -1440,11 +1440,13 @@ def test_copy_dir_preserve_metadata(self): if hasattr(os, 'chflags') and hasattr(stat, 'UF_NODUMP'): os.chflags(source / 'fileC', stat.UF_NODUMP) target = base / 'copyA' + + subpaths = ['.', 'fileC', 'dirD', 'dirD/fileD'] + source_sts = [source.joinpath(subpath).stat() for subpath in subpaths] source.copy(target, preserve_metadata=True) + target_sts = [target.joinpath(subpath).stat() for subpath in subpaths] - for subpath in ['.', 'fileC', 'dirD', 'dirD/fileD']: - source_st = source.joinpath(subpath).stat() - target_st = target.joinpath(subpath).stat() + for source_st, target_st in zip(source_sts, target_sts): 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) From f6b2249b8558d09fc400a0e3678e9ea30059343e Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 13 Feb 2025 17:09:03 +0000 Subject: [PATCH 12/13] Delete `_device_id()` --- Lib/pathlib/_os.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 38c92a339f3e01..52d95eafef5610 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -379,8 +379,6 @@ def ensure_different_files(source, target): try: source_file_id = source.info._file_id target_file_id = target.info._file_id - source_device_id = source.info._device_id - target_device_id = target.info._device_id except AttributeError: if source != target: return @@ -388,8 +386,6 @@ def ensure_different_files(source, target): try: if source_file_id() != target_file_id(): return - if source_device_id() != target_device_id(): - return except (OSError, ValueError): return err = OSError(EINVAL, "Source and target are the same file") @@ -447,12 +443,9 @@ def _posix_permissions(self, *, follow_symlinks=True): return S_IMODE(self._stat(follow_symlinks=follow_symlinks).st_mode) def _file_id(self, *, follow_symlinks=True): - """Returns the identifier of the file (unique for a device ID).""" - return self._stat(follow_symlinks=follow_symlinks).st_ino - - def _device_id(self, *, follow_symlinks=True): - """Returns the identifier of the device on which the file resides.""" - return self._stat(follow_symlinks=follow_symlinks).st_dev + """Returns the identifier of the file.""" + st = self._stat(follow_symlinks=follow_symlinks) + return st.st_dev, st.st_ino def _access_time_ns(self, *, follow_symlinks=True): """Return the access time in nanoseconds.""" From 13db1fe5828684c8fd6e22a872d672498c321983 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 17 Feb 2025 18:50:43 +0000 Subject: [PATCH 13/13] `_create_metadata` -> `_copy_metadata` --- Lib/pathlib/_os.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 52d95eafef5610..e5b894b524ca7e 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -211,7 +211,7 @@ class CopyWriter: def __init__(self, path): self._path = path - def _create_metadata(self, source, follow_symlinks=True): + def _copy_metadata(self, source, follow_symlinks=True): """Copy metadata from the given path to our path.""" pass @@ -239,7 +239,7 @@ def _create_dir(self, source, follow_symlinks, dirs_exist_ok, preserve_metadata) dst._copy_writer._create_file(src, preserve_metadata) if preserve_metadata: - self._create_metadata(source) + self._copy_metadata(source) def _create_file(self, source, preserve_metadata): """Copy the given file to our path.""" @@ -255,13 +255,13 @@ def _create_file(self, source, preserve_metadata): f'Directory does not exist: {self._path}') from e raise if preserve_metadata: - self._create_metadata(source) + self._copy_metadata(source) def _create_symlink(self, source, preserve_metadata): """Copy the given symbolic link to our path.""" self._path.symlink_to(source.readlink()) if preserve_metadata: - self._create_metadata(source, follow_symlinks=False) + self._copy_metadata(source, follow_symlinks=False) def ensure_distinct_paths(source, target): @@ -290,7 +290,7 @@ class LocalCopyWriter(CopyWriter): """ __slots__ = () - def _create_metadata(self, source, follow_symlinks=True): + def _copy_metadata(self, source, follow_symlinks=True): """Copy metadata from the given path to our path.""" target = self._path info = source.info @@ -369,7 +369,7 @@ def _create_symlink(self, source, preserve_metadata): """Copy the given symlink to the given target.""" self._path.symlink_to(source.readlink(), source.is_dir()) if preserve_metadata: - self._create_metadata(source, follow_symlinks=False) + self._copy_metadata(source, follow_symlinks=False) def ensure_different_files(source, target):