Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Version History
Unreleased
----------

- Nothing yet
- Add ``JoinablePath.relative_to()`` and ``JoinablePath.is_relative_to()``.

v0.5.1
------
Expand Down
15 changes: 15 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ This package offers the following abstract base classes:
:meth:`~JoinablePath.__truediv__`
:meth:`~JoinablePath.__rtruediv__`

:meth:`~JoinablePath.relative_to`
:meth:`~JoinablePath.is_relative_to`

:meth:`~JoinablePath.full_match`

- * :class:`ReadablePath`
Expand Down Expand Up @@ -291,6 +294,18 @@ This package offers the following abstract base classes:

Return a new path with the given path segment joined on the beginning.

.. method:: relative_to(other, *, walk_up=False)

Return a new relative path from *other* to this path. The default
implementation compares this path and the parents of *other*;
``__eq__()`` must be implemented for this to work correctly.

.. method:: is_relative_to(other)

Returns ``True`` is this path is relative to *other*, ``False``
otherwise. The default implementation compares this path and the parents
of *other*; ``__eq__()`` must be implemented for this to work correctly.

.. method:: full_match(pattern)

Return true if the path matches the given glob-style pattern, false
Expand Down
27 changes: 27 additions & 0 deletions pathlib_abc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,33 @@ def parents(self):
parent = split(path)[0]
return tuple(parents)

def relative_to(self, other, *, walk_up=False):
"""Return the relative path to another path identified by the passed
arguments. If the operation is not possible (because this is not
related to the other path), raise ValueError.

The *walk_up* parameter controls whether `..` may be used to resolve
the path.
"""
parts = []
for path in (other,) + other.parents:
if self.is_relative_to(path):
break
elif not walk_up:
raise ValueError(f"{self!r} is not in the subpath of {other!r}")
elif path.name == '..':
raise ValueError(f"'..' segment in {other!r} cannot be walked")
else:
parts.append('..')
else:
raise ValueError(f"{self!r} and {other!r} have different anchors")
return self.with_segments(*parts, *self.parts[len(path.parts):])

def is_relative_to(self, other):
"""Return True if the path is relative to another path or False.
"""
return other == self or other in self.parents

def full_match(self, pattern):
"""
Return True if this path matches the given glob-style pattern. The
Expand Down
279 changes: 0 additions & 279 deletions pathlib_abc/_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from errno import *
from io import TextIOWrapper
from stat import S_ISDIR, S_ISREG, S_ISLNK, S_IMODE
import os
import sys
try:
Expand Down Expand Up @@ -307,281 +306,3 @@ def ensure_different_files(source, target):
err.filename = vfspath(source)
err.filename2 = vfspath(target)
raise err


def copy_info(info, target, follow_symlinks=True):
"""Copy metadata from the given PathInfo to the given local path."""
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:
t0 = info._access_time_ns(follow_symlinks=follow_symlinks)
t1 = info._mod_time_ns(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.
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=follow_symlinks)
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)
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
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=follow_symlinks)
try:
os.chflags(target, bsd_flags, follow_symlinks=follow_symlinks)
except OSError as why:
if why.errno not in (EOPNOTSUPP, ENOTSUP):
raise


class _PathInfoBase:
__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 _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:
result = self._stat_result
except AttributeError:
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:
result = self._lstat_result
except AttributeError:
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."""
return S_IMODE(self._stat(follow_symlinks=follow_symlinks).st_mode)

def _file_id(self, *, follow_symlinks=True):
"""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."""
return self._stat(follow_symlinks=follow_symlinks).st_atime_ns

def _mod_time_ns(self, *, follow_symlinks=True):
"""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."""
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 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__ = ('_exists', '_is_dir', '_is_file', '_is_symlink')

def exists(self, *, follow_symlinks=True):
"""Whether this path exists."""
if not follow_symlinks and self.is_symlink():
return True
try:
return self._exists
except AttributeError:
if os.path.exists(self._path):
self._exists = True
return True
else:
self._exists = self._is_dir = self._is_file = False
return False

def is_dir(self, *, follow_symlinks=True):
"""Whether this path is a directory."""
if not follow_symlinks and self.is_symlink():
return False
try:
return self._is_dir
except AttributeError:
if os.path.isdir(self._path):
self._is_dir = self._exists = True
return True
else:
self._is_dir = False
return False

def is_file(self, *, follow_symlinks=True):
"""Whether this path is a regular file."""
if not follow_symlinks and self.is_symlink():
return False
try:
return self._is_file
except AttributeError:
if os.path.isfile(self._path):
self._is_file = self._exists = True
return True
else:
self._is_file = False
return False

def is_symlink(self):
"""Whether this path is a symbolic link."""
try:
return self._is_symlink
except AttributeError:
self._is_symlink = os.path.islink(self._path)
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, 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, 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, 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, ignore_errors=True)
if st is None:
return False
return S_ISLNK(st.st_mode)


PathInfo = _WindowsPathInfo if os.name == 'nt' else _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."""
__slots__ = ('_entry',)

def __init__(self, entry):
super().__init__(entry.path)
self._entry = entry

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(ignore_errors=True) 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
Loading