• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2Abstract base classes for rich path objects.
3
4This module is published as a PyPI package called "pathlib-abc".
5
6This module is also a *PRIVATE* part of the Python standard library, where
7it's developed alongside pathlib. If it finds success and maturity as a PyPI
8package, it could become a public part of the standard library.
9
10Two base classes are defined here -- PurePathBase and PathBase -- that
11resemble pathlib's PurePath and Path respectively.
12"""
13
14import functools
15from glob import _Globber, _no_recurse_symlinks
16from errno import ENOENT, ENOTDIR, EBADF, ELOOP, EINVAL
17from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
18
19
20__all__ = ["UnsupportedOperation"]
21
22#
23# Internals
24#
25
26_WINERROR_NOT_READY = 21  # drive exists but is not accessible
27_WINERROR_INVALID_NAME = 123  # fix for bpo-35306
28_WINERROR_CANT_RESOLVE_FILENAME = 1921  # broken symlink pointing to itself
29
30# EBADF - guard against macOS `stat` throwing EBADF
31_IGNORED_ERRNOS = (ENOENT, ENOTDIR, EBADF, ELOOP)
32
33_IGNORED_WINERRORS = (
34    _WINERROR_NOT_READY,
35    _WINERROR_INVALID_NAME,
36    _WINERROR_CANT_RESOLVE_FILENAME)
37
38def _ignore_error(exception):
39    return (getattr(exception, 'errno', None) in _IGNORED_ERRNOS or
40            getattr(exception, 'winerror', None) in _IGNORED_WINERRORS)
41
42
43@functools.cache
44def _is_case_sensitive(parser):
45    return parser.normcase('Aa') == 'Aa'
46
47
48class UnsupportedOperation(NotImplementedError):
49    """An exception that is raised when an unsupported operation is called on
50    a path object.
51    """
52    pass
53
54
55class ParserBase:
56    """Base class for path parsers, which do low-level path manipulation.
57
58    Path parsers provide a subset of the os.path API, specifically those
59    functions needed to provide PurePathBase functionality. Each PurePathBase
60    subclass references its path parser via a 'parser' class attribute.
61
62    Every method in this base class raises an UnsupportedOperation exception.
63    """
64
65    @classmethod
66    def _unsupported_msg(cls, attribute):
67        return f"{cls.__name__}.{attribute} is unsupported"
68
69    @property
70    def sep(self):
71        """The character used to separate path components."""
72        raise UnsupportedOperation(self._unsupported_msg('sep'))
73
74    def join(self, path, *paths):
75        """Join path segments."""
76        raise UnsupportedOperation(self._unsupported_msg('join()'))
77
78    def split(self, path):
79        """Split the path into a pair (head, tail), where *head* is everything
80        before the final path separator, and *tail* is everything after.
81        Either part may be empty.
82        """
83        raise UnsupportedOperation(self._unsupported_msg('split()'))
84
85    def splitdrive(self, path):
86        """Split the path into a 2-item tuple (drive, tail), where *drive* is
87        a device name or mount point, and *tail* is everything after the
88        drive. Either part may be empty."""
89        raise UnsupportedOperation(self._unsupported_msg('splitdrive()'))
90
91    def normcase(self, path):
92        """Normalize the case of the path."""
93        raise UnsupportedOperation(self._unsupported_msg('normcase()'))
94
95    def isabs(self, path):
96        """Returns whether the path is absolute, i.e. unaffected by the
97        current directory or drive."""
98        raise UnsupportedOperation(self._unsupported_msg('isabs()'))
99
100
101class PurePathBase:
102    """Base class for pure path objects.
103
104    This class *does not* provide several magic methods that are defined in
105    its subclass PurePath. They are: __fspath__, __bytes__, __reduce__,
106    __hash__, __eq__, __lt__, __le__, __gt__, __ge__. Its initializer and path
107    joining methods accept only strings, not os.PathLike objects more broadly.
108    """
109
110    __slots__ = (
111        # The `_raw_path` slot store a joined string path. This is set in the
112        # `__init__()` method.
113        '_raw_path',
114
115        # The '_resolving' slot stores a boolean indicating whether the path
116        # is being processed by `PathBase.resolve()`. This prevents duplicate
117        # work from occurring when `resolve()` calls `stat()` or `readlink()`.
118        '_resolving',
119    )
120    parser = ParserBase()
121    _globber = _Globber
122
123    def __init__(self, path, *paths):
124        self._raw_path = self.parser.join(path, *paths) if paths else path
125        if not isinstance(self._raw_path, str):
126            raise TypeError(
127                f"path should be a str, not {type(self._raw_path).__name__!r}")
128        self._resolving = False
129
130    def with_segments(self, *pathsegments):
131        """Construct a new path object from any number of path-like objects.
132        Subclasses may override this method to customize how new path objects
133        are created from methods like `iterdir()`.
134        """
135        return type(self)(*pathsegments)
136
137    def __str__(self):
138        """Return the string representation of the path, suitable for
139        passing to system calls."""
140        return self._raw_path
141
142    def as_posix(self):
143        """Return the string representation of the path with forward (/)
144        slashes."""
145        return str(self).replace(self.parser.sep, '/')
146
147    @property
148    def drive(self):
149        """The drive prefix (letter or UNC path), if any."""
150        return self.parser.splitdrive(self.anchor)[0]
151
152    @property
153    def root(self):
154        """The root of the path, if any."""
155        return self.parser.splitdrive(self.anchor)[1]
156
157    @property
158    def anchor(self):
159        """The concatenation of the drive and root, or ''."""
160        return self._stack[0]
161
162    @property
163    def name(self):
164        """The final path component, if any."""
165        return self.parser.split(self._raw_path)[1]
166
167    @property
168    def suffix(self):
169        """
170        The final component's last suffix, if any.
171
172        This includes the leading period. For example: '.txt'
173        """
174        name = self.name
175        i = name.rfind('.')
176        if 0 < i < len(name) - 1:
177            return name[i:]
178        else:
179            return ''
180
181    @property
182    def suffixes(self):
183        """
184        A list of the final component's suffixes, if any.
185
186        These include the leading periods. For example: ['.tar', '.gz']
187        """
188        name = self.name
189        if name.endswith('.'):
190            return []
191        name = name.lstrip('.')
192        return ['.' + suffix for suffix in name.split('.')[1:]]
193
194    @property
195    def stem(self):
196        """The final path component, minus its last suffix."""
197        name = self.name
198        i = name.rfind('.')
199        if 0 < i < len(name) - 1:
200            return name[:i]
201        else:
202            return name
203
204    def with_name(self, name):
205        """Return a new path with the file name changed."""
206        split = self.parser.split
207        if split(name)[0]:
208            raise ValueError(f"Invalid name {name!r}")
209        return self.with_segments(split(self._raw_path)[0], name)
210
211    def with_stem(self, stem):
212        """Return a new path with the stem changed."""
213        suffix = self.suffix
214        if not suffix:
215            return self.with_name(stem)
216        elif not stem:
217            # If the suffix is non-empty, we can't make the stem empty.
218            raise ValueError(f"{self!r} has a non-empty suffix")
219        else:
220            return self.with_name(stem + suffix)
221
222    def with_suffix(self, suffix):
223        """Return a new path with the file suffix changed.  If the path
224        has no suffix, add given suffix.  If the given suffix is an empty
225        string, remove the suffix from the path.
226        """
227        stem = self.stem
228        if not stem:
229            # If the stem is empty, we can't make the suffix non-empty.
230            raise ValueError(f"{self!r} has an empty name")
231        elif suffix and not (suffix.startswith('.') and len(suffix) > 1):
232            raise ValueError(f"Invalid suffix {suffix!r}")
233        else:
234            return self.with_name(stem + suffix)
235
236    def relative_to(self, other, *, walk_up=False):
237        """Return the relative path to another path identified by the passed
238        arguments.  If the operation is not possible (because this is not
239        related to the other path), raise ValueError.
240
241        The *walk_up* parameter controls whether `..` may be used to resolve
242        the path.
243        """
244        if not isinstance(other, PurePathBase):
245            other = self.with_segments(other)
246        anchor0, parts0 = self._stack
247        anchor1, parts1 = other._stack
248        if anchor0 != anchor1:
249            raise ValueError(f"{self._raw_path!r} and {other._raw_path!r} have different anchors")
250        while parts0 and parts1 and parts0[-1] == parts1[-1]:
251            parts0.pop()
252            parts1.pop()
253        for part in parts1:
254            if not part or part == '.':
255                pass
256            elif not walk_up:
257                raise ValueError(f"{self._raw_path!r} is not in the subpath of {other._raw_path!r}")
258            elif part == '..':
259                raise ValueError(f"'..' segment in {other._raw_path!r} cannot be walked")
260            else:
261                parts0.append('..')
262        return self.with_segments('', *reversed(parts0))
263
264    def is_relative_to(self, other):
265        """Return True if the path is relative to another path or False.
266        """
267        if not isinstance(other, PurePathBase):
268            other = self.with_segments(other)
269        anchor0, parts0 = self._stack
270        anchor1, parts1 = other._stack
271        if anchor0 != anchor1:
272            return False
273        while parts0 and parts1 and parts0[-1] == parts1[-1]:
274            parts0.pop()
275            parts1.pop()
276        for part in parts1:
277            if part and part != '.':
278                return False
279        return True
280
281    @property
282    def parts(self):
283        """An object providing sequence-like access to the
284        components in the filesystem path."""
285        anchor, parts = self._stack
286        if anchor:
287            parts.append(anchor)
288        return tuple(reversed(parts))
289
290    def joinpath(self, *pathsegments):
291        """Combine this path with one or several arguments, and return a
292        new path representing either a subpath (if all arguments are relative
293        paths) or a totally different path (if one of the arguments is
294        anchored).
295        """
296        return self.with_segments(self._raw_path, *pathsegments)
297
298    def __truediv__(self, key):
299        try:
300            return self.with_segments(self._raw_path, key)
301        except TypeError:
302            return NotImplemented
303
304    def __rtruediv__(self, key):
305        try:
306            return self.with_segments(key, self._raw_path)
307        except TypeError:
308            return NotImplemented
309
310    @property
311    def _stack(self):
312        """
313        Split the path into a 2-tuple (anchor, parts), where *anchor* is the
314        uppermost parent of the path (equivalent to path.parents[-1]), and
315        *parts* is a reversed list of parts following the anchor.
316        """
317        split = self.parser.split
318        path = self._raw_path
319        parent, name = split(path)
320        names = []
321        while path != parent:
322            names.append(name)
323            path = parent
324            parent, name = split(path)
325        return path, names
326
327    @property
328    def parent(self):
329        """The logical parent of the path."""
330        path = self._raw_path
331        parent = self.parser.split(path)[0]
332        if path != parent:
333            parent = self.with_segments(parent)
334            parent._resolving = self._resolving
335            return parent
336        return self
337
338    @property
339    def parents(self):
340        """A sequence of this path's logical parents."""
341        split = self.parser.split
342        path = self._raw_path
343        parent = split(path)[0]
344        parents = []
345        while path != parent:
346            parents.append(self.with_segments(parent))
347            path = parent
348            parent = split(path)[0]
349        return tuple(parents)
350
351    def is_absolute(self):
352        """True if the path is absolute (has both a root and, if applicable,
353        a drive)."""
354        return self.parser.isabs(self._raw_path)
355
356    @property
357    def _pattern_str(self):
358        """The path expressed as a string, for use in pattern-matching."""
359        return str(self)
360
361    def match(self, path_pattern, *, case_sensitive=None):
362        """
363        Return True if this path matches the given pattern. If the pattern is
364        relative, matching is done from the right; otherwise, the entire path
365        is matched. The recursive wildcard '**' is *not* supported by this
366        method.
367        """
368        if not isinstance(path_pattern, PurePathBase):
369            path_pattern = self.with_segments(path_pattern)
370        if case_sensitive is None:
371            case_sensitive = _is_case_sensitive(self.parser)
372        sep = path_pattern.parser.sep
373        path_parts = self.parts[::-1]
374        pattern_parts = path_pattern.parts[::-1]
375        if not pattern_parts:
376            raise ValueError("empty pattern")
377        if len(path_parts) < len(pattern_parts):
378            return False
379        if len(path_parts) > len(pattern_parts) and path_pattern.anchor:
380            return False
381        globber = self._globber(sep, case_sensitive)
382        for path_part, pattern_part in zip(path_parts, pattern_parts):
383            match = globber.compile(pattern_part)
384            if match(path_part) is None:
385                return False
386        return True
387
388    def full_match(self, pattern, *, case_sensitive=None):
389        """
390        Return True if this path matches the given glob-style pattern. The
391        pattern is matched against the entire path.
392        """
393        if not isinstance(pattern, PurePathBase):
394            pattern = self.with_segments(pattern)
395        if case_sensitive is None:
396            case_sensitive = _is_case_sensitive(self.parser)
397        globber = self._globber(pattern.parser.sep, case_sensitive, recursive=True)
398        match = globber.compile(pattern._pattern_str)
399        return match(self._pattern_str) is not None
400
401
402
403class PathBase(PurePathBase):
404    """Base class for concrete path objects.
405
406    This class provides dummy implementations for many methods that derived
407    classes can override selectively; the default implementations raise
408    UnsupportedOperation. The most basic methods, such as stat() and open(),
409    directly raise UnsupportedOperation; these basic methods are called by
410    other methods such as is_dir() and read_text().
411
412    The Path class derives this class to implement local filesystem paths.
413    Users may derive their own classes to implement virtual filesystem paths,
414    such as paths in archive files or on remote storage systems.
415    """
416    __slots__ = ()
417
418    # Maximum number of symlinks to follow in resolve()
419    _max_symlinks = 40
420
421    @classmethod
422    def _unsupported_msg(cls, attribute):
423        return f"{cls.__name__}.{attribute} is unsupported"
424
425    def stat(self, *, follow_symlinks=True):
426        """
427        Return the result of the stat() system call on this path, like
428        os.stat() does.
429        """
430        raise UnsupportedOperation(self._unsupported_msg('stat()'))
431
432    def lstat(self):
433        """
434        Like stat(), except if the path points to a symlink, the symlink's
435        status information is returned, rather than its target's.
436        """
437        return self.stat(follow_symlinks=False)
438
439
440    # Convenience functions for querying the stat results
441
442    def exists(self, *, follow_symlinks=True):
443        """
444        Whether this path exists.
445
446        This method normally follows symlinks; to check whether a symlink exists,
447        add the argument follow_symlinks=False.
448        """
449        try:
450            self.stat(follow_symlinks=follow_symlinks)
451        except OSError as e:
452            if not _ignore_error(e):
453                raise
454            return False
455        except ValueError:
456            # Non-encodable path
457            return False
458        return True
459
460    def is_dir(self, *, follow_symlinks=True):
461        """
462        Whether this path is a directory.
463        """
464        try:
465            return S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode)
466        except OSError as e:
467            if not _ignore_error(e):
468                raise
469            # Path doesn't exist or is a broken symlink
470            # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
471            return False
472        except ValueError:
473            # Non-encodable path
474            return False
475
476    def is_file(self, *, follow_symlinks=True):
477        """
478        Whether this path is a regular file (also True for symlinks pointing
479        to regular files).
480        """
481        try:
482            return S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode)
483        except OSError as e:
484            if not _ignore_error(e):
485                raise
486            # Path doesn't exist or is a broken symlink
487            # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
488            return False
489        except ValueError:
490            # Non-encodable path
491            return False
492
493    def is_mount(self):
494        """
495        Check if this path is a mount point
496        """
497        # Need to exist and be a dir
498        if not self.exists() or not self.is_dir():
499            return False
500
501        try:
502            parent_dev = self.parent.stat().st_dev
503        except OSError:
504            return False
505
506        dev = self.stat().st_dev
507        if dev != parent_dev:
508            return True
509        ino = self.stat().st_ino
510        parent_ino = self.parent.stat().st_ino
511        return ino == parent_ino
512
513    def is_symlink(self):
514        """
515        Whether this path is a symbolic link.
516        """
517        try:
518            return S_ISLNK(self.lstat().st_mode)
519        except OSError as e:
520            if not _ignore_error(e):
521                raise
522            # Path doesn't exist
523            return False
524        except ValueError:
525            # Non-encodable path
526            return False
527
528    def is_junction(self):
529        """
530        Whether this path is a junction.
531        """
532        # Junctions are a Windows-only feature, not present in POSIX nor the
533        # majority of virtual filesystems. There is no cross-platform idiom
534        # to check for junctions (using stat().st_mode).
535        return False
536
537    def is_block_device(self):
538        """
539        Whether this path is a block device.
540        """
541        try:
542            return S_ISBLK(self.stat().st_mode)
543        except OSError as e:
544            if not _ignore_error(e):
545                raise
546            # Path doesn't exist or is a broken symlink
547            # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
548            return False
549        except ValueError:
550            # Non-encodable path
551            return False
552
553    def is_char_device(self):
554        """
555        Whether this path is a character device.
556        """
557        try:
558            return S_ISCHR(self.stat().st_mode)
559        except OSError as e:
560            if not _ignore_error(e):
561                raise
562            # Path doesn't exist or is a broken symlink
563            # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
564            return False
565        except ValueError:
566            # Non-encodable path
567            return False
568
569    def is_fifo(self):
570        """
571        Whether this path is a FIFO.
572        """
573        try:
574            return S_ISFIFO(self.stat().st_mode)
575        except OSError as e:
576            if not _ignore_error(e):
577                raise
578            # Path doesn't exist or is a broken symlink
579            # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
580            return False
581        except ValueError:
582            # Non-encodable path
583            return False
584
585    def is_socket(self):
586        """
587        Whether this path is a socket.
588        """
589        try:
590            return S_ISSOCK(self.stat().st_mode)
591        except OSError as e:
592            if not _ignore_error(e):
593                raise
594            # Path doesn't exist or is a broken symlink
595            # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
596            return False
597        except ValueError:
598            # Non-encodable path
599            return False
600
601    def samefile(self, other_path):
602        """Return whether other_path is the same or not as this file
603        (as returned by os.path.samefile()).
604        """
605        st = self.stat()
606        try:
607            other_st = other_path.stat()
608        except AttributeError:
609            other_st = self.with_segments(other_path).stat()
610        return (st.st_ino == other_st.st_ino and
611                st.st_dev == other_st.st_dev)
612
613    def open(self, mode='r', buffering=-1, encoding=None,
614             errors=None, newline=None):
615        """
616        Open the file pointed to by this path and return a file object, as
617        the built-in open() function does.
618        """
619        raise UnsupportedOperation(self._unsupported_msg('open()'))
620
621    def read_bytes(self):
622        """
623        Open the file in bytes mode, read it, and close the file.
624        """
625        with self.open(mode='rb') as f:
626            return f.read()
627
628    def read_text(self, encoding=None, errors=None, newline=None):
629        """
630        Open the file in text mode, read it, and close the file.
631        """
632        with self.open(mode='r', encoding=encoding, errors=errors, newline=newline) as f:
633            return f.read()
634
635    def write_bytes(self, data):
636        """
637        Open the file in bytes mode, write to it, and close the file.
638        """
639        # type-check for the buffer interface before truncating the file
640        view = memoryview(data)
641        with self.open(mode='wb') as f:
642            return f.write(view)
643
644    def write_text(self, data, encoding=None, errors=None, newline=None):
645        """
646        Open the file in text mode, write to it, and close the file.
647        """
648        if not isinstance(data, str):
649            raise TypeError('data must be str, not %s' %
650                            data.__class__.__name__)
651        with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f:
652            return f.write(data)
653
654    def iterdir(self):
655        """Yield path objects of the directory contents.
656
657        The children are yielded in arbitrary order, and the
658        special entries '.' and '..' are not included.
659        """
660        raise UnsupportedOperation(self._unsupported_msg('iterdir()'))
661
662    def _glob_selector(self, parts, case_sensitive, recurse_symlinks):
663        if case_sensitive is None:
664            case_sensitive = _is_case_sensitive(self.parser)
665            case_pedantic = False
666        else:
667            # The user has expressed a case sensitivity choice, but we don't
668            # know the case sensitivity of the underlying filesystem, so we
669            # must use scandir() for everything, including non-wildcard parts.
670            case_pedantic = True
671        recursive = True if recurse_symlinks else _no_recurse_symlinks
672        globber = self._globber(self.parser.sep, case_sensitive, case_pedantic, recursive)
673        return globber.selector(parts)
674
675    def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):
676        """Iterate over this subtree and yield all existing files (of any
677        kind, including directories) matching the given relative pattern.
678        """
679        if not isinstance(pattern, PurePathBase):
680            pattern = self.with_segments(pattern)
681        anchor, parts = pattern._stack
682        if anchor:
683            raise NotImplementedError("Non-relative patterns are unsupported")
684        select = self._glob_selector(parts, case_sensitive, recurse_symlinks)
685        return select(self)
686
687    def rglob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):
688        """Recursively yield all existing files (of any kind, including
689        directories) matching the given relative pattern, anywhere in
690        this subtree.
691        """
692        if not isinstance(pattern, PurePathBase):
693            pattern = self.with_segments(pattern)
694        pattern = '**' / pattern
695        return self.glob(pattern, case_sensitive=case_sensitive, recurse_symlinks=recurse_symlinks)
696
697    def walk(self, top_down=True, on_error=None, follow_symlinks=False):
698        """Walk the directory tree from this directory, similar to os.walk()."""
699        paths = [self]
700        while paths:
701            path = paths.pop()
702            if isinstance(path, tuple):
703                yield path
704                continue
705            dirnames = []
706            filenames = []
707            if not top_down:
708                paths.append((path, dirnames, filenames))
709            try:
710                for child in path.iterdir():
711                    try:
712                        if child.is_dir(follow_symlinks=follow_symlinks):
713                            if not top_down:
714                                paths.append(child)
715                            dirnames.append(child.name)
716                        else:
717                            filenames.append(child.name)
718                    except OSError:
719                        filenames.append(child.name)
720            except OSError as error:
721                if on_error is not None:
722                    on_error(error)
723                if not top_down:
724                    while not isinstance(paths.pop(), tuple):
725                        pass
726                continue
727            if top_down:
728                yield path, dirnames, filenames
729                paths += [path.joinpath(d) for d in reversed(dirnames)]
730
731    def absolute(self):
732        """Return an absolute version of this path
733        No normalization or symlink resolution is performed.
734
735        Use resolve() to resolve symlinks and remove '..' segments.
736        """
737        raise UnsupportedOperation(self._unsupported_msg('absolute()'))
738
739    @classmethod
740    def cwd(cls):
741        """Return a new path pointing to the current working directory."""
742        # We call 'absolute()' rather than using 'os.getcwd()' directly to
743        # enable users to replace the implementation of 'absolute()' in a
744        # subclass and benefit from the new behaviour here. This works because
745        # os.path.abspath('.') == os.getcwd().
746        return cls('').absolute()
747
748    def expanduser(self):
749        """ Return a new path with expanded ~ and ~user constructs
750        (as returned by os.path.expanduser)
751        """
752        raise UnsupportedOperation(self._unsupported_msg('expanduser()'))
753
754    @classmethod
755    def home(cls):
756        """Return a new path pointing to expanduser('~').
757        """
758        return cls("~").expanduser()
759
760    def readlink(self):
761        """
762        Return the path to which the symbolic link points.
763        """
764        raise UnsupportedOperation(self._unsupported_msg('readlink()'))
765    readlink._supported = False
766
767    def resolve(self, strict=False):
768        """
769        Make the path absolute, resolving all symlinks on the way and also
770        normalizing it.
771        """
772        if self._resolving:
773            return self
774        path_root, parts = self._stack
775        path = self.with_segments(path_root)
776        try:
777            path = path.absolute()
778        except UnsupportedOperation:
779            path_tail = []
780        else:
781            path_root, path_tail = path._stack
782            path_tail.reverse()
783
784        # If the user has *not* overridden the `readlink()` method, then symlinks are unsupported
785        # and (in non-strict mode) we can improve performance by not calling `stat()`.
786        querying = strict or getattr(self.readlink, '_supported', True)
787        link_count = 0
788        while parts:
789            part = parts.pop()
790            if not part or part == '.':
791                continue
792            if part == '..':
793                if not path_tail:
794                    if path_root:
795                        # Delete '..' segment immediately following root
796                        continue
797                elif path_tail[-1] != '..':
798                    # Delete '..' segment and its predecessor
799                    path_tail.pop()
800                    continue
801            path_tail.append(part)
802            if querying and part != '..':
803                path = self.with_segments(path_root + self.parser.sep.join(path_tail))
804                path._resolving = True
805                try:
806                    st = path.stat(follow_symlinks=False)
807                    if S_ISLNK(st.st_mode):
808                        # Like Linux and macOS, raise OSError(errno.ELOOP) if too many symlinks are
809                        # encountered during resolution.
810                        link_count += 1
811                        if link_count >= self._max_symlinks:
812                            raise OSError(ELOOP, "Too many symbolic links in path", self._raw_path)
813                        target_root, target_parts = path.readlink()._stack
814                        # If the symlink target is absolute (like '/etc/hosts'), set the current
815                        # path to its uppermost parent (like '/').
816                        if target_root:
817                            path_root = target_root
818                            path_tail.clear()
819                        else:
820                            path_tail.pop()
821                        # Add the symlink target's reversed tail parts (like ['hosts', 'etc']) to
822                        # the stack of unresolved path parts.
823                        parts.extend(target_parts)
824                        continue
825                    elif parts and not S_ISDIR(st.st_mode):
826                        raise NotADirectoryError(ENOTDIR, "Not a directory", self._raw_path)
827                except OSError:
828                    if strict:
829                        raise
830                    else:
831                        querying = False
832        return self.with_segments(path_root + self.parser.sep.join(path_tail))
833
834    def symlink_to(self, target, target_is_directory=False):
835        """
836        Make this path a symlink pointing to the target path.
837        Note the order of arguments (link, target) is the reverse of os.symlink.
838        """
839        raise UnsupportedOperation(self._unsupported_msg('symlink_to()'))
840
841    def hardlink_to(self, target):
842        """
843        Make this path a hard link pointing to the same file as *target*.
844
845        Note the order of arguments (self, target) is the reverse of os.link's.
846        """
847        raise UnsupportedOperation(self._unsupported_msg('hardlink_to()'))
848
849    def touch(self, mode=0o666, exist_ok=True):
850        """
851        Create this file with the given access mode, if it doesn't exist.
852        """
853        raise UnsupportedOperation(self._unsupported_msg('touch()'))
854
855    def mkdir(self, mode=0o777, parents=False, exist_ok=False):
856        """
857        Create a new directory at this given path.
858        """
859        raise UnsupportedOperation(self._unsupported_msg('mkdir()'))
860
861    def rename(self, target):
862        """
863        Rename this path to the target path.
864
865        The target path may be absolute or relative. Relative paths are
866        interpreted relative to the current working directory, *not* the
867        directory of the Path object.
868
869        Returns the new Path instance pointing to the target path.
870        """
871        raise UnsupportedOperation(self._unsupported_msg('rename()'))
872
873    def replace(self, target):
874        """
875        Rename this path to the target path, overwriting if that path exists.
876
877        The target path may be absolute or relative. Relative paths are
878        interpreted relative to the current working directory, *not* the
879        directory of the Path object.
880
881        Returns the new Path instance pointing to the target path.
882        """
883        raise UnsupportedOperation(self._unsupported_msg('replace()'))
884
885    def chmod(self, mode, *, follow_symlinks=True):
886        """
887        Change the permissions of the path, like os.chmod().
888        """
889        raise UnsupportedOperation(self._unsupported_msg('chmod()'))
890
891    def lchmod(self, mode):
892        """
893        Like chmod(), except if the path points to a symlink, the symlink's
894        permissions are changed, rather than its target's.
895        """
896        self.chmod(mode, follow_symlinks=False)
897
898    def unlink(self, missing_ok=False):
899        """
900        Remove this file or link.
901        If the path is a directory, use rmdir() instead.
902        """
903        raise UnsupportedOperation(self._unsupported_msg('unlink()'))
904
905    def rmdir(self):
906        """
907        Remove this directory.  The directory must be empty.
908        """
909        raise UnsupportedOperation(self._unsupported_msg('rmdir()'))
910
911    def owner(self, *, follow_symlinks=True):
912        """
913        Return the login name of the file owner.
914        """
915        raise UnsupportedOperation(self._unsupported_msg('owner()'))
916
917    def group(self, *, follow_symlinks=True):
918        """
919        Return the group name of the file gid.
920        """
921        raise UnsupportedOperation(self._unsupported_msg('group()'))
922
923    @classmethod
924    def from_uri(cls, uri):
925        """Return a new path from the given 'file' URI."""
926        raise UnsupportedOperation(cls._unsupported_msg('from_uri()'))
927
928    def as_uri(self):
929        """Return the path as a URI."""
930        raise UnsupportedOperation(self._unsupported_msg('as_uri()'))
931