1# -*- test-case-name: twisted.test.test_paths -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6Object-oriented filesystem path representation.
7"""
8
9
10import base64
11import errno
12import os
13import sys
14from os import listdir, stat, utime
15from os.path import (
16    abspath,
17    basename,
18    dirname,
19    exists,
20    isabs,
21    join as joinpath,
22    normpath,
23    splitext,
24)
25from stat import (
26    S_IMODE,
27    S_IRGRP,
28    S_IROTH,
29    S_IRUSR,
30    S_ISBLK,
31    S_ISDIR,
32    S_ISREG,
33    S_ISSOCK,
34    S_IWGRP,
35    S_IWOTH,
36    S_IWUSR,
37    S_IXGRP,
38    S_IXOTH,
39    S_IXUSR,
40)
41from typing import IO, Union, cast
42
43from zope.interface import Attribute, Interface, implementer
44
45from twisted.python.compat import cmp, comparable
46from twisted.python.runtime import platform
47from twisted.python.util import FancyEqMixin
48from twisted.python.win32 import (
49    ERROR_DIRECTORY,
50    ERROR_FILE_NOT_FOUND,
51    ERROR_INVALID_NAME,
52    ERROR_PATH_NOT_FOUND,
53    O_BINARY,
54)
55
56# Please keep this as light as possible on other Twisted imports; many, many
57# things import this module, and it would be good if it could easily be
58# modified for inclusion in the standard library.  --glyph
59
60
61_CREATE_FLAGS = os.O_EXCL | os.O_CREAT | os.O_RDWR | O_BINARY
62
63
64def _stub_islink(path):
65    """
66    Always return C{False} if the operating system does not support symlinks.
67
68    @param path: A path string.
69    @type path: L{str}
70
71    @return: C{False}
72    @rtype: L{bool}
73    """
74    return False
75
76
77islink = getattr(os.path, "islink", _stub_islink)
78randomBytes = os.urandom
79armor = base64.urlsafe_b64encode
80
81
82class IFilePath(Interface):
83    """
84    File path object.
85
86    A file path represents a location for a file-like-object and can be
87    organized into a hierarchy; a file path can can children which are
88    themselves file paths.
89
90    A file path has a name which unique identifies it in the context of its
91    parent (if it has one); a file path can not have two children with the same
92    name.  This name is referred to as the file path's "base name".
93
94    A series of such names can be used to locate nested children of a file
95    path; such a series is referred to as the child's "path", relative to the
96    parent.  In this case, each name in the path is referred to as a "path
97    segment"; the child's base name is the segment in the path.
98
99    When representing a file path as a string, a "path separator" is used to
100    delimit the path segments within the string.  For a file system path, that
101    would be C{os.sep}.
102
103    Note that the values of child names may be restricted.  For example, a file
104    system path will not allow the use of the path separator in a name, and
105    certain names (e.g. C{"."} and C{".."}) may be reserved or have special
106    meanings.
107
108    @since: 12.1
109    """
110
111    sep = Attribute("The path separator to use in string representations")
112
113    def child(name):
114        """
115        Obtain a direct child of this file path.  The child may or may not
116        exist.
117
118        @param name: the name of a child of this path. C{name} must be a direct
119            child of this path and may not contain a path separator.
120        @return: the child of this path with the given C{name}.
121        @raise InsecurePath: if C{name} describes a file path that is not a
122            direct child of this file path.
123        """
124
125    def open(mode="r"):
126        """
127        Opens this file path with the given mode.
128
129        @return: a file-like object.
130        @raise Exception: if this file path cannot be opened.
131        """
132
133    def changed():
134        """
135        Clear any cached information about the state of this path on disk.
136        """
137
138    def getsize():
139        """
140        Retrieve the size of this file in bytes.
141
142        @return: the size of the file at this file path in bytes.
143        @raise Exception: if the size cannot be obtained.
144        """
145
146    def getModificationTime():
147        """
148        Retrieve the time of last access from this file.
149
150        @return: a number of seconds from the epoch.
151        @rtype: L{float}
152        """
153
154    def getStatusChangeTime():
155        """
156        Retrieve the time of the last status change for this file.
157
158        @return: a number of seconds from the epoch.
159        @rtype: L{float}
160        """
161
162    def getAccessTime():
163        """
164        Retrieve the time that this file was last accessed.
165
166        @return: a number of seconds from the epoch.
167        @rtype: L{float}
168        """
169
170    def exists():
171        """
172        Check if this file path exists.
173
174        @return: C{True} if the file at this file path exists, C{False}
175            otherwise.
176        @rtype: L{bool}
177        """
178
179    def isdir():
180        """
181        Check if this file path refers to a directory.
182
183        @return: C{True} if the file at this file path is a directory, C{False}
184            otherwise.
185        """
186
187    def isfile():
188        """
189        Check if this file path refers to a regular file.
190
191        @return: C{True} if the file at this file path is a regular file,
192            C{False} otherwise.
193        """
194
195    def children():
196        """
197        List the children of this path object.
198
199        @return: a sequence of the children of the directory at this file path.
200        @raise Exception: if the file at this file path is not a directory.
201        """
202
203    def basename():
204        """
205        Retrieve the final component of the file path's path (everything
206        after the final path separator).
207
208        @return: the base name of this file path.
209        @rtype: L{str}
210        """
211
212    def parent():
213        """
214        A file path for the directory containing the file at this file path.
215        """
216
217    def sibling(name):
218        """
219        A file path for the directory containing the file at this file path.
220
221        @param name: the name of a sibling of this path.  C{name} must be a
222            direct sibling of this path and may not contain a path separator.
223
224        @return: a sibling file path of this one.
225        """
226
227
228class InsecurePath(Exception):
229    """
230    Error that is raised when the path provided to L{FilePath} is invalid.
231    """
232
233
234class LinkError(Exception):
235    """
236    An error with symlinks - either that there are cyclical symlinks or that
237    symlink are not supported on this platform.
238    """
239
240
241class UnlistableError(OSError):
242    """
243    An exception which is used to distinguish between errors which mean 'this
244    is not a directory you can list' and other, more catastrophic errors.
245
246    This error will try to look as much like the original error as possible,
247    while still being catchable as an independent type.
248
249    @ivar originalException: the actual original exception instance.
250    """
251
252    def __init__(self, originalException: OSError):
253        """
254        Create an UnlistableError exception.
255
256        @param originalException: an instance of OSError.
257        """
258        self.__dict__.update(originalException.__dict__)
259        self.originalException = originalException
260
261
262def _secureEnoughString(path):
263    """
264    Compute a string usable as a new, temporary filename.
265
266    @param path: The path that the new temporary filename should be able to be
267        concatenated with.
268
269    @return: A pseudorandom, 16 byte string for use in secure filenames.
270    @rtype: the type of C{path}
271    """
272    secureishString = armor(randomBytes(16))[:16]
273    return _coerceToFilesystemEncoding(path, secureishString)
274
275
276class AbstractFilePath:
277    """
278    Abstract implementation of an L{IFilePath}; must be completed by a
279    subclass.
280
281    This class primarily exists to provide common implementations of certain
282    methods in L{IFilePath}. It is *not* a required parent class for
283    L{IFilePath} implementations, just a useful starting point.
284    """
285
286    def getContent(self):
287        """
288        Retrieve the contents of the file at this path.
289
290        @return: the contents of the file
291        @rtype: L{bytes}
292        """
293        with self.open() as fp:
294            return fp.read()
295
296    def parents(self):
297        """
298        Retrieve an iterator of all the ancestors of this path.
299
300        @return: an iterator of all the ancestors of this path, from the most
301        recent (its immediate parent) to the root of its filesystem.
302        """
303        path = self
304        parent = path.parent()
305        # root.parent() == root, so this means "are we the root"
306        while path != parent:
307            yield parent
308            path = parent
309            parent = parent.parent()
310
311    def children(self):
312        """
313        List the children of this path object.
314
315        @raise OSError: If an error occurs while listing the directory.  If the
316        error is 'serious', meaning that the operation failed due to an access
317        violation, exhaustion of some kind of resource (file descriptors or
318        memory), OSError or a platform-specific variant will be raised.
319
320        @raise UnlistableError: If the inability to list the directory is due
321        to this path not existing or not being a directory, the more specific
322        OSError subclass L{UnlistableError} is raised instead.
323
324        @return: an iterable of all currently-existing children of this object.
325        """
326        try:
327            subnames = self.listdir()
328        except OSError as ose:
329            # Under Python 3.3 and higher on Windows, WindowsError is an
330            # alias for OSError.  OSError has a winerror attribute and an
331            # errno attribute.
332            #
333            # The winerror attribute is bound to the Windows error code while
334            # the errno attribute is bound to a translation of that code to a
335            # perhaps equivalent POSIX error number.
336            #
337            # For further details, refer to:
338            # https://docs.python.org/3/library/exceptions.html#OSError
339            if getattr(ose, "winerror", None) in (
340                ERROR_PATH_NOT_FOUND,
341                ERROR_FILE_NOT_FOUND,
342                ERROR_INVALID_NAME,
343                ERROR_DIRECTORY,
344            ):
345                raise UnlistableError(ose)
346            if ose.errno in (errno.ENOENT, errno.ENOTDIR):
347                raise UnlistableError(ose)
348            # Other possible errors here, according to linux manpages:
349            # EACCES, EMIFLE, ENFILE, ENOMEM.  None of these seem like the
350            # sort of thing which should be handled normally. -glyph
351            raise
352        return [self.child(name) for name in subnames]
353
354    def walk(self, descend=None):
355        """
356        Yield myself, then each of my children, and each of those children's
357        children in turn.
358
359        The optional argument C{descend} is a predicate that takes a FilePath,
360        and determines whether or not that FilePath is traversed/descended
361        into.  It will be called with each path for which C{isdir} returns
362        C{True}.  If C{descend} is not specified, all directories will be
363        traversed (including symbolic links which refer to directories).
364
365        @param descend: A one-argument callable that will return True for
366            FilePaths that should be traversed, False otherwise.
367
368        @return: a generator yielding FilePath-like objects.
369        """
370        yield self
371        if self.isdir():
372            for c in self.children():
373                # we should first see if it's what we want, then we
374                # can walk through the directory
375                if descend is None or descend(c):
376                    for subc in c.walk(descend):
377                        if os.path.realpath(self.path).startswith(
378                            os.path.realpath(subc.path)
379                        ):
380                            raise LinkError("Cycle in file graph.")
381                        yield subc
382                else:
383                    yield c
384
385    def sibling(self, path):
386        """
387        Return a L{FilePath} with the same directory as this instance but with
388        a basename of C{path}.
389
390        @param path: The basename of the L{FilePath} to return.
391        @type path: L{str}
392
393        @return: The sibling path.
394        @rtype: L{FilePath}
395        """
396        return self.parent().child(path)
397
398    def descendant(self, segments):
399        """
400        Retrieve a child or child's child of this path.
401
402        @param segments: A sequence of path segments as L{str} instances.
403
404        @return: A L{FilePath} constructed by looking up the C{segments[0]}
405            child of this path, the C{segments[1]} child of that path, and so
406            on.
407
408        @since: 10.2
409        """
410        path = self
411        for name in segments:
412            path = path.child(name)
413        return path
414
415    def segmentsFrom(self, ancestor):
416        """
417        Return a list of segments between a child and its ancestor.
418
419        For example, in the case of a path X representing /a/b/c/d and a path Y
420        representing /a/b, C{Y.segmentsFrom(X)} will return C{['c',
421        'd']}.
422
423        @param ancestor: an instance of the same class as self, ostensibly an
424        ancestor of self.
425
426        @raise ValueError: If the C{ancestor} parameter is not actually an
427        ancestor, i.e. a path for /x/y/z is passed as an ancestor for /a/b/c/d.
428
429        @return: a list of strs
430        """
431        # this might be an unnecessarily inefficient implementation but it will
432        # work on win32 and for zipfiles; later I will deterimine if the
433        # obvious fast implemenation does the right thing too
434        f = self
435        p = f.parent()
436        segments = []
437        while f != ancestor and p != f:
438            segments[0:0] = [f.basename()]
439            f = p
440            p = p.parent()
441        if f == ancestor and segments:
442            return segments
443        raise ValueError(f"{ancestor!r} not parent of {self!r}")
444
445    # new in 8.0
446    def __hash__(self):
447        """
448        Hash the same as another L{FilePath} with the same path as mine.
449        """
450        return hash((self.__class__, self.path))
451
452    # pending deprecation in 8.0
453    def getmtime(self):
454        """
455        Deprecated.  Use getModificationTime instead.
456        """
457        return int(self.getModificationTime())
458
459    def getatime(self):
460        """
461        Deprecated.  Use getAccessTime instead.
462        """
463        return int(self.getAccessTime())
464
465    def getctime(self):
466        """
467        Deprecated.  Use getStatusChangeTime instead.
468        """
469        return int(self.getStatusChangeTime())
470
471
472class RWX(FancyEqMixin):
473    """
474    A class representing read/write/execute permissions for a single user
475    category (i.e. user/owner, group, or other/world).  Instantiate with
476    three boolean values: readable? writable? executable?.
477
478    @type read: C{bool}
479    @ivar read: Whether permission to read is given
480
481    @type write: C{bool}
482    @ivar write: Whether permission to write is given
483
484    @type execute: C{bool}
485    @ivar execute: Whether permission to execute is given
486
487    @since: 11.1
488    """
489
490    compareAttributes = ("read", "write", "execute")
491
492    def __init__(self, readable, writable, executable):
493        self.read = readable
494        self.write = writable
495        self.execute = executable
496
497    def __repr__(self) -> str:
498        return "RWX(read={}, write={}, execute={})".format(
499            self.read,
500            self.write,
501            self.execute,
502        )
503
504    def shorthand(self):
505        """
506        Returns a short string representing the permission bits.  Looks like
507        part of what is printed by command line utilities such as 'ls -l'
508        (e.g. 'rwx')
509
510        @return: The shorthand string.
511        @rtype: L{str}
512        """
513        returnval = ["r", "w", "x"]
514        i = 0
515        for val in (self.read, self.write, self.execute):
516            if not val:
517                returnval[i] = "-"
518            i += 1
519        return "".join(returnval)
520
521
522class Permissions(FancyEqMixin):
523    """
524    A class representing read/write/execute permissions.  Instantiate with any
525    portion of the file's mode that includes the permission bits.
526
527    @type user: L{RWX}
528    @ivar user: User/Owner permissions
529
530    @type group: L{RWX}
531    @ivar group: Group permissions
532
533    @type other: L{RWX}
534    @ivar other: Other/World permissions
535
536    @since: 11.1
537    """
538
539    compareAttributes = ("user", "group", "other")
540
541    def __init__(self, statModeInt):
542        self.user, self.group, self.other = (
543            RWX(*(statModeInt & bit > 0 for bit in bitGroup))
544            for bitGroup in [
545                [S_IRUSR, S_IWUSR, S_IXUSR],
546                [S_IRGRP, S_IWGRP, S_IXGRP],
547                [S_IROTH, S_IWOTH, S_IXOTH],
548            ]
549        )
550
551    def __repr__(self) -> str:
552        return f"[{str(self.user)} | {str(self.group)} | {str(self.other)}]"
553
554    def shorthand(self):
555        """
556        Returns a short string representing the permission bits.  Looks like
557        what is printed by command line utilities such as 'ls -l'
558        (e.g. 'rwx-wx--x')
559
560        @return: The shorthand string.
561        @rtype: L{str}
562        """
563        return "".join([x.shorthand() for x in (self.user, self.group, self.other)])
564
565
566def _asFilesystemBytes(path: Union[bytes, str], encoding: str = "") -> bytes:
567    """
568    Return C{path} as a string of L{bytes} suitable for use on this system's
569    filesystem.
570
571    @param path: The path to be made suitable.
572    @type path: L{bytes} or L{unicode}
573    @param encoding: The encoding to use if coercing to L{bytes}. If none is
574        given, L{sys.getfilesystemencoding} is used.
575
576    @return: L{bytes}
577    """
578    if isinstance(path, bytes):
579        return path
580    else:
581        if not encoding:
582            encoding = sys.getfilesystemencoding()
583        return path.encode(encoding)
584
585
586def _asFilesystemText(path, encoding=None):
587    """
588    Return C{path} as a string of L{unicode} suitable for use on this system's
589    filesystem.
590
591    @param path: The path to be made suitable.
592    @type path: L{bytes} or L{unicode}
593
594    @param encoding: The encoding to use if coercing to L{unicode}. If none
595        is given, L{sys.getfilesystemencoding} is used.
596
597    @return: L{unicode}
598    """
599    if type(path) == str:
600        return path
601    else:
602        if encoding is None:
603            encoding = sys.getfilesystemencoding()
604        return path.decode(encoding)
605
606
607def _coerceToFilesystemEncoding(path, newpath, encoding=None):
608    """
609    Return a C{newpath} that is suitable for joining to C{path}.
610
611    @param path: The path that it should be suitable for joining to.
612    @param newpath: The new portion of the path to be coerced if needed.
613    @param encoding: If coerced, the encoding that will be used.
614    """
615    if type(path) == bytes:
616        return _asFilesystemBytes(newpath, encoding=encoding)
617    else:
618        return _asFilesystemText(newpath, encoding=encoding)
619
620
621@comparable
622@implementer(IFilePath)
623class FilePath(AbstractFilePath):
624    """
625    I am a path on the filesystem that only permits 'downwards' access.
626
627    Instantiate me with a pathname (for example,
628    FilePath('/home/myuser/public_html')) and I will attempt to only provide
629    access to files which reside inside that path.  I may be a path to a file,
630    a directory, or a file which does not exist.
631
632    The correct way to use me is to instantiate me, and then do ALL filesystem
633    access through me.  In other words, do not import the 'os' module; if you
634    need to open a file, call my 'open' method.  If you need to list a
635    directory, call my 'path' method.
636
637    Even if you pass me a relative path, I will convert that to an absolute
638    path internally.
639
640    The type of C{path} when instantiating decides the mode of the L{FilePath}.
641    That is, C{FilePath(b"/")} will return a L{bytes} mode L{FilePath}, and
642    C{FilePath(u"/")} will return a L{unicode} mode L{FilePath}.
643    C{FilePath("/")} will return a L{bytes} mode L{FilePath} on Python 2, and a
644    L{unicode} mode L{FilePath} on Python 3.
645
646    Methods that return a new L{FilePath} use the type of the given subpath to
647    decide its mode. For example, C{FilePath(b"/").child(u"tmp")} will return a
648    L{unicode} mode L{FilePath}.
649
650    @type alwaysCreate: L{bool}
651    @ivar alwaysCreate: When opening this file, only succeed if the file does
652        not already exist.
653
654    @ivar path: The path from which 'downward' traversal is permitted.
655    """
656
657    _statinfo = None
658    path: Union[bytes, str] = None  # type: ignore[assignment]
659
660    def __init__(self, path, alwaysCreate=False):
661        """
662        Convert a path string to an absolute path if necessary and initialize
663        the L{FilePath} with the result.
664        """
665        self.path = abspath(path)
666        self.alwaysCreate = alwaysCreate
667
668    def __getstate__(self):
669        """
670        Support serialization by discarding cached L{os.stat} results and
671        returning everything else.
672        """
673        d = self.__dict__.copy()
674        if "_statinfo" in d:
675            del d["_statinfo"]
676        return d
677
678    @property
679    def sep(self):
680        """
681        Return a filesystem separator.
682
683        @return: The native filesystem separator.
684        @returntype: The same type as C{self.path}.
685        """
686        return _coerceToFilesystemEncoding(self.path, os.sep)
687
688    def _asBytesPath(self, encoding=None):
689        """
690        Return the path of this L{FilePath} as bytes.
691
692        @param encoding: The encoding to use if coercing to L{bytes}. If none is
693            given, L{sys.getfilesystemencoding} is used.
694
695        @return: L{bytes}
696        """
697        return _asFilesystemBytes(self.path, encoding=encoding)
698
699    def _asTextPath(self, encoding=None):
700        """
701        Return the path of this L{FilePath} as text.
702
703        @param encoding: The encoding to use if coercing to L{unicode}. If none
704            is given, L{sys.getfilesystemencoding} is used.
705
706        @return: L{unicode}
707        """
708        return _asFilesystemText(self.path, encoding=encoding)
709
710    def asBytesMode(self, encoding=None):
711        """
712        Return this L{FilePath} in L{bytes}-mode.
713
714        @param encoding: The encoding to use if coercing to L{bytes}. If none is
715            given, L{sys.getfilesystemencoding} is used.
716
717        @return: L{bytes} mode L{FilePath}
718        """
719        if type(self.path) == str:
720            return self.clonePath(self._asBytesPath(encoding=encoding))
721        return self
722
723    def asTextMode(self, encoding=None):
724        """
725        Return this L{FilePath} in L{unicode}-mode.
726
727        @param encoding: The encoding to use if coercing to L{unicode}. If none
728            is given, L{sys.getfilesystemencoding} is used.
729
730        @return: L{unicode} mode L{FilePath}
731        """
732        if type(self.path) == bytes:
733            return self.clonePath(self._asTextPath(encoding=encoding))
734        return self
735
736    def _getPathAsSameTypeAs(self, pattern):
737        """
738        If C{pattern} is C{bytes}, return L{FilePath.path} as L{bytes}.
739        Otherwise, return L{FilePath.path} as L{unicode}.
740
741        @param pattern: The new element of the path that L{FilePath.path} may
742            need to be coerced to match.
743        """
744        if type(pattern) == bytes:
745            return self._asBytesPath()
746        else:
747            return self._asTextPath()
748
749    def child(self, path):
750        """
751        Create and return a new L{FilePath} representing a path contained by
752        C{self}.
753
754        @param path: The base name of the new L{FilePath}.  If this contains
755            directory separators or parent references it will be rejected.
756        @type path: L{bytes} or L{unicode}
757
758        @raise InsecurePath: If the result of combining this path with C{path}
759            would result in a path which is not a direct child of this path.
760
761        @return: The child path.
762        @rtype: L{FilePath} with a mode equal to the type of C{path}.
763        """
764        colon = _coerceToFilesystemEncoding(path, ":")
765        sep = _coerceToFilesystemEncoding(path, os.sep)
766        ourPath = self._getPathAsSameTypeAs(path)
767
768        if platform.isWindows() and path.count(colon):
769            # Catch paths like C:blah that don't have a slash
770            raise InsecurePath(f"{path!r} contains a colon.")
771
772        norm = normpath(path)
773        if sep in norm:
774            raise InsecurePath(f"{path!r} contains one or more directory separators")
775
776        newpath = abspath(joinpath(ourPath, norm))
777        if not newpath.startswith(ourPath):
778            raise InsecurePath(f"{newpath!r} is not a child of {ourPath}")
779        return self.clonePath(newpath)
780
781    def preauthChild(self, path):
782        """
783        Use me if C{path} might have slashes in it, but you know they're safe.
784
785        @param path: A relative path (ie, a path not starting with C{"/"})
786            which will be interpreted as a child or descendant of this path.
787        @type path: L{bytes} or L{unicode}
788
789        @return: The child path.
790        @rtype: L{FilePath} with a mode equal to the type of C{path}.
791        """
792        ourPath = self._getPathAsSameTypeAs(path)
793
794        newpath = abspath(joinpath(ourPath, normpath(path)))
795        if not newpath.startswith(ourPath):
796            raise InsecurePath(f"{newpath} is not a child of {ourPath}")
797        return self.clonePath(newpath)
798
799    def childSearchPreauth(self, *paths):
800        """
801        Return my first existing child with a name in C{paths}.
802
803        C{paths} is expected to be a list of *pre-secured* path fragments;
804        in most cases this will be specified by a system administrator and not
805        an arbitrary user.
806
807        If no appropriately-named children exist, this will return L{None}.
808
809        @return: L{None} or the child path.
810        @rtype: L{None} or L{FilePath}
811        """
812        for child in paths:
813            p = self._getPathAsSameTypeAs(child)
814            jp = joinpath(p, child)
815            if exists(jp):
816                return self.clonePath(jp)
817
818    def siblingExtensionSearch(self, *exts):
819        """
820        Attempt to return a path with my name, given multiple possible
821        extensions.
822
823        Each extension in C{exts} will be tested and the first path which
824        exists will be returned.  If no path exists, L{None} will be returned.
825        If C{''} is in C{exts}, then if the file referred to by this path
826        exists, C{self} will be returned.
827
828        The extension '*' has a magic meaning, which means "any path that
829        begins with C{self.path + '.'} is acceptable".
830        """
831        for ext in exts:
832            if not ext and self.exists():
833                return self
834
835            p = self._getPathAsSameTypeAs(ext)
836            star = _coerceToFilesystemEncoding(ext, "*")
837            dot = _coerceToFilesystemEncoding(ext, ".")
838
839            if ext == star:
840                basedot = basename(p) + dot
841                for fn in listdir(dirname(p)):
842                    if fn.startswith(basedot):
843                        return self.clonePath(joinpath(dirname(p), fn))
844            p2 = p + ext
845            if exists(p2):
846                return self.clonePath(p2)
847
848    def realpath(self):
849        """
850        Returns the absolute target as a L{FilePath} if self is a link, self
851        otherwise.
852
853        The absolute link is the ultimate file or directory the
854        link refers to (for instance, if the link refers to another link, and
855        another...).  If the filesystem does not support symlinks, or
856        if the link is cyclical, raises a L{LinkError}.
857
858        Behaves like L{os.path.realpath} in that it does not resolve link
859        names in the middle (ex. /x/y/z, y is a link to w - realpath on z
860        will return /x/y/z, not /x/w/z).
861
862        @return: L{FilePath} of the target path.
863        @rtype: L{FilePath}
864        @raises LinkError: if links are not supported or links are cyclical.
865        """
866        if self.islink():
867            result = os.path.realpath(self.path)
868            if result == self.path:
869                raise LinkError("Cyclical link - will loop forever")
870            return self.clonePath(result)
871        return self
872
873    def siblingExtension(self, ext):
874        """
875        Attempt to return a path with my name, given the extension at C{ext}.
876
877        @param ext: File-extension to search for.
878        @type ext: L{bytes} or L{unicode}
879
880        @return: The sibling path.
881        @rtype: L{FilePath} with the same mode as the type of C{ext}.
882        """
883        ourPath = self._getPathAsSameTypeAs(ext)
884        return self.clonePath(ourPath + ext)
885
886    def linkTo(self, linkFilePath):
887        """
888        Creates a symlink to self to at the path in the L{FilePath}
889        C{linkFilePath}.
890
891        Only works on posix systems due to its dependence on
892        L{os.symlink}.  Propagates L{OSError}s up from L{os.symlink} if
893        C{linkFilePath.parent()} does not exist, or C{linkFilePath} already
894        exists.
895
896        @param linkFilePath: a FilePath representing the link to be created.
897        @type linkFilePath: L{FilePath}
898        """
899        os.symlink(self.path, linkFilePath.path)
900
901    def open(self, mode: str = "r") -> IO[bytes]:
902        """
903        Open this file using C{mode} or for writing if C{alwaysCreate} is
904        C{True}.
905
906        In all cases the file is opened in binary mode, so it is not necessary
907        to include C{"b"} in C{mode}.
908
909        @param mode: The mode to open the file in.  Default is C{"r"}.
910        @raises AssertionError: If C{"a"} is included in the mode and
911            C{alwaysCreate} is C{True}.
912        @return: An open file-like object.
913        """
914        if self.alwaysCreate:
915            assert "a" not in mode, (
916                "Appending not supported when " "alwaysCreate == True"
917            )
918            return self.create()
919        # Make sure we open with exactly one "b" in the mode.
920        mode = mode.replace("b", "")
921        return open(self.path, mode + "b")
922
923    # stat methods below
924
925    def restat(self, reraise=True):
926        """
927        Re-calculate cached effects of 'stat'.  To refresh information on this
928        path after you know the filesystem may have changed, call this method.
929
930        @param reraise: a boolean.  If true, re-raise exceptions from
931            L{os.stat}; otherwise, mark this path as not existing, and remove
932            any cached stat information.
933
934        @raise Exception: If C{reraise} is C{True} and an exception occurs
935            while reloading metadata.
936        """
937        try:
938            self._statinfo = stat(self.path)
939        except OSError:
940            self._statinfo = 0
941            if reraise:
942                raise
943
944    def changed(self):
945        """
946        Clear any cached information about the state of this path on disk.
947
948        @since: 10.1.0
949        """
950        self._statinfo = None
951
952    def chmod(self, mode):
953        """
954        Changes the permissions on self, if possible.  Propagates errors from
955        L{os.chmod} up.
956
957        @param mode: integer representing the new permissions desired (same as
958            the command line chmod)
959        @type mode: L{int}
960        """
961        os.chmod(self.path, mode)
962
963    def getsize(self):
964        """
965        Retrieve the size of this file in bytes.
966
967        @return: The size of the file at this file path in bytes.
968        @raise Exception: if the size cannot be obtained.
969        @rtype: L{int}
970        """
971        st = self._statinfo
972        if not st:
973            self.restat()
974            st = self._statinfo
975        return st.st_size
976
977    def getModificationTime(self):
978        """
979        Retrieve the time of last access from this file.
980
981        @return: a number of seconds from the epoch.
982        @rtype: L{float}
983        """
984        st = self._statinfo
985        if not st:
986            self.restat()
987            st = self._statinfo
988        return float(st.st_mtime)
989
990    def getStatusChangeTime(self):
991        """
992        Retrieve the time of the last status change for this file.
993
994        @return: a number of seconds from the epoch.
995        @rtype: L{float}
996        """
997        st = self._statinfo
998        if not st:
999            self.restat()
1000            st = self._statinfo
1001        return float(st.st_ctime)
1002
1003    def getAccessTime(self):
1004        """
1005        Retrieve the time that this file was last accessed.
1006
1007        @return: a number of seconds from the epoch.
1008        @rtype: L{float}
1009        """
1010        st = self._statinfo
1011        if not st:
1012            self.restat()
1013            st = self._statinfo
1014        return float(st.st_atime)
1015
1016    def getInodeNumber(self):
1017        """
1018        Retrieve the file serial number, also called inode number, which
1019        distinguishes this file from all other files on the same device.
1020
1021        @raise NotImplementedError: if the platform is Windows, since the
1022            inode number would be a dummy value for all files in Windows
1023        @return: a number representing the file serial number
1024        @rtype: L{int}
1025        @since: 11.0
1026        """
1027        if platform.isWindows():
1028            raise NotImplementedError
1029
1030        st = self._statinfo
1031        if not st:
1032            self.restat()
1033            st = self._statinfo
1034        return st.st_ino
1035
1036    def getDevice(self):
1037        """
1038        Retrieves the device containing the file.  The inode number and device
1039        number together uniquely identify the file, but the device number is
1040        not necessarily consistent across reboots or system crashes.
1041
1042        @raise NotImplementedError: if the platform is Windows, since the
1043            device number would be 0 for all partitions on a Windows platform
1044
1045        @return: a number representing the device
1046        @rtype: L{int}
1047
1048        @since: 11.0
1049        """
1050        if platform.isWindows():
1051            raise NotImplementedError
1052
1053        st = self._statinfo
1054        if not st:
1055            self.restat()
1056            st = self._statinfo
1057        return st.st_dev
1058
1059    def getNumberOfHardLinks(self):
1060        """
1061        Retrieves the number of hard links to the file.
1062
1063        This count keeps track of how many directories have entries for this
1064        file. If the count is ever decremented to zero then the file itself is
1065        discarded as soon as no process still holds it open.  Symbolic links
1066        are not counted in the total.
1067
1068        @raise NotImplementedError: if the platform is Windows, since Windows
1069            doesn't maintain a link count for directories, and L{os.stat} does
1070            not set C{st_nlink} on Windows anyway.
1071        @return: the number of hard links to the file
1072        @rtype: L{int}
1073        @since: 11.0
1074        """
1075        if platform.isWindows():
1076            raise NotImplementedError
1077
1078        st = self._statinfo
1079        if not st:
1080            self.restat()
1081            st = self._statinfo
1082        return st.st_nlink
1083
1084    def getUserID(self):
1085        """
1086        Returns the user ID of the file's owner.
1087
1088        @raise NotImplementedError: if the platform is Windows, since the UID
1089            is always 0 on Windows
1090        @return: the user ID of the file's owner
1091        @rtype: L{int}
1092        @since: 11.0
1093        """
1094        if platform.isWindows():
1095            raise NotImplementedError
1096
1097        st = self._statinfo
1098        if not st:
1099            self.restat()
1100            st = self._statinfo
1101        return st.st_uid
1102
1103    def getGroupID(self):
1104        """
1105        Returns the group ID of the file.
1106
1107        @raise NotImplementedError: if the platform is Windows, since the GID
1108            is always 0 on windows
1109        @return: the group ID of the file
1110        @rtype: L{int}
1111        @since: 11.0
1112        """
1113        if platform.isWindows():
1114            raise NotImplementedError
1115
1116        st = self._statinfo
1117        if not st:
1118            self.restat()
1119            st = self._statinfo
1120        return st.st_gid
1121
1122    def getPermissions(self):
1123        """
1124        Returns the permissions of the file.  Should also work on Windows,
1125        however, those permissions may not be what is expected in Windows.
1126
1127        @return: the permissions for the file
1128        @rtype: L{Permissions}
1129        @since: 11.1
1130        """
1131        st = self._statinfo
1132        if not st:
1133            self.restat()
1134            st = self._statinfo
1135        return Permissions(S_IMODE(st.st_mode))
1136
1137    def exists(self):
1138        """
1139        Check if this L{FilePath} exists.
1140
1141        @return: C{True} if the stats of C{path} can be retrieved successfully,
1142            C{False} in the other cases.
1143        @rtype: L{bool}
1144        """
1145        if self._statinfo:
1146            return True
1147        else:
1148            self.restat(False)
1149            if self._statinfo:
1150                return True
1151            else:
1152                return False
1153
1154    def isdir(self):
1155        """
1156        Check if this L{FilePath} refers to a directory.
1157
1158        @return: C{True} if this L{FilePath} refers to a directory, C{False}
1159            otherwise.
1160        @rtype: L{bool}
1161        """
1162        st = self._statinfo
1163        if not st:
1164            self.restat(False)
1165            st = self._statinfo
1166            if not st:
1167                return False
1168        return S_ISDIR(st.st_mode)
1169
1170    def isfile(self):
1171        """
1172        Check if this file path refers to a regular file.
1173
1174        @return: C{True} if this L{FilePath} points to a regular file (not a
1175            directory, socket, named pipe, etc), C{False} otherwise.
1176        @rtype: L{bool}
1177        """
1178        st = self._statinfo
1179        if not st:
1180            self.restat(False)
1181            st = self._statinfo
1182            if not st:
1183                return False
1184        return S_ISREG(st.st_mode)
1185
1186    def isBlockDevice(self):
1187        """
1188        Returns whether the underlying path is a block device.
1189
1190        @return: C{True} if it is a block device, C{False} otherwise
1191        @rtype: L{bool}
1192        @since: 11.1
1193        """
1194        st = self._statinfo
1195        if not st:
1196            self.restat(False)
1197            st = self._statinfo
1198            if not st:
1199                return False
1200        return S_ISBLK(st.st_mode)
1201
1202    def isSocket(self):
1203        """
1204        Returns whether the underlying path is a socket.
1205
1206        @return: C{True} if it is a socket, C{False} otherwise
1207        @rtype: L{bool}
1208        @since: 11.1
1209        """
1210        st = self._statinfo
1211        if not st:
1212            self.restat(False)
1213            st = self._statinfo
1214            if not st:
1215                return False
1216        return S_ISSOCK(st.st_mode)
1217
1218    def islink(self):
1219        """
1220        Check if this L{FilePath} points to a symbolic link.
1221
1222        @return: C{True} if this L{FilePath} points to a symbolic link,
1223            C{False} otherwise.
1224        @rtype: L{bool}
1225        """
1226        # We can't use cached stat results here, because that is the stat of
1227        # the destination - (see #1773) which in *every case* but this one is
1228        # the right thing to use.  We could call lstat here and use that, but
1229        # it seems unlikely we'd actually save any work that way.  -glyph
1230        return islink(self.path)
1231
1232    def isabs(self):
1233        """
1234        Check if this L{FilePath} refers to an absolute path.
1235
1236        This always returns C{True}.
1237
1238        @return: C{True}, always.
1239        @rtype: L{bool}
1240        """
1241        return isabs(self.path)
1242
1243    def listdir(self):
1244        """
1245        List the base names of the direct children of this L{FilePath}.
1246
1247        @return: A L{list} of L{bytes}/L{unicode} giving the names of the
1248            contents of the directory this L{FilePath} refers to. These names
1249            are relative to this L{FilePath}.
1250        @rtype: L{list}
1251
1252        @raise OSError: Any exception the platform L{os.listdir} implementation
1253            may raise.
1254        """
1255        return listdir(self.path)
1256
1257    def splitext(self):
1258        """
1259        Split the file path into a pair C{(root, ext)} such that
1260        C{root + ext == path}.
1261
1262        @return: Tuple where the first item is the filename and second item is
1263            the file extension. See Python docs for L{os.path.splitext}.
1264        @rtype: L{tuple}
1265        """
1266        return splitext(self.path)
1267
1268    def __repr__(self) -> str:
1269        return f"FilePath({self.path!r})"
1270
1271    def touch(self):
1272        """
1273        Updates the access and last modification times of the file at this
1274        file path to the current time. Also creates the file if it does not
1275        already exist.
1276
1277        @raise Exception: if unable to create or modify the last modification
1278            time of the file.
1279        """
1280        try:
1281            self.open("a").close()
1282        except OSError:
1283            pass
1284        utime(self.path, None)
1285
1286    def remove(self):
1287        """
1288        Removes the file or directory that is represented by self.  If
1289        C{self.path} is a directory, recursively remove all its children
1290        before removing the directory. If it's a file or link, just delete it.
1291        """
1292        if self.isdir() and not self.islink():
1293            for child in self.children():
1294                child.remove()
1295            os.rmdir(self.path)
1296        else:
1297            os.remove(self.path)
1298        self.changed()
1299
1300    def makedirs(self, ignoreExistingDirectory=False):
1301        """
1302        Create all directories not yet existing in C{path} segments, using
1303        L{os.makedirs}.
1304
1305        @param ignoreExistingDirectory: Don't raise L{OSError} if directory
1306            already exists.
1307        @type ignoreExistingDirectory: L{bool}
1308
1309        @return: L{None}
1310        """
1311        try:
1312            return os.makedirs(self.path)
1313        except OSError as e:
1314            if not (
1315                e.errno == errno.EEXIST and ignoreExistingDirectory and self.isdir()
1316            ):
1317                raise
1318
1319    def globChildren(self, pattern):
1320        """
1321        Assuming I am representing a directory, return a list of FilePaths
1322        representing my children that match the given pattern.
1323
1324        @param pattern: A glob pattern to use to match child paths.
1325        @type pattern: L{unicode} or L{bytes}
1326
1327        @return: A L{list} of matching children.
1328        @rtype: L{list} of L{FilePath}, with the mode of C{pattern}'s type
1329        """
1330        sep = _coerceToFilesystemEncoding(pattern, os.sep)
1331        ourPath = self._getPathAsSameTypeAs(pattern)
1332
1333        import glob
1334
1335        path = ourPath[-1] == sep and ourPath + pattern or sep.join([ourPath, pattern])
1336        return [self.clonePath(p) for p in glob.glob(path)]
1337
1338    def basename(self):
1339        """
1340        Retrieve the final component of the file path's path (everything
1341        after the final path separator).
1342
1343        @return: The final component of the L{FilePath}'s path (Everything
1344            after the final path separator).
1345        @rtype: the same type as this L{FilePath}'s C{path} attribute
1346        """
1347        return basename(self.path)
1348
1349    def dirname(self):
1350        """
1351        Retrieve all of the components of the L{FilePath}'s path except the
1352        last one (everything up to the final path separator).
1353
1354        @return: All of the components of the L{FilePath}'s path except the
1355            last one (everything up to the final path separator).
1356        @rtype: the same type as this L{FilePath}'s C{path} attribute
1357        """
1358        return dirname(self.path)
1359
1360    def parent(self):
1361        """
1362        A file path for the directory containing the file at this file path.
1363
1364        @return: A L{FilePath} representing the path which directly contains
1365            this L{FilePath}.
1366        @rtype: L{FilePath}
1367        """
1368        return self.clonePath(self.dirname())
1369
1370    def setContent(self, content, ext=b".new"):
1371        """
1372        Replace the file at this path with a new file that contains the given
1373        bytes, trying to avoid data-loss in the meanwhile.
1374
1375        On UNIX-like platforms, this method does its best to ensure that by the
1376        time this method returns, either the old contents I{or} the new
1377        contents of the file will be present at this path for subsequent
1378        readers regardless of premature device removal, program crash, or power
1379        loss, making the following assumptions:
1380
1381            - your filesystem is journaled (i.e. your filesystem will not
1382              I{itself} lose data due to power loss)
1383
1384            - your filesystem's C{rename()} is atomic
1385
1386            - your filesystem will not discard new data while preserving new
1387              metadata (see U{http://mjg59.livejournal.com/108257.html} for
1388              more detail)
1389
1390        On most versions of Windows there is no atomic C{rename()} (see
1391        U{http://bit.ly/win32-overwrite} for more information), so this method
1392        is slightly less helpful.  There is a small window where the file at
1393        this path may be deleted before the new file is moved to replace it:
1394        however, the new file will be fully written and flushed beforehand so
1395        in the unlikely event that there is a crash at that point, it should be
1396        possible for the user to manually recover the new version of their
1397        data.  In the future, Twisted will support atomic file moves on those
1398        versions of Windows which I{do} support them: see U{Twisted ticket
1399        3004<http://twistedmatrix.com/trac/ticket/3004>}.
1400
1401        This method should be safe for use by multiple concurrent processes,
1402        but note that it is not easy to predict which process's contents will
1403        ultimately end up on disk if they invoke this method at close to the
1404        same time.
1405
1406        @param content: The desired contents of the file at this path.
1407        @type content: L{bytes}
1408
1409        @param ext: An extension to append to the temporary filename used to
1410            store the bytes while they are being written.  This can be used to
1411            make sure that temporary files can be identified by their suffix,
1412            for cleanup in case of crashes.
1413        @type ext: L{bytes}
1414        """
1415        sib = self.temporarySibling(ext)
1416        with sib.open("w") as f:
1417            f.write(content)
1418        if platform.isWindows() and exists(self.path):
1419            os.unlink(self.path)
1420        os.rename(sib.path, self.asBytesMode().path)
1421
1422    def __cmp__(self, other):
1423        if not isinstance(other, FilePath):
1424            return NotImplemented
1425        return cmp(self.path, other.path)
1426
1427    def createDirectory(self):
1428        """
1429        Create the directory the L{FilePath} refers to.
1430
1431        @see: L{makedirs}
1432
1433        @raise OSError: If the directory cannot be created.
1434        """
1435        os.mkdir(self.path)
1436
1437    def requireCreate(self, val=1):
1438        """
1439        Sets the C{alwaysCreate} variable.
1440
1441        @param val: C{True} or C{False}, indicating whether opening this path
1442            will be required to create the file or not.
1443        @type val: L{bool}
1444
1445        @return: L{None}
1446        """
1447        self.alwaysCreate = val
1448
1449    def create(self) -> IO[bytes]:
1450        """
1451        Exclusively create a file, only if this file previously did not exist.
1452
1453        @return: A file-like object opened from this path.
1454        """
1455        fdint = os.open(self.path, _CREATE_FLAGS)
1456
1457        # XXX TODO: 'name' attribute of returned files is not mutable or
1458        # settable via fdopen, so this file is slightly less functional than the
1459        # one returned from 'open' by default.  send a patch to Python...
1460
1461        return cast(IO[bytes], os.fdopen(fdint, "w+b"))
1462
1463    def temporarySibling(self, extension=b""):
1464        """
1465        Construct a path referring to a sibling of this path.
1466
1467        The resulting path will be unpredictable, so that other subprocesses
1468        should neither accidentally attempt to refer to the same path before it
1469        is created, nor they should other processes be able to guess its name
1470        in advance.
1471
1472        @param extension: A suffix to append to the created filename.  (Note
1473            that if you want an extension with a '.' you must include the '.'
1474            yourself.)
1475        @type extension: L{bytes} or L{unicode}
1476
1477        @return: a path object with the given extension suffix, C{alwaysCreate}
1478            set to True.
1479        @rtype: L{FilePath} with a mode equal to the type of C{extension}
1480        """
1481        ourPath = self._getPathAsSameTypeAs(extension)
1482        sib = self.sibling(
1483            _secureEnoughString(ourPath)
1484            + self.clonePath(ourPath).basename()
1485            + extension
1486        )
1487        sib.requireCreate()
1488        return sib
1489
1490    _chunkSize = 2 ** 2 ** 2 ** 2
1491
1492    def copyTo(self, destination, followLinks=True):
1493        """
1494        Copies self to destination.
1495
1496        If self doesn't exist, an OSError is raised.
1497
1498        If self is a directory, this method copies its children (but not
1499        itself) recursively to destination - if destination does not exist as a
1500        directory, this method creates it.  If destination is a file, an
1501        IOError will be raised.
1502
1503        If self is a file, this method copies it to destination.  If
1504        destination is a file, this method overwrites it.  If destination is a
1505        directory, an IOError will be raised.
1506
1507        If self is a link (and followLinks is False), self will be copied
1508        over as a new symlink with the same target as returned by os.readlink.
1509        That means that if it is absolute, both the old and new symlink will
1510        link to the same thing.  If it's relative, then perhaps not (and
1511        it's also possible that this relative link will be broken).
1512
1513        File/directory permissions and ownership will NOT be copied over.
1514
1515        If followLinks is True, symlinks are followed so that they're treated
1516        as their targets.  In other words, if self is a link, the link's target
1517        will be copied.  If destination is a link, self will be copied to the
1518        destination's target (the actual destination will be destination's
1519        target).  Symlinks under self (if self is a directory) will be
1520        followed and its target's children be copied recursively.
1521
1522        If followLinks is False, symlinks will be copied over as symlinks.
1523
1524        @param destination: the destination (a FilePath) to which self
1525            should be copied
1526        @param followLinks: whether symlinks in self should be treated as links
1527            or as their targets
1528        """
1529        if self.islink() and not followLinks:
1530            os.symlink(os.readlink(self.path), destination.path)
1531            return
1532        # XXX TODO: *thorough* audit and documentation of the exact desired
1533        # semantics of this code.  Right now the behavior of existent
1534        # destination symlinks is convenient, and quite possibly correct, but
1535        # its security properties need to be explained.
1536        if self.isdir():
1537            if not destination.exists():
1538                destination.createDirectory()
1539            for child in self.children():
1540                destChild = destination.child(child.basename())
1541                child.copyTo(destChild, followLinks)
1542        elif self.isfile():
1543            with destination.open("w") as writefile, self.open() as readfile:
1544                while 1:
1545                    # XXX TODO: optionally use os.open, os.read and
1546                    # O_DIRECT and use os.fstatvfs to determine chunk sizes
1547                    # and make *****sure**** copy is page-atomic; the
1548                    # following is good enough for 99.9% of everybody and
1549                    # won't take a week to audit though.
1550                    chunk = readfile.read(self._chunkSize)
1551                    writefile.write(chunk)
1552                    if len(chunk) < self._chunkSize:
1553                        break
1554        elif not self.exists():
1555            raise OSError(errno.ENOENT, "No such file or directory")
1556        else:
1557            # If you see the following message because you want to copy
1558            # symlinks, fifos, block devices, character devices, or unix
1559            # sockets, please feel free to add support to do sensible things in
1560            # reaction to those types!
1561            raise NotImplementedError("Only copying of files and directories supported")
1562
1563    def moveTo(self, destination, followLinks=True):
1564        """
1565        Move self to destination - basically renaming self to whatever
1566        destination is named.
1567
1568        If destination is an already-existing directory,
1569        moves all children to destination if destination is empty.  If
1570        destination is a non-empty directory, or destination is a file, an
1571        OSError will be raised.
1572
1573        If moving between filesystems, self needs to be copied, and everything
1574        that applies to copyTo applies to moveTo.
1575
1576        @param destination: the destination (a FilePath) to which self
1577            should be copied
1578        @param followLinks: whether symlinks in self should be treated as links
1579            or as their targets (only applicable when moving between
1580            filesystems)
1581        """
1582        try:
1583            os.rename(self._getPathAsSameTypeAs(destination.path), destination.path)
1584        except OSError as ose:
1585            if ose.errno == errno.EXDEV:
1586                # man 2 rename, ubuntu linux 5.10 "breezy":
1587
1588                #   oldpath and newpath are not on the same mounted filesystem.
1589                #   (Linux permits a filesystem to be mounted at multiple
1590                #   points, but rename(2) does not work across different mount
1591                #   points, even if the same filesystem is mounted on both.)
1592
1593                # that means it's time to copy trees of directories!
1594                secsib = destination.temporarySibling()
1595                self.copyTo(secsib, followLinks)  # slow
1596                secsib.moveTo(destination, followLinks)  # visible
1597
1598                # done creating new stuff.  let's clean me up.
1599                mysecsib = self.temporarySibling()
1600                self.moveTo(mysecsib, followLinks)  # visible
1601                mysecsib.remove()  # slow
1602            else:
1603                raise
1604        else:
1605            self.changed()
1606            destination.changed()
1607
1608
1609FilePath.clonePath = FilePath  # type: ignore[attr-defined]
1610