Skip to content

Commit a4276c1

Browse files
committed
Sync from upstream
1 parent 3ab34ab commit a4276c1

File tree

5 files changed

+98
-280
lines changed

5 files changed

+98
-280
lines changed

CHANGES.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Version History
44
Unreleased
55
----------
66

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

99
v0.5.1
1010
------

docs/api.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ This package offers the following abstract base classes:
164164
:meth:`~JoinablePath.__truediv__`
165165
:meth:`~JoinablePath.__rtruediv__`
166166

167+
:meth:`~JoinablePath.relative_to`
168+
:meth:`~JoinablePath.is_relative_to`
169+
167170
:meth:`~JoinablePath.full_match`
168171

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

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

297+
.. method:: relative_to(other, *, walk_up=False)
298+
299+
Return a new relative path from *other* to this path. The default
300+
implementation compares this path and the parents of *other*;
301+
``__eq__()`` must be implemented for this to work correctly.
302+
303+
.. method:: is_relative_to(other)
304+
305+
Returns ``True`` is this path is relative to *other*, ``False``
306+
otherwise. The default implementation compares this path and the parents
307+
of *other*; ``__eq__()`` must be implemented for this to work correctly.
308+
294309
.. method:: full_match(pattern)
295310

296311
Return true if the path matches the given glob-style pattern, false

pathlib_abc/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,33 @@ def parents(self):
241241
parent = split(path)[0]
242242
return tuple(parents)
243243

244+
def relative_to(self, other, *, walk_up=False):
245+
"""Return the relative path to another path identified by the passed
246+
arguments. If the operation is not possible (because this is not
247+
related to the other path), raise ValueError.
248+
249+
The *walk_up* parameter controls whether `..` may be used to resolve
250+
the path.
251+
"""
252+
parts = []
253+
for path in (other,) + other.parents:
254+
if self.is_relative_to(path):
255+
break
256+
elif not walk_up:
257+
raise ValueError(f"{self!r} is not in the subpath of {other!r}")
258+
elif path.name == '..':
259+
raise ValueError(f"'..' segment in {other!r} cannot be walked")
260+
else:
261+
parts.append('..')
262+
else:
263+
raise ValueError(f"{self!r} and {other!r} have different anchors")
264+
return self.with_segments(*parts, *self.parts[len(path.parts):])
265+
266+
def is_relative_to(self, other):
267+
"""Return True if the path is relative to another path or False.
268+
"""
269+
return other == self or other in self.parents
270+
244271
def full_match(self, pattern):
245272
"""
246273
Return True if this path matches the given glob-style pattern. The

pathlib_abc/_os.py

Lines changed: 0 additions & 279 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from errno import *
66
from io import TextIOWrapper
7-
from stat import S_ISDIR, S_ISREG, S_ISLNK, S_IMODE
87
import os
98
import sys
109
try:
@@ -307,281 +306,3 @@ def ensure_different_files(source, target):
307306
err.filename = vfspath(source)
308307
err.filename2 = vfspath(target)
309308
raise err
310-
311-
312-
def copy_info(info, target, follow_symlinks=True):
313-
"""Copy metadata from the given PathInfo to the given local path."""
314-
copy_times_ns = (
315-
hasattr(info, '_access_time_ns') and
316-
hasattr(info, '_mod_time_ns') and
317-
(follow_symlinks or os.utime in os.supports_follow_symlinks))
318-
if copy_times_ns:
319-
t0 = info._access_time_ns(follow_symlinks=follow_symlinks)
320-
t1 = info._mod_time_ns(follow_symlinks=follow_symlinks)
321-
os.utime(target, ns=(t0, t1), follow_symlinks=follow_symlinks)
322-
323-
# We must copy extended attributes before the file is (potentially)
324-
# chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
325-
copy_xattrs = (
326-
hasattr(info, '_xattrs') and
327-
hasattr(os, 'setxattr') and
328-
(follow_symlinks or os.setxattr in os.supports_follow_symlinks))
329-
if copy_xattrs:
330-
xattrs = info._xattrs(follow_symlinks=follow_symlinks)
331-
for attr, value in xattrs:
332-
try:
333-
os.setxattr(target, attr, value, follow_symlinks=follow_symlinks)
334-
except OSError as e:
335-
if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
336-
raise
337-
338-
copy_posix_permissions = (
339-
hasattr(info, '_posix_permissions') and
340-
(follow_symlinks or os.chmod in os.supports_follow_symlinks))
341-
if copy_posix_permissions:
342-
posix_permissions = info._posix_permissions(follow_symlinks=follow_symlinks)
343-
try:
344-
os.chmod(target, posix_permissions, follow_symlinks=follow_symlinks)
345-
except NotImplementedError:
346-
# if we got a NotImplementedError, it's because
347-
# * follow_symlinks=False,
348-
# * lchown() is unavailable, and
349-
# * either
350-
# * fchownat() is unavailable or
351-
# * fchownat() doesn't implement AT_SYMLINK_NOFOLLOW.
352-
# (it returned ENOSUP.)
353-
# therefore we're out of options--we simply cannot chown the
354-
# symlink. give up, suppress the error.
355-
# (which is what shutil always did in this circumstance.)
356-
pass
357-
358-
copy_bsd_flags = (
359-
hasattr(info, '_bsd_flags') and
360-
hasattr(os, 'chflags') and
361-
(follow_symlinks or os.chflags in os.supports_follow_symlinks))
362-
if copy_bsd_flags:
363-
bsd_flags = info._bsd_flags(follow_symlinks=follow_symlinks)
364-
try:
365-
os.chflags(target, bsd_flags, follow_symlinks=follow_symlinks)
366-
except OSError as why:
367-
if why.errno not in (EOPNOTSUPP, ENOTSUP):
368-
raise
369-
370-
371-
class _PathInfoBase:
372-
__slots__ = ('_path', '_stat_result', '_lstat_result')
373-
374-
def __init__(self, path):
375-
self._path = str(path)
376-
377-
def __repr__(self):
378-
path_type = "WindowsPath" if os.name == "nt" else "PosixPath"
379-
return f"<{path_type}.info>"
380-
381-
def _stat(self, *, follow_symlinks=True, ignore_errors=False):
382-
"""Return the status as an os.stat_result, or None if stat() fails and
383-
ignore_errors is true."""
384-
if follow_symlinks:
385-
try:
386-
result = self._stat_result
387-
except AttributeError:
388-
pass
389-
else:
390-
if ignore_errors or result is not None:
391-
return result
392-
try:
393-
self._stat_result = os.stat(self._path)
394-
except (OSError, ValueError):
395-
self._stat_result = None
396-
if not ignore_errors:
397-
raise
398-
return self._stat_result
399-
else:
400-
try:
401-
result = self._lstat_result
402-
except AttributeError:
403-
pass
404-
else:
405-
if ignore_errors or result is not None:
406-
return result
407-
try:
408-
self._lstat_result = os.lstat(self._path)
409-
except (OSError, ValueError):
410-
self._lstat_result = None
411-
if not ignore_errors:
412-
raise
413-
return self._lstat_result
414-
415-
def _posix_permissions(self, *, follow_symlinks=True):
416-
"""Return the POSIX file permissions."""
417-
return S_IMODE(self._stat(follow_symlinks=follow_symlinks).st_mode)
418-
419-
def _file_id(self, *, follow_symlinks=True):
420-
"""Returns the identifier of the file."""
421-
st = self._stat(follow_symlinks=follow_symlinks)
422-
return st.st_dev, st.st_ino
423-
424-
def _access_time_ns(self, *, follow_symlinks=True):
425-
"""Return the access time in nanoseconds."""
426-
return self._stat(follow_symlinks=follow_symlinks).st_atime_ns
427-
428-
def _mod_time_ns(self, *, follow_symlinks=True):
429-
"""Return the modify time in nanoseconds."""
430-
return self._stat(follow_symlinks=follow_symlinks).st_mtime_ns
431-
432-
if hasattr(os.stat_result, 'st_flags'):
433-
def _bsd_flags(self, *, follow_symlinks=True):
434-
"""Return the flags."""
435-
return self._stat(follow_symlinks=follow_symlinks).st_flags
436-
437-
if hasattr(os, 'listxattr'):
438-
def _xattrs(self, *, follow_symlinks=True):
439-
"""Return the xattrs as a list of (attr, value) pairs, or an empty
440-
list if extended attributes aren't supported."""
441-
try:
442-
return [
443-
(attr, os.getxattr(self._path, attr, follow_symlinks=follow_symlinks))
444-
for attr in os.listxattr(self._path, follow_symlinks=follow_symlinks)]
445-
except OSError as err:
446-
if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
447-
raise
448-
return []
449-
450-
451-
class _WindowsPathInfo(_PathInfoBase):
452-
"""Implementation of pathlib.types.PathInfo that provides status
453-
information for Windows paths. Don't try to construct it yourself."""
454-
__slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink')
455-
456-
def exists(self, *, follow_symlinks=True):
457-
"""Whether this path exists."""
458-
if not follow_symlinks and self.is_symlink():
459-
return True
460-
try:
461-
return self._exists
462-
except AttributeError:
463-
if os.path.exists(self._path):
464-
self._exists = True
465-
return True
466-
else:
467-
self._exists = self._is_dir = self._is_file = False
468-
return False
469-
470-
def is_dir(self, *, follow_symlinks=True):
471-
"""Whether this path is a directory."""
472-
if not follow_symlinks and self.is_symlink():
473-
return False
474-
try:
475-
return self._is_dir
476-
except AttributeError:
477-
if os.path.isdir(self._path):
478-
self._is_dir = self._exists = True
479-
return True
480-
else:
481-
self._is_dir = False
482-
return False
483-
484-
def is_file(self, *, follow_symlinks=True):
485-
"""Whether this path is a regular file."""
486-
if not follow_symlinks and self.is_symlink():
487-
return False
488-
try:
489-
return self._is_file
490-
except AttributeError:
491-
if os.path.isfile(self._path):
492-
self._is_file = self._exists = True
493-
return True
494-
else:
495-
self._is_file = False
496-
return False
497-
498-
def is_symlink(self):
499-
"""Whether this path is a symbolic link."""
500-
try:
501-
return self._is_symlink
502-
except AttributeError:
503-
self._is_symlink = os.path.islink(self._path)
504-
return self._is_symlink
505-
506-
507-
class _PosixPathInfo(_PathInfoBase):
508-
"""Implementation of pathlib.types.PathInfo that provides status
509-
information for POSIX paths. Don't try to construct it yourself."""
510-
__slots__ = ()
511-
512-
def exists(self, *, follow_symlinks=True):
513-
"""Whether this path exists."""
514-
st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True)
515-
if st is None:
516-
return False
517-
return True
518-
519-
def is_dir(self, *, follow_symlinks=True):
520-
"""Whether this path is a directory."""
521-
st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True)
522-
if st is None:
523-
return False
524-
return S_ISDIR(st.st_mode)
525-
526-
def is_file(self, *, follow_symlinks=True):
527-
"""Whether this path is a regular file."""
528-
st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True)
529-
if st is None:
530-
return False
531-
return S_ISREG(st.st_mode)
532-
533-
def is_symlink(self):
534-
"""Whether this path is a symbolic link."""
535-
st = self._stat(follow_symlinks=False, ignore_errors=True)
536-
if st is None:
537-
return False
538-
return S_ISLNK(st.st_mode)
539-
540-
541-
PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo
542-
543-
544-
class DirEntryInfo(_PathInfoBase):
545-
"""Implementation of pathlib.types.PathInfo that provides status
546-
information by querying a wrapped os.DirEntry object. Don't try to
547-
construct it yourself."""
548-
__slots__ = ('_entry',)
549-
550-
def __init__(self, entry):
551-
super().__init__(entry.path)
552-
self._entry = entry
553-
554-
def _stat(self, *, follow_symlinks=True, ignore_errors=False):
555-
try:
556-
return self._entry.stat(follow_symlinks=follow_symlinks)
557-
except OSError:
558-
if not ignore_errors:
559-
raise
560-
return None
561-
562-
def exists(self, *, follow_symlinks=True):
563-
"""Whether this path exists."""
564-
if not follow_symlinks:
565-
return True
566-
return self._stat(ignore_errors=True) is not None
567-
568-
def is_dir(self, *, follow_symlinks=True):
569-
"""Whether this path is a directory."""
570-
try:
571-
return self._entry.is_dir(follow_symlinks=follow_symlinks)
572-
except OSError:
573-
return False
574-
575-
def is_file(self, *, follow_symlinks=True):
576-
"""Whether this path is a regular file."""
577-
try:
578-
return self._entry.is_file(follow_symlinks=follow_symlinks)
579-
except OSError:
580-
return False
581-
582-
def is_symlink(self):
583-
"""Whether this path is a symbolic link."""
584-
try:
585-
return self._entry.is_symlink()
586-
except OSError:
587-
return False

0 commit comments

Comments
 (0)