Repository: twisted/twisted

Issue:
FilePath and friends should have type annotations
Particularly given the text/bytes-mode distinction of how they can work, we should really include type hints with them.


Discussion:

Resolving patch:
diff --git a/src/twisted/internet/address.py b/src/twisted/internet/address.py
--- a/src/twisted/internet/address.py
+++ b/src/twisted/internet/address.py
@@ -147,9 +147,8 @@ def __eq__(self, other: object) -> bool:
 
     def __repr__(self) -> str:
         name = self.name
-        if name:
-            name = _coerceToFilesystemEncoding("", self.name)
-        return f"UNIXAddress({name!r})"
+        show = _coerceToFilesystemEncoding("", name) if name is not None else None
+        return f"UNIXAddress({show!r})"
 
     def __hash__(self):
         if self.name is None:
diff --git a/src/twisted/python/filepath.py b/src/twisted/python/filepath.py
--- a/src/twisted/python/filepath.py
+++ b/src/twisted/python/filepath.py
@@ -6,6 +6,7 @@
 Object-oriented filesystem path representation.
 """
 
+from __future__ import annotations
 
 import base64
 import errno
@@ -18,6 +19,7 @@
     dirname,
     exists,
     isabs,
+    islink,
     join as joinpath,
     normpath,
     splitext,
@@ -38,10 +40,28 @@
     S_IXOTH,
     S_IXUSR,
 )
-from typing import IO, Union, cast
+from typing import (
+    IO,
+    TYPE_CHECKING,
+    AnyStr,
+    Callable,
+    Dict,
+    Generic,
+    Iterable,
+    List,
+    Optional,
+    Sequence,
+    Tuple,
+    TypeVar,
+    Union,
+    cast,
+    overload,
+)
 
 from zope.interface import Attribute, Interface, implementer
 
+from typing_extensions import Literal
+
 from twisted.python.compat import cmp, comparable
 from twisted.python.runtime import platform
 from twisted.python.util import FancyEqMixin
@@ -59,22 +79,9 @@
 
 
 _CREATE_FLAGS = os.O_EXCL | os.O_CREAT | os.O_RDWR | O_BINARY
+_Self = TypeVar("_Self", bound="AbstractFilePath")
 
 
-def _stub_islink(path):
-    """
-    Always return C{False} if the operating system does not support symlinks.
-
-    @param path: A path string.
-    @type path: L{str}
-
-    @return: C{False}
-    @rtype: L{bool}
-    """
-    return False
-
-
-islink = getattr(os.path, "islink", _stub_islink)
 randomBytes = os.urandom
 armor = base64.urlsafe_b64encode
 
@@ -110,7 +117,7 @@ class IFilePath(Interface):
 
     sep = Attribute("The path separator to use in string representations")
 
-    def child(name):
+    def child(name: AnyStr) -> IFilePath:
         """
         Obtain a direct child of this file path.  The child may or may not
         exist.
@@ -122,7 +129,7 @@ def child(name):
             direct child of this file path.
         """
 
-    def open(mode="r"):
+    def open(mode: FileMode = "r") -> IO[bytes]:
         """
         Opens this file path with the given mode.
 
@@ -130,12 +137,12 @@ def open(mode="r"):
         @raise Exception: if this file path cannot be opened.
         """
 
-    def changed():
+    def changed() -> None:
         """
         Clear any cached information about the state of this path on disk.
         """
 
-    def getsize():
+    def getsize() -> int:
         """
         Retrieve the size of this file in bytes.
 
@@ -143,7 +150,7 @@ def getsize():
         @raise Exception: if the size cannot be obtained.
         """
 
-    def getModificationTime():
+    def getModificationTime() -> float:
         """
         Retrieve the time of last access from this file.
 
@@ -151,7 +158,7 @@ def getModificationTime():
         @rtype: L{float}
         """
 
-    def getStatusChangeTime():
+    def getStatusChangeTime() -> float:
         """
         Retrieve the time of the last status change for this file.
 
@@ -159,7 +166,7 @@ def getStatusChangeTime():
         @rtype: L{float}
         """
 
-    def getAccessTime():
+    def getAccessTime() -> float:
         """
         Retrieve the time that this file was last accessed.
 
@@ -167,7 +174,7 @@ def getAccessTime():
         @rtype: L{float}
         """
 
-    def exists():
+    def exists() -> bool:
         """
         Check if this file path exists.
 
@@ -176,7 +183,7 @@ def exists():
         @rtype: L{bool}
         """
 
-    def isdir():
+    def isdir() -> bool:
         """
         Check if this file path refers to a directory.
 
@@ -184,7 +191,7 @@ def isdir():
             otherwise.
         """
 
-    def isfile():
+    def isfile() -> bool:
         """
         Check if this file path refers to a regular file.
 
@@ -192,7 +199,7 @@ def isfile():
             C{False} otherwise.
         """
 
-    def children():
+    def children() -> Iterable[IFilePath]:
         """
         List the children of this path object.
 
@@ -200,21 +207,24 @@ def children():
         @raise Exception: if the file at this file path is not a directory.
         """
 
-    def basename():
+    def basename() -> Union[str, bytes]:
         """
-        Retrieve the final component of the file path's path (everything
-        after the final path separator).
+        Retrieve the final component of the file path's path (everything after
+        the final path separator).
+
+        @note: In implementors, the return type should be generic, i.e.
+            C{AbstractFilePath[str].basename()} is a C{str}.  However,
+            L{Interface} objects cannot be generic as of this writing.
 
         @return: the base name of this file path.
-        @rtype: L{str}
         """
 
-    def parent():
+    def parent() -> IFilePath:
         """
         A file path for the directory containing the file at this file path.
         """
 
-    def sibling(name):
+    def sibling(name: AnyStr) -> IFilePath:
         """
         A file path for the directory containing the file at this file path.
 
@@ -273,7 +283,11 @@ def _secureEnoughString(path):
     return _coerceToFilesystemEncoding(path, secureishString)
 
 
-class AbstractFilePath:
+OtherAnyStr = TypeVar("OtherAnyStr", str, bytes)
+FileMode = Literal["r", "w", "a", "r+", "w+", "a+"]
+
+
+class AbstractFilePath(Generic[AnyStr]):
     """
     Abstract implementation of an L{IFilePath}; must be completed by a
     subclass.
@@ -281,9 +295,75 @@ class AbstractFilePath:
     This class primarily exists to provide common implementations of certain
     methods in L{IFilePath}. It is *not* a required parent class for
     L{IFilePath} implementations, just a useful starting point.
+
+    @ivar path: Subclasses must set this variable.
     """
 
-    def getContent(self):
+    Selfish = TypeVar("Selfish", bound="AbstractFilePath[AnyStr]")
+
+    path: AnyStr
+
+    def getAccessTime(self) -> float:
+        """
+        Subclasses must implement this.
+
+        @see: L{FilePath.getAccessTime}
+        """
+        raise NotImplementedError()
+
+    def getModificationTime(self) -> float:
+        """
+        Subclasses must implement this.
+
+        @see: L{FilePath.getModificationTime}
+        """
+        raise NotImplementedError()
+
+    def getStatusChangeTime(self) -> float:
+        """
+        Subclasses must implement this.
+
+        @see: L{FilePath.getStatusChangeTime}
+        """
+        raise NotImplementedError()
+
+    def open(self, mode: FileMode = "r") -> IO[bytes]:
+        """
+        Subclasses must implement this.
+        """
+        raise NotImplementedError()
+
+    def isdir(self) -> bool:
+        """
+        Subclasses must implement this.
+        """
+        raise NotImplementedError()
+
+    def basename(self) -> AnyStr:
+        """
+        Subclasses must implement this.
+        """
+        raise NotImplementedError()
+
+    def parent(self) -> AbstractFilePath[AnyStr]:
+        """
+        Subclasses must implement this.
+        """
+        raise NotImplementedError()
+
+    def listdir(self) -> List[AnyStr]:
+        """
+        Subclasses must implement this.
+        """
+        raise NotImplementedError()
+
+    def child(self, path: OtherAnyStr) -> AbstractFilePath[OtherAnyStr]:
+        """
+        Subclasses must implement this.
+        """
+        raise NotImplementedError()
+
+    def getContent(self) -> bytes:
         """
         Retrieve the contents of the file at this path.
 
@@ -293,7 +373,7 @@ def getContent(self):
         with self.open() as fp:
             return fp.read()
 
-    def parents(self):
+    def parents(self) -> Iterable[AbstractFilePath[AnyStr]]:
         """
         Retrieve an iterator of all the ancestors of this path.
 
@@ -308,7 +388,7 @@ def parents(self):
             path = parent
             parent = parent.parent()
 
-    def children(self):
+    def children(self: _Self) -> Iterable[_Self]:
         """
         List the children of this path object.
 
@@ -324,7 +404,7 @@ def children(self):
         @return: an iterable of all currently-existing children of this object.
         """
         try:
-            subnames = self.listdir()
+            subnames: List[AnyStr] = self.listdir()
         except OSError as ose:
             # Under Python 3.3 and higher on Windows, WindowsError is an
             # alias for OSError.  OSError has a winerror attribute and an
@@ -349,9 +429,19 @@ def children(self):
             # EACCES, EMIFLE, ENFILE, ENOMEM.  None of these seem like the
             # sort of thing which should be handled normally. -glyph
             raise
-        return [self.child(name) for name in subnames]
-
-    def walk(self, descend=None):
+        result = []
+        for name in subnames:
+            # It's not possible to tell mypy that child/clone etc must be
+            # overridden to return respecializable forms of _Self, but they
+            # must, so we will say that they are.
+            child: _Self = self.child(name)  # type:ignore[assignment]
+            result.append(child)
+        return result
+
+    def walk(
+        self: _Self,
+        descend: Optional[Callable[[_Self], bool]] = None,
+    ) -> Iterable[_Self]:
         """
         Yield myself, then each of my children, and each of those children's
         children in turn.
@@ -382,11 +472,15 @@ def walk(self, descend=None):
                 else:
                     yield c
 
-    def sibling(self, path):
+    def sibling(self: _Self, path: OtherAnyStr) -> AbstractFilePath[OtherAnyStr]:
         """
         Return a L{FilePath} with the same directory as this instance but with
         a basename of C{path}.
 
+        @note: for type-checking, subclasses should override this signature to
+            make it clear that it returns the subclass and not
+            L{AbstractFilePath}.
+
         @param path: The basename of the L{FilePath} to return.
         @type path: L{str}
 
@@ -395,10 +489,16 @@ def sibling(self, path):
         """
         return self.parent().child(path)
 
-    def descendant(self, segments):
+    def descendant(
+        self, segments: Sequence[OtherAnyStr]
+    ) -> AbstractFilePath[OtherAnyStr]:
         """
         Retrieve a child or child's child of this path.
 
+        @note: for type-checking, subclasses should override this signature to
+            make it clear that it returns the subclass and not
+            L{AbstractFilePath}.
+
         @param segments: A sequence of path segments as L{str} instances.
 
         @return: A L{FilePath} constructed by looking up the C{segments[0]}
@@ -407,12 +507,12 @@ def descendant(self, segments):
 
         @since: 10.2
         """
-        path = self
+        path: AbstractFilePath[OtherAnyStr] = self  # type:ignore[assignment]
         for name in segments:
             path = path.child(name)
         return path
 
-    def segmentsFrom(self, ancestor):
+    def segmentsFrom(self: _Self, ancestor: _Self) -> List[AnyStr]:
         """
         Return a list of segments between a child and its ancestor.
 
@@ -432,37 +532,37 @@ def segmentsFrom(self, ancestor):
         # work on win32 and for zipfiles; later I will deterimine if the
         # obvious fast implemenation does the right thing too
         f = self
-        p = f.parent()
-        segments = []
+        p: _Self = f.parent()  # type:ignore[assignment]
+        segments: List[AnyStr] = []
         while f != ancestor and p != f:
             segments[0:0] = [f.basename()]
             f = p
-            p = p.parent()
+            p = p.parent()  # type:ignore[assignment]
         if f == ancestor and segments:
             return segments
         raise ValueError(f"{ancestor!r} not parent of {self!r}")
 
     # new in 8.0
-    def __hash__(self):
+    def __hash__(self) -> int:
         """
-        Hash the same as another L{FilePath} with the same path as mine.
+        Hash the same as another L{AbstractFilePath} with the same path as mine.
         """
         return hash((self.__class__, self.path))
 
     # pending deprecation in 8.0
-    def getmtime(self):
+    def getmtime(self) -> int:
         """
         Deprecated.  Use getModificationTime instead.
         """
         return int(self.getModificationTime())
 
-    def getatime(self):
+    def getatime(self) -> int:
         """
         Deprecated.  Use getAccessTime instead.
         """
         return int(self.getAccessTime())
 
-    def getctime(self):
+    def getctime(self) -> int:
         """
         Deprecated.  Use getStatusChangeTime instead.
         """
@@ -489,7 +589,7 @@ class RWX(FancyEqMixin):
 
     compareAttributes = ("read", "write", "execute")
 
-    def __init__(self, readable, writable, executable):
+    def __init__(self, readable: bool, writable: bool, executable: bool) -> None:
         self.read = readable
         self.write = writable
         self.execute = executable
@@ -501,7 +601,7 @@ def __repr__(self) -> str:
             self.execute,
         )
 
-    def shorthand(self):
+    def shorthand(self) -> str:
         """
         Returns a short string representing the permission bits.  Looks like
         part of what is printed by command line utilities such as 'ls -l'
@@ -538,7 +638,7 @@ class Permissions(FancyEqMixin):
 
     compareAttributes = ("user", "group", "other")
 
-    def __init__(self, statModeInt):
+    def __init__(self, statModeInt: int) -> None:
         self.user, self.group, self.other = (
             RWX(*(statModeInt & bit > 0 for bit in bitGroup))
             for bitGroup in [
@@ -551,7 +651,7 @@ def __init__(self, statModeInt):
     def __repr__(self) -> str:
         return f"[{str(self.user)} | {str(self.group)} | {str(self.other)}]"
 
-    def shorthand(self):
+    def shorthand(self) -> str:
         """
         Returns a short string representing the permission bits.  Looks like
         what is printed by command line utilities such as 'ls -l'
@@ -563,7 +663,7 @@ def shorthand(self):
         return "".join([x.shorthand() for x in (self.user, self.group, self.other)])
 
 
-def _asFilesystemBytes(path: Union[bytes, str], encoding: str = "") -> bytes:
+def _asFilesystemBytes(path: Union[bytes, str], encoding: Optional[str] = "") -> bytes:
     """
     Return C{path} as a string of L{bytes} suitable for use on this system's
     filesystem.
@@ -580,10 +680,10 @@ def _asFilesystemBytes(path: Union[bytes, str], encoding: str = "") -> bytes:
     else:
         if not encoding:
             encoding = sys.getfilesystemencoding()
-        return path.encode(encoding)
+        return path.encode(encoding, errors="surrogateescape")
 
 
-def _asFilesystemText(path, encoding=None):
+def _asFilesystemText(path: Union[bytes, str], encoding: Optional[str] = None) -> str:
     """
     Return C{path} as a string of L{unicode} suitable for use on this system's
     filesystem.
@@ -596,15 +696,17 @@ def _asFilesystemText(path, encoding=None):
 
     @return: L{unicode}
     """
-    if type(path) == str:
+    if isinstance(path, str):
         return path
     else:
         if encoding is None:
             encoding = sys.getfilesystemencoding()
-        return path.decode(encoding)
+        return path.decode(encoding, errors="surrogateescape")
 
 
-def _coerceToFilesystemEncoding(path, newpath, encoding=None):
+def _coerceToFilesystemEncoding(
+    path: AnyStr, newpath: Union[bytes, str], encoding: Optional[str] = None
+) -> AnyStr:
     """
     Return a C{newpath} that is suitable for joining to C{path}.
 
@@ -612,7 +714,7 @@ def _coerceToFilesystemEncoding(path, newpath, encoding=None):
     @param newpath: The new portion of the path to be coerced if needed.
     @param encoding: If coerced, the encoding that will be used.
     """
-    if type(path) == bytes:
+    if isinstance(path, bytes):
         return _asFilesystemBytes(newpath, encoding=encoding)
     else:
         return _asFilesystemText(newpath, encoding=encoding)
@@ -620,7 +722,7 @@ def _coerceToFilesystemEncoding(path, newpath, encoding=None):
 
 @comparable
 @implementer(IFilePath)
-class FilePath(AbstractFilePath):
+class FilePath(AbstractFilePath[AnyStr]):
     """
     I am a path on the filesystem that only permits 'downwards' access.
 
@@ -655,9 +757,9 @@ class FilePath(AbstractFilePath):
     """
 
     _statinfo = None
-    path: Union[bytes, str] = None  # type: ignore[assignment]
+    path: AnyStr
 
-    def __init__(self, path, alwaysCreate=False):
+    def __init__(self, path: AnyStr, alwaysCreate: bool = False) -> None:
         """
         Convert a path string to an absolute path if necessary and initialize
         the L{FilePath} with the result.
@@ -665,7 +767,46 @@ def __init__(self, path, alwaysCreate=False):
         self.path = abspath(path)
         self.alwaysCreate = alwaysCreate
 
-    def __getstate__(self):
+    if TYPE_CHECKING:
+
+        def sibling(self: _Self, path: OtherAnyStr) -> FilePath[OtherAnyStr]:
+            ...
+
+        def descendant(self, segments: Sequence[OtherAnyStr]) -> FilePath[OtherAnyStr]:
+            ...
+
+        def parents(self) -> Iterable[FilePath[AnyStr]]:
+            ...
+
+        # provided by @comparable
+        def __gt__(self, other: object) -> bool:
+            ...
+
+        def __ge__(self, other: object) -> bool:
+            ...
+
+        def __lt__(self, other: object) -> bool:
+            ...
+
+        def __le__(self, other: object) -> bool:
+            ...
+
+        def __eq__(self, other: object) -> bool:
+            ...
+
+        def __ne__(self, other: object) -> bool:
+            ...
+
+    def clonePath(
+        self, path: OtherAnyStr, alwaysCreate: bool = False
+    ) -> FilePath[OtherAnyStr]:
+        """
+        Make an object of the same type as this FilePath, but with path of
+        C{path}.
+        """
+        return FilePath(path)
+
+    def __getstate__(self) -> Dict[str, object]:
         """
         Support serialization by discarding cached L{os.stat} results and
         returning everything else.
@@ -676,7 +817,7 @@ def __getstate__(self):
         return d
 
     @property
-    def sep(self):
+    def sep(self) -> AnyStr:
         """
         Return a filesystem separator.
 
@@ -685,7 +826,7 @@ def sep(self):
         """
         return _coerceToFilesystemEncoding(self.path, os.sep)
 
-    def _asBytesPath(self, encoding=None):
+    def _asBytesPath(self, encoding: Optional[str] = None) -> bytes:
         """
         Return the path of this L{FilePath} as bytes.
 
@@ -696,7 +837,7 @@ def _asBytesPath(self, encoding=None):
         """
         return _asFilesystemBytes(self.path, encoding=encoding)
 
-    def _asTextPath(self, encoding=None):
+    def _asTextPath(self, encoding: Optional[str] = None) -> str:
         """
         Return the path of this L{FilePath} as text.
 
@@ -707,7 +848,7 @@ def _asTextPath(self, encoding=None):
         """
         return _asFilesystemText(self.path, encoding=encoding)
 
-    def asBytesMode(self, encoding=None):
+    def asBytesMode(self, encoding: Optional[str] = None) -> FilePath[bytes]:
         """
         Return this L{FilePath} in L{bytes}-mode.
 
@@ -716,11 +857,11 @@ def asBytesMode(self, encoding=None):
 
         @return: L{bytes} mode L{FilePath}
         """
-        if type(self.path) == str:
+        if isinstance(self.path, str):
             return self.clonePath(self._asBytesPath(encoding=encoding))
         return self
 
-    def asTextMode(self, encoding=None):
+    def asTextMode(self, encoding: Optional[str] = None) -> FilePath[str]:
         """
         Return this L{FilePath} in L{unicode}-mode.
 
@@ -729,11 +870,11 @@ def asTextMode(self, encoding=None):
 
         @return: L{unicode} mode L{FilePath}
         """
-        if type(self.path) == bytes:
+        if isinstance(self.path, bytes):
             return self.clonePath(self._asTextPath(encoding=encoding))
         return self
 
-    def _getPathAsSameTypeAs(self, pattern):
+    def _getPathAsSameTypeAs(self, pattern: OtherAnyStr) -> OtherAnyStr:
         """
         If C{pattern} is C{bytes}, return L{FilePath.path} as L{bytes}.
         Otherwise, return L{FilePath.path} as L{unicode}.
@@ -741,12 +882,12 @@ def _getPathAsSameTypeAs(self, pattern):
         @param pattern: The new element of the path that L{FilePath.path} may
             need to be coerced to match.
         """
-        if type(pattern) == bytes:
+        if isinstance(pattern, bytes):
             return self._asBytesPath()
         else:
             return self._asTextPath()
 
-    def child(self, path):
+    def child(self, path: OtherAnyStr) -> FilePath[OtherAnyStr]:
         """
         Create and return a new L{FilePath} representing a path contained by
         C{self}.
@@ -775,10 +916,10 @@ def child(self, path):
 
         newpath = abspath(joinpath(ourPath, norm))
         if not newpath.startswith(ourPath):
-            raise InsecurePath(f"{newpath!r} is not a child of {ourPath}")
+            raise InsecurePath(f"{newpath!r} is not a child of {ourPath!r}")
         return self.clonePath(newpath)
 
-    def preauthChild(self, path):
+    def preauthChild(self, path: OtherAnyStr) -> FilePath[OtherAnyStr]:
         """
         Use me if C{path} might have slashes in it, but you know they're safe.
 
@@ -793,10 +934,12 @@ def preauthChild(self, path):
 
         newpath = abspath(joinpath(ourPath, normpath(path)))
         if not newpath.startswith(ourPath):
-            raise InsecurePath(f"{newpath} is not a child of {ourPath}")
+            raise InsecurePath(f"{newpath!r} is not a child of {ourPath!r}")
         return self.clonePath(newpath)
 
-    def childSearchPreauth(self, *paths):
+    def childSearchPreauth(
+        self, *paths: OtherAnyStr
+    ) -> Optional[FilePath[OtherAnyStr]]:
         """
         Return my first existing child with a name in C{paths}.
 
@@ -814,8 +957,11 @@ def childSearchPreauth(self, *paths):
             jp = joinpath(p, child)
             if exists(jp):
                 return self.clonePath(jp)
+        return None
 
-    def siblingExtensionSearch(self, *exts):
+    def siblingExtensionSearch(
+        self, *exts: OtherAnyStr
+    ) -> Optional[FilePath[OtherAnyStr]]:
         """
         Attempt to return a path with my name, given multiple possible
         extensions.
@@ -830,7 +976,7 @@ def siblingExtensionSearch(self, *exts):
         """
         for ext in exts:
             if not ext and self.exists():
-                return self
+                return self.clonePath(self._getPathAsSameTypeAs(ext))
 
             p = self._getPathAsSameTypeAs(ext)
             star = _coerceToFilesystemEncoding(ext, "*")
@@ -844,8 +990,9 @@ def siblingExtensionSearch(self, *exts):
             p2 = p + ext
             if exists(p2):
                 return self.clonePath(p2)
+        return None
 
-    def realpath(self):
+    def realpath(self) -> FilePath[AnyStr]:
         """
         Returns the absolute target as a L{FilePath} if self is a link, self
         otherwise.
@@ -870,7 +1017,7 @@ def realpath(self):
             return self.clonePath(result)
         return self
 
-    def siblingExtension(self, ext):
+    def siblingExtension(self, ext: OtherAnyStr) -> FilePath[OtherAnyStr]:
         """
         Attempt to return a path with my name, given the extension at C{ext}.
 
@@ -883,7 +1030,7 @@ def siblingExtension(self, ext):
         ourPath = self._getPathAsSameTypeAs(ext)
         return self.clonePath(ourPath + ext)
 
-    def linkTo(self, linkFilePath):
+    def linkTo(self, linkFilePath: FilePath[AnyStr]) -> None:
         """
         Creates a symlink to self to at the path in the L{FilePath}
         C{linkFilePath}.
@@ -898,7 +1045,7 @@ def linkTo(self, linkFilePath):
         """
         os.symlink(self.path, linkFilePath.path)
 
-    def open(self, mode: str = "r") -> IO[bytes]:
+    def open(self, mode: FileMode = "r") -> IO[bytes]:
         """
         Open this file using C{mode} or for writing if C{alwaysCreate} is
         C{True}.
@@ -917,12 +1064,11 @@ def open(self, mode: str = "r") -> IO[bytes]:
             )
             return self.create()
         # Make sure we open with exactly one "b" in the mode.
-        mode = mode.replace("b", "")
-        return open(self.path, mode + "b")
+        return open(self.path, mode.replace("b", "") + "b")
 
     # stat methods below
 
-    def restat(self, reraise=True):
+    def restat(self, reraise: bool = True) -> None:
         """
         Re-calculate cached effects of 'stat'.  To refresh information on this
         path after you know the filesystem may have changed, call this method.
@@ -937,11 +1083,11 @@ def restat(self, reraise=True):
         try:
             self._statinfo = stat(self.path)
         except OSError:
-            self._statinfo = 0
+            self._statinfo = None
             if reraise:
                 raise
 
-    def changed(self):
+    def changed(self) -> None:
         """
         Clear any cached information about the state of this path on disk.
 
@@ -949,7 +1095,7 @@ def changed(self):
         """
         self._statinfo = None
 
-    def chmod(self, mode):
+    def chmod(self, mode: int) -> None:
         """
         Changes the permissions on self, if possible.  Propagates errors from
         L{os.chmod} up.
@@ -960,7 +1106,7 @@ def chmod(self, mode):
         """
         os.chmod(self.path, mode)
 
-    def getsize(self):
+    def getsize(self) -> int:
         """
         Retrieve the size of this file in bytes.
 
@@ -972,9 +1118,10 @@ def getsize(self):
         if not st:
             self.restat()
             st = self._statinfo
+        assert st is not None
         return st.st_size
 
-    def getModificationTime(self):
+    def getModificationTime(self) -> float:
         """
         Retrieve the time of last access from this file.
 
@@ -985,9 +1132,10 @@ def getModificationTime(self):
         if not st:
             self.restat()
             st = self._statinfo
+        assert st is not None
         return float(st.st_mtime)
 
-    def getStatusChangeTime(self):
+    def getStatusChangeTime(self) -> float:
         """
         Retrieve the time of the last status change for this file.
 
@@ -998,9 +1146,10 @@ def getStatusChangeTime(self):
         if not st:
             self.restat()
             st = self._statinfo
+        assert st is not None
         return float(st.st_ctime)
 
-    def getAccessTime(self):
+    def getAccessTime(self) -> float:
         """
         Retrieve the time that this file was last accessed.
 
@@ -1011,9 +1160,10 @@ def getAccessTime(self):
         if not st:
             self.restat()
             st = self._statinfo
+        assert st is not None
         return float(st.st_atime)
 
-    def getInodeNumber(self):
+    def getInodeNumber(self) -> int:
         """
         Retrieve the file serial number, also called inode number, which
         distinguishes this file from all other files on the same device.
@@ -1031,9 +1181,10 @@ def getInodeNumber(self):
         if not st:
             self.restat()
             st = self._statinfo
+        assert st is not None
         return st.st_ino
 
-    def getDevice(self):
+    def getDevice(self) -> int:
         """
         Retrieves the device containing the file.  The inode number and device
         number together uniquely identify the file, but the device number is
@@ -1054,9 +1205,10 @@ def getDevice(self):
         if not st:
             self.restat()
             st = self._statinfo
+        assert st is not None
         return st.st_dev
 
-    def getNumberOfHardLinks(self):
+    def getNumberOfHardLinks(self) -> int:
         """
         Retrieves the number of hard links to the file.
 
@@ -1079,9 +1231,10 @@ def getNumberOfHardLinks(self):
         if not st:
             self.restat()
             st = self._statinfo
+        assert st is not None
         return st.st_nlink
 
-    def getUserID(self):
+    def getUserID(self) -> int:
         """
         Returns the user ID of the file's owner.
 
@@ -1098,9 +1251,10 @@ def getUserID(self):
         if not st:
             self.restat()
             st = self._statinfo
+        assert st is not None
         return st.st_uid
 
-    def getGroupID(self):
+    def getGroupID(self) -> int:
         """
         Returns the group ID of the file.
 
@@ -1117,9 +1271,10 @@ def getGroupID(self):
         if not st:
             self.restat()
             st = self._statinfo
+        assert st is not None
         return st.st_gid
 
-    def getPermissions(self):
+    def getPermissions(self) -> Permissions:
         """
         Returns the permissions of the file.  Should also work on Windows,
         however, those permissions may not be what is expected in Windows.
@@ -1132,9 +1287,10 @@ def getPermissions(self):
         if not st:
             self.restat()
             st = self._statinfo
+        assert st is not None
         return Permissions(S_IMODE(st.st_mode))
 
-    def exists(self):
+    def exists(self) -> bool:
         """
         Check if this L{FilePath} exists.
 
@@ -1151,7 +1307,7 @@ def exists(self):
             else:
                 return False
 
-    def isdir(self):
+    def isdir(self) -> bool:
         """
         Check if this L{FilePath} refers to a directory.
 
@@ -1167,7 +1323,7 @@ def isdir(self):
                 return False
         return S_ISDIR(st.st_mode)
 
-    def isfile(self):
+    def isfile(self) -> bool:
         """
         Check if this file path refers to a regular file.
 
@@ -1183,7 +1339,7 @@ def isfile(self):
                 return False
         return S_ISREG(st.st_mode)
 
-    def isBlockDevice(self):
+    def isBlockDevice(self) -> bool:
         """
         Returns whether the underlying path is a block device.
 
@@ -1199,7 +1355,7 @@ def isBlockDevice(self):
                 return False
         return S_ISBLK(st.st_mode)
 
-    def isSocket(self):
+    def isSocket(self) -> bool:
         """
         Returns whether the underlying path is a socket.
 
@@ -1215,7 +1371,7 @@ def isSocket(self):
                 return False
         return S_ISSOCK(st.st_mode)
 
-    def islink(self):
+    def islink(self) -> bool:
         """
         Check if this L{FilePath} points to a symbolic link.
 
@@ -1229,7 +1385,7 @@ def islink(self):
         # it seems unlikely we'd actually save any work that way.  -glyph
         return islink(self.path)
 
-    def isabs(self):
+    def isabs(self) -> bool:
         """
         Check if this L{FilePath} refers to an absolute path.
 
@@ -1240,7 +1396,7 @@ def isabs(self):
         """
         return isabs(self.path)
 
-    def listdir(self):
+    def listdir(self) -> List[AnyStr]:
         """
         List the base names of the direct children of this L{FilePath}.
 
@@ -1254,7 +1410,7 @@ def listdir(self):
         """
         return listdir(self.path)
 
-    def splitext(self):
+    def splitext(self) -> Tuple[AnyStr, AnyStr]:
         """
         Split the file path into a pair C{(root, ext)} such that
         C{root + ext == path}.
@@ -1268,7 +1424,7 @@ def splitext(self):
     def __repr__(self) -> str:
         return f"FilePath({self.path!r})"
 
-    def touch(self):
+    def touch(self) -> None:
         """
         Updates the access and last modification times of the file at this
         file path to the current time. Also creates the file if it does not
@@ -1283,7 +1439,7 @@ def touch(self):
             pass
         utime(self.path, None)
 
-    def remove(self):
+    def remove(self) -> None:
         """
         Removes the file or directory that is represented by self.  If
         C{self.path} is a directory, recursively remove all its children
@@ -1297,7 +1453,7 @@ def remove(self):
             os.remove(self.path)
         self.changed()
 
-    def makedirs(self, ignoreExistingDirectory=False):
+    def makedirs(self, ignoreExistingDirectory: bool = False) -> None:
         """
         Create all directories not yet existing in C{path} segments, using
         L{os.makedirs}.
@@ -1309,14 +1465,14 @@ def makedirs(self, ignoreExistingDirectory=False):
         @return: L{None}
         """
         try:
-            return os.makedirs(self.path)
+            os.makedirs(self.path)
         except OSError as e:
             if not (
                 e.errno == errno.EEXIST and ignoreExistingDirectory and self.isdir()
             ):
                 raise
 
-    def globChildren(self, pattern):
+    def globChildren(self, pattern: OtherAnyStr) -> List[FilePath[OtherAnyStr]]:
         """
         Assuming I am representing a directory, return a list of FilePaths
         representing my children that match the given pattern.
@@ -1335,7 +1491,7 @@ def globChildren(self, pattern):
         path = ourPath[-1] == sep and ourPath + pattern or sep.join([ourPath, pattern])
         return [self.clonePath(p) for p in glob.glob(path)]
 
-    def basename(self):
+    def basename(self) -> AnyStr:
         """
         Retrieve the final component of the file path's path (everything
         after the final path separator).
@@ -1346,7 +1502,7 @@ def basename(self):
         """
         return basename(self.path)
 
-    def dirname(self):
+    def dirname(self) -> AnyStr:
         """
         Retrieve all of the components of the L{FilePath}'s path except the
         last one (everything up to the final path separator).
@@ -1357,7 +1513,7 @@ def dirname(self):
         """
         return dirname(self.path)
 
-    def parent(self):
+    def parent(self) -> FilePath[AnyStr]:
         """
         A file path for the directory containing the file at this file path.
 
@@ -1367,7 +1523,7 @@ def parent(self):
         """
         return self.clonePath(self.dirname())
 
-    def setContent(self, content, ext=b".new"):
+    def setContent(self, content: bytes, ext: Union[str, bytes] = ".new") -> None:
         """
         Replace the file at this path with a new file that contains the given
         bytes, trying to avoid data-loss in the meanwhile.
@@ -1419,12 +1575,12 @@ def setContent(self, content, ext=b".new"):
             os.unlink(self.path)
         os.rename(sib.path, self.asBytesMode().path)
 
-    def __cmp__(self, other):
+    def __cmp__(self, other: object) -> int:
         if not isinstance(other, FilePath):
             return NotImplemented
         return cmp(self.path, other.path)
 
-    def createDirectory(self):
+    def createDirectory(self) -> None:
         """
         Create the directory the L{FilePath} refers to.
 
@@ -1434,7 +1590,7 @@ def createDirectory(self):
         """
         os.mkdir(self.path)
 
-    def requireCreate(self, val=1):
+    def requireCreate(self, val: bool = True) -> None:
         """
         Sets the C{alwaysCreate} variable.
 
@@ -1460,7 +1616,19 @@ def create(self) -> IO[bytes]:
 
         return cast(IO[bytes], os.fdopen(fdint, "w+b"))
 
-    def temporarySibling(self, extension=b""):
+    @overload
+    def temporarySibling(self) -> FilePath[AnyStr]:
+        ...
+
+    @overload
+    def temporarySibling(
+        self, extension: Optional[OtherAnyStr]
+    ) -> FilePath[OtherAnyStr]:
+        ...
+
+    def temporarySibling(
+        self, extension: Optional[OtherAnyStr] = None
+    ) -> FilePath[OtherAnyStr]:
         """
         Construct a path referring to a sibling of this path.
 
@@ -1478,18 +1646,25 @@ def temporarySibling(self, extension=b""):
             set to True.
         @rtype: L{FilePath} with a mode equal to the type of C{extension}
         """
-        ourPath = self._getPathAsSameTypeAs(extension)
+        ext: OtherAnyStr
+        if extension is None:
+            # It's not possible to provide a default type argument which is why
+            # the overload is required.
+            ext = self.path[0:0]  # type:ignore[assignment]
+        else:
+            ext = extension
+        ourPath = self._getPathAsSameTypeAs(ext)
         sib = self.sibling(
-            _secureEnoughString(ourPath)
-            + self.clonePath(ourPath).basename()
-            + extension
+            _secureEnoughString(ourPath) + self.clonePath(ourPath).basename() + ext
         )
         sib.requireCreate()
         return sib
 
     _chunkSize = 2 ** 2 ** 2 ** 2
 
-    def copyTo(self, destination, followLinks=True):
+    def copyTo(
+        self, destination: FilePath[OtherAnyStr], followLinks: bool = True
+    ) -> None:
         """
         Copies self to destination.
 
@@ -1560,7 +1735,9 @@ def copyTo(self, destination, followLinks=True):
             # reaction to those types!
             raise NotImplementedError("Only copying of files and directories supported")
 
-    def moveTo(self, destination, followLinks=True):
+    def moveTo(
+        self, destination: FilePath[OtherAnyStr], followLinks: bool = True
+    ) -> None:
         """
         Move self to destination - basically renaming self to whatever
         destination is named.
@@ -1604,6 +1781,3 @@ def moveTo(self, destination, followLinks=True):
         else:
             self.changed()
             destination.changed()
-
-
-FilePath.clonePath = FilePath  # type: ignore[attr-defined]
diff --git a/src/twisted/python/zippath.py b/src/twisted/python/zippath.py
--- a/src/twisted/python/zippath.py
+++ b/src/twisted/python/zippath.py
@@ -7,21 +7,35 @@
 
 See the constructor of L{ZipArchive} for use.
 """
-
+from __future__ import annotations
 
 import errno
 import os
 import time
-from typing import Dict
+from typing import (
+    IO,
+    TYPE_CHECKING,
+    AnyStr,
+    Dict,
+    Generic,
+    Iterable,
+    List,
+    Tuple,
+    TypeVar,
+    Union,
+)
 from zipfile import ZipFile
 
 from zope.interface import implementer
 
+from typing_extensions import Literal
+
 from twisted.python.compat import cmp, comparable
 from twisted.python.filepath import (
     AbstractFilePath,
     FilePath,
     IFilePath,
+    OtherAnyStr,
     UnlistableError,
     _coerceToFilesystemEncoding,
 )
@@ -29,15 +43,23 @@
 ZIP_PATH_SEP = "/"  # In zipfiles, "/" is universally used as the
 # path separator, regardless of platform.
 
+_ArchiveStr = TypeVar("_ArchiveStr", bytes, str)
+_ZipStr = TypeVar("_ZipStr", bytes, str)
+_ZipSelf = TypeVar("_ZipSelf", bound="ZipPath")
+
 
 @comparable
 @implementer(IFilePath)
-class ZipPath(AbstractFilePath):
+class ZipPath(Generic[_ZipStr, _ArchiveStr], AbstractFilePath[_ZipStr]):
     """
     I represent a file or directory contained within a zip file.
     """
 
-    def __init__(self, archive, pathInArchive):
+    path: _ZipStr
+
+    def __init__(
+        self, archive: ZipArchive[_ArchiveStr], pathInArchive: _ZipStr
+    ) -> None:
         """
         Don't construct me directly.  Use C{ZipArchive.child()}.
 
@@ -45,18 +67,23 @@ def __init__(self, archive, pathInArchive):
 
         @param pathInArchive: a ZIP_PATH_SEP-separated string.
         """
-        self.archive = archive
-        self.pathInArchive = pathInArchive
+        self.archive: ZipArchive[_ArchiveStr] = archive
+        self.pathInArchive: _ZipStr = pathInArchive
+        self._nativePathInArchive: _ArchiveStr = _coerceToFilesystemEncoding(
+            archive._zipfileFilename, pathInArchive
+        )
 
         # self.path pretends to be os-specific because that's the way the
         # 'zipimport' module does it.
         sep = _coerceToFilesystemEncoding(pathInArchive, ZIP_PATH_SEP)
-        archiveFilename = _coerceToFilesystemEncoding(
-            pathInArchive, archive.zipfile.filename
+        archiveFilename: _ZipStr = _coerceToFilesystemEncoding(
+            pathInArchive, archive._zipfileFilename
         )
-        self.path = os.path.join(archiveFilename, *(self.pathInArchive.split(sep)))
+        segments: List[_ZipStr] = self.pathInArchive.split(sep)
+        fakePath: _ZipStr = os.path.join(archiveFilename, *segments)
+        self.path: _ZipStr = fakePath
 
-    def __cmp__(self, other):
+    def __cmp__(self, other: object) -> int:
         if not isinstance(other, ZipPath):
             return NotImplemented
         return cmp(
@@ -64,6 +91,7 @@ def __cmp__(self, other):
         )
 
     def __repr__(self) -> str:
+        parts: List[_ZipStr]
         parts = [
             _coerceToFilesystemEncoding(self.sep, os.path.abspath(self.archive.path))
         ]
@@ -72,7 +100,7 @@ def __repr__(self) -> str:
         return f"ZipPath({ossep.join(parts)!r})"
 
     @property
-    def sep(self):
+    def sep(self) -> _ZipStr:
         """
         Return a zip directory separator.
 
@@ -81,13 +109,34 @@ def sep(self):
         """
         return _coerceToFilesystemEncoding(self.path, ZIP_PATH_SEP)
 
-    def parent(self):
+    def _nativeParent(
+        self,
+    ) -> Union[ZipPath[_ZipStr, _ArchiveStr], ZipArchive[_ArchiveStr]]:
+        """
+        Return parent, discarding our own encoding in favor of whatever the
+        archive's is.
+        """
         splitup = self.pathInArchive.split(self.sep)
         if len(splitup) == 1:
             return self.archive
         return ZipPath(self.archive, self.sep.join(splitup[:-1]))
 
-    def child(self, path):
+    def parent(self) -> Union[ZipPath[_ZipStr, _ArchiveStr], ZipArchive[_ZipStr]]:
+        parent = self._nativeParent()
+        if isinstance(parent, ZipArchive):
+            return ZipArchive(
+                _coerceToFilesystemEncoding(self.path, self.archive._zipfileFilename)
+            )
+        return parent
+
+    if TYPE_CHECKING:
+
+        def parents(
+            self,
+        ) -> Iterable[Union[ZipPath[_ZipStr, _ArchiveStr], ZipArchive[_ZipStr]]]:
+            ...
+
+    def child(self, path: OtherAnyStr) -> ZipPath[OtherAnyStr, _ArchiveStr]:
         """
         Return a new ZipPath representing a path in C{self.archive} which is
         a child of this path.
@@ -103,25 +152,37 @@ def child(self, path):
         pathInArchive = _coerceToFilesystemEncoding(path, self.pathInArchive)
         return ZipPath(self.archive, joiner.join([pathInArchive, path]))
 
-    def sibling(self, path):
-        return self.parent().child(path)
+    def sibling(self, path: OtherAnyStr) -> ZipPath[OtherAnyStr, _ArchiveStr]:
+        parent: Union[ZipPath[_ZipStr, _ArchiveStr], ZipArchive[_ZipStr]]
+        rightTypedParent: Union[ZipPath[_ZipStr, _ArchiveStr], ZipArchive[_ArchiveStr]]
 
-    def exists(self):
+        parent = self.parent()
+        rightTypedParent = self.archive if isinstance(parent, ZipArchive) else parent
+        child: ZipPath[OtherAnyStr, _ArchiveStr] = rightTypedParent.child(path)
+        return child
+
+    def exists(self) -> bool:
         return self.isdir() or self.isfile()
 
-    def isdir(self):
+    def isdir(self) -> bool:
         return self.pathInArchive in self.archive.childmap
 
-    def isfile(self):
+    def isfile(self) -> bool:
         return self.pathInArchive in self.archive.zipfile.NameToInfo
 
-    def islink(self):
+    def islink(self) -> bool:
         return False
 
-    def listdir(self):
+    def listdir(self) -> List[_ZipStr]:
         if self.exists():
             if self.isdir():
-                return list(self.archive.childmap[self.pathInArchive].keys())
+                parentArchivePath: _ArchiveStr = _coerceToFilesystemEncoding(
+                    self.archive._zipfileFilename, self.pathInArchive
+                )
+                return [
+                    _coerceToFilesystemEncoding(self.path, each)
+                    for each in self.archive.childmap[parentArchivePath].keys()
+                ]
             else:
                 raise UnlistableError(OSError(errno.ENOTDIR, "Leaf zip entry listed"))
         else:
@@ -129,7 +190,7 @@ def listdir(self):
                 OSError(errno.ENOENT, "Non-existent zip entry listed")
             )
 
-    def splitext(self):
+    def splitext(self) -> Tuple[_ZipStr, _ZipStr]:
         """
         Return a value similar to that returned by C{os.path.splitext}.
         """
@@ -138,22 +199,25 @@ def splitext(self):
         # attribute.
         return os.path.splitext(self.path)
 
-    def basename(self):
+    def basename(self) -> _ZipStr:
         return self.pathInArchive.split(self.sep)[-1]
 
-    def dirname(self):
+    def dirname(self) -> _ZipStr:
         # XXX NOTE: This API isn't a very good idea on filepath, but it's even
         # less meaningful here.
         return self.parent().path
 
-    def open(self, mode="r"):
+    def open(self, mode: Literal["r", "w"] = "r") -> IO[bytes]:  # type:ignore[override]
+        # TODO: liskov substitutability is broken here because the stdlib
+        # zipfile does not support appending to files within archives, only to
+        # archives themselves; we could fix this by emulating append mode.
         pathInArchive = _coerceToFilesystemEncoding("", self.pathInArchive)
         return self.archive.zipfile.open(pathInArchive, mode=mode)
 
-    def changed(self):
+    def changed(self) -> None:
         pass
 
-    def getsize(self):
+    def getsize(self) -> int:
         """
         Retrieve this file's size.
 
@@ -162,7 +226,7 @@ def getsize(self):
         pathInArchive = _coerceToFilesystemEncoding("", self.pathInArchive)
         return self.archive.zipfile.NameToInfo[pathInArchive].file_size
 
-    def getAccessTime(self):
+    def getAccessTime(self) -> float:
         """
         Retrieve this file's last access-time.  This is the same as the last access
         time for the archive.
@@ -171,7 +235,7 @@ def getAccessTime(self):
         """
         return self.archive.getAccessTime()
 
-    def getModificationTime(self):
+    def getModificationTime(self) -> float:
         """
         Retrieve this file's last modification time.  This is the time of
         modification recorded in the zipfile.
@@ -183,7 +247,7 @@ def getModificationTime(self):
             self.archive.zipfile.NameToInfo[pathInArchive].date_time + (0, 0, 0)
         )
 
-    def getStatusChangeTime(self):
+    def getStatusChangeTime(self) -> float:
         """
         Retrieve this file's last modification time.  This name is provided for
         compatibility, and returns the same value as getmtime.
@@ -193,7 +257,7 @@ def getStatusChangeTime(self):
         return self.getModificationTime()
 
 
-class ZipArchive(ZipPath):
+class ZipArchive(ZipPath[AnyStr, AnyStr]):
     """
     I am a L{FilePath}-like object which can wrap a zip archive as if it were a
     directory.
@@ -206,11 +270,13 @@ class ZipArchive(ZipPath):
     converting if required.
     """
 
+    _zipfileFilename: AnyStr
+
     @property
     def archive(self):
         return self
 
-    def __init__(self, archivePathname):
+    def __init__(self, archivePathname: AnyStr) -> None:
         """
         Create a ZipArchive, treating the archive at archivePathname as a zip
         file.
@@ -220,22 +286,32 @@ def __init__(self, archivePathname):
         """
         self.path = archivePathname
         self.zipfile = ZipFile(_coerceToFilesystemEncoding("", archivePathname))
+        zfname = self.zipfile.filename
+        assert (
+            zfname is not None
+        ), "zipfile must have filename when initialized with a path"
+        self._zipfileFilename = _coerceToFilesystemEncoding(archivePathname, zfname)
         self.pathInArchive = _coerceToFilesystemEncoding(archivePathname, "")
         # zipfile is already wasting O(N) memory on cached ZipInfo instances,
         # so there's no sense in trying to do this lazily or intelligently
-        self.childmap: Dict[str, Dict[str, int]] = {}
+        self.childmap: Dict[AnyStr, Dict[AnyStr, int]] = {}
 
         for name in self.zipfile.namelist():
-            name = _coerceToFilesystemEncoding(self.path, name).split(self.sep)
-            for x in range(len(name)):
-                child = name[-x]
-                parent = self.sep.join(name[:-x])
+            splitName = _coerceToFilesystemEncoding(self.path, name).split(self.sep)
+            for x in range(len(splitName)):
+                child = splitName[-x]
+                parent = self.sep.join(splitName[:-x])
                 if parent not in self.childmap:
                     self.childmap[parent] = {}
                 self.childmap[parent][child] = 1
             parent = _coerceToFilesystemEncoding(archivePathname, "")
 
-    def child(self, path):
+    def __cmp__(self, other: object) -> int:
+        if not isinstance(other, ZipArchive):
+            return NotImplemented
+        return cmp(self.path, other.path)
+
+    def child(self, path: OtherAnyStr) -> ZipPath[OtherAnyStr, AnyStr]:
         """
         Create a ZipPath pointing at a path within the archive.
 
@@ -244,29 +320,29 @@ def child(self, path):
         """
         return ZipPath(self, path)
 
-    def exists(self):
+    def exists(self) -> bool:
         """
         Returns C{True} if the underlying archive exists.
         """
-        return FilePath(self.zipfile.filename).exists()
+        return FilePath(self._zipfileFilename).exists()
 
-    def getAccessTime(self):
+    def getAccessTime(self) -> float:
         """
         Return the archive file's last access time.
         """
-        return FilePath(self.zipfile.filename).getAccessTime()
+        return FilePath(self._zipfileFilename).getAccessTime()
 
-    def getModificationTime(self):
+    def getModificationTime(self) -> float:
         """
         Return the archive file's modification time.
         """
-        return FilePath(self.zipfile.filename).getModificationTime()
+        return FilePath(self._zipfileFilename).getModificationTime()
 
-    def getStatusChangeTime(self):
+    def getStatusChangeTime(self) -> float:
         """
         Return the archive file's status change time.
         """
-        return FilePath(self.zipfile.filename).getStatusChangeTime()
+        return FilePath(self._zipfileFilename).getStatusChangeTime()
 
     def __repr__(self) -> str:
         return f"ZipArchive({os.path.abspath(self.path)!r})"


Test patch:
diff --git a/src/twisted/python/test/test_zippath.py b/src/twisted/python/test/test_zippath.py
--- a/src/twisted/python/test/test_zippath.py
+++ b/src/twisted/python/test/test_zippath.py
@@ -8,9 +8,10 @@
 
 import os
 import zipfile
+from typing import Union
 
 from twisted.python.filepath import _coerceToFilesystemEncoding
-from twisted.python.zippath import ZipArchive
+from twisted.python.zippath import ZipArchive, ZipPath
 from twisted.test.test_paths import AbstractFilePathTests
 
 
@@ -39,6 +40,9 @@ class ZipFilePathTests(AbstractFilePathTests):
     and L{ZipArchive}.
     """
 
+    path: ZipArchive[bytes]  # type:ignore[assignment]
+    root: ZipArchive[bytes]  # type:ignore[assignment]
+
     def setUp(self):
         AbstractFilePathTests.setUp(self)
         zipit(self.cmn, self.cmn + b".zip")
@@ -47,12 +51,18 @@ def setUp(self):
         self.root = self.path
         self.all = [x.replace(self.cmn, self.cmn + b".zip") for x in self.all]
 
+    def test_sibling(self) -> None:
+        """
+        L{ZipPath.sibling} returns a path at the same level.
+        """
+        self.assertEqual(self.path.child("one").sibling("two"), self.path.child("two"))
+
     def test_zipPathRepr(self):
         """
         Make sure that invoking ZipPath's repr prints the correct class name
         and an absolute path to the zip file.
         """
-        child = self.path.child("foo")
+        child: Union[ZipPath[str, bytes], ZipPath[str, str]] = self.path.child("foo")
         pathRepr = "ZipPath({!r})".format(
             os.path.abspath(self.nativecmn + ".zip" + os.sep + "foo"),
         )
diff --git a/src/twisted/test/test_paths.py b/src/twisted/test/test_paths.py
--- a/src/twisted/test/test_paths.py
+++ b/src/twisted/test/test_paths.py
@@ -5,6 +5,7 @@
 Test cases covering L{twisted.python.filepath}.
 """
 
+from __future__ import annotations
 
 import errno
 import os
@@ -12,11 +13,13 @@
 import stat
 import time
 from pprint import pformat
+from typing import IO, AnyStr, Callable, Dict, List, Optional, Tuple, TypeVar, Union
 from unittest import skipIf
 
 from zope.interface.verify import verifyObject
 
 from twisted.python import filepath
+from twisted.python.filepath import FileMode, OtherAnyStr
 from twisted.python.runtime import platform
 from twisted.python.win32 import ERROR_DIRECTORY
 from twisted.trial.unittest import SynchronousTestCase as TestCase
@@ -29,11 +32,11 @@ class BytesTestCase(TestCase):
     Override default method implementations to support byte paths.
     """
 
-    def mktemp(self):
+    def mktemp(self) -> bytes:
         """
         Return a temporary path, encoded as bytes.
         """
-        return TestCase.mktemp(self).encode("utf-8")
+        return TestCase.mktemp(self).encode("utf-8")  # type:ignore[no-any-return]
 
 
 class AbstractFilePathTests(BytesTestCase):
@@ -55,7 +58,7 @@ def subdir(self, *dirname):
     def subfile(self, *dirname):
         return open(self._mkpath(*dirname), "wb")
 
-    def setUp(self):
+    def setUp(self) -> None:
         self.now = time.time()
         cmn = self.cmn = os.path.abspath(self.mktemp())
         self.all = [cmn]
@@ -72,13 +75,13 @@ def setUp(self):
         self.path = filepath.FilePath(cmn)
         self.root = filepath.FilePath(b"/")
 
-    def test_verifyObject(self):
+    def test_verifyObject(self) -> None:
         """
         Instances of the path type being tested provide L{IFilePath}.
         """
         self.assertTrue(verifyObject(filepath.IFilePath, self.path))
 
-    def test_segmentsFromPositive(self):
+    def test_segmentsFromPositive(self) -> None:
         """
         Verify that the segments between two paths are correctly identified.
         """
@@ -87,7 +90,7 @@ def test_segmentsFromPositive(self):
             [b"a", b"b", b"c"],
         )
 
-    def test_segmentsFromNegative(self):
+    def test_segmentsFromNegative(self) -> None:
         """
         Verify that segmentsFrom notices when the ancestor isn't an ancestor.
         """
@@ -97,7 +100,7 @@ def test_segmentsFromNegative(self):
             self.path.child(b"d").child(b"c").child(b"e"),
         )
 
-    def test_walk(self):
+    def test_walk(self) -> None:
         """
         Verify that walking the path gives the same result as the known file
         hierarchy.
@@ -105,7 +108,7 @@ def test_walk(self):
         x = [foo.path for foo in self.path.walk()]
         self.assertEqual(set(x), set(self.all))
 
-    def test_parents(self):
+    def test_parents(self) -> None:
         """
         L{FilePath.parents()} should return an iterator of every ancestor of
         the L{FilePath} in question.
@@ -121,7 +124,7 @@ def test_parents(self):
             thispath = os.path.dirname(thispath)
         self.assertEqual([x.path for x in pathobj.parents()], L)
 
-    def test_validSubdir(self):
+    def test_validSubdir(self) -> None:
         """
         Verify that a valid subdirectory will show up as a directory, but not as a
         file, not as a symlink, and be listable.
@@ -133,14 +136,14 @@ def test_validSubdir(self):
         self.assertFalse(sub1.islink(), "It's a directory.")
         self.assertEqual(sub1.listdir(), [b"file2"])
 
-    def test_invalidSubdir(self):
+    def test_invalidSubdir(self) -> None:
         """
         Verify that a subdirectory that doesn't exist is reported as such.
         """
         sub2 = self.path.child(b"sub2")
         self.assertFalse(sub2.exists(), "This directory does not exist.")
 
-    def test_validFiles(self):
+    def test_validFiles(self) -> None:
         """
         Make sure that we can read existent non-empty files.
         """
@@ -151,7 +154,7 @@ def test_validFiles(self):
         with f2.open() as f:
             self.assertEqual(f.read(), self.f2content)
 
-    def test_multipleChildSegments(self):
+    def test_multipleChildSegments(self) -> None:
         """
         C{fp.descendant([a, b, c])} returns the same L{FilePath} as is returned
         by C{fp.child(a).child(b).child(c)}.
@@ -160,7 +163,7 @@ def test_multipleChildSegments(self):
         single = self.path.child(b"a").child(b"b").child(b"c")
         self.assertEqual(multiple, single)
 
-    def test_dictionaryKeys(self):
+    def test_dictionaryKeys(self) -> None:
         """
         Verify that path instances are usable as dictionary keys.
         """
@@ -178,17 +181,17 @@ def test_dictionaryKeys(self):
         self.assertEqual(dictoid[f2], 5)
         self.assertEqual(len(dictoid), 2)
 
-    def test_dictionaryKeyWithString(self):
+    def test_dictionaryKeyWithString(self) -> None:
         """
         Verify that path instances are usable as dictionary keys which do not clash
         with their string counterparts.
         """
         f1 = self.path.child(b"file1")
-        dictoid = {f1: "hello"}
+        dictoid: Dict[Union[filepath.FilePath[bytes], bytes], str] = {f1: "hello"}
         dictoid[f1.path] = "goodbye"
         self.assertEqual(len(dictoid), 2)
 
-    def test_childrenNonexistentError(self):
+    def test_childrenNonexistentError(self) -> None:
         """
         Verify that children raises the appropriate exception for non-existent
         directories.
@@ -197,14 +200,14 @@ def test_childrenNonexistentError(self):
             filepath.UnlistableError, self.path.child(b"not real").children
         )
 
-    def test_childrenNotDirectoryError(self):
+    def test_childrenNotDirectoryError(self) -> None:
         """
         Verify that listdir raises the appropriate exception for attempting to list
         a file rather than a directory.
         """
         self.assertRaises(filepath.UnlistableError, self.path.child(b"file1").children)
 
-    def test_newTimesAreFloats(self):
+    def test_newTimesAreFloats(self) -> None:
         """
         Verify that all times returned from the various new time functions are ints
         (and hopefully therefore 'high precision').
@@ -214,7 +217,7 @@ def test_newTimesAreFloats(self):
             self.assertEqual(type(p.getModificationTime()), float)
             self.assertEqual(type(p.getStatusChangeTime()), float)
 
-    def test_oldTimesAreInts(self):
+    def test_oldTimesAreInts(self) -> None:
         """
         Verify that all times returned from the various time functions are
         integers, for compatibility.
@@ -225,12 +228,12 @@ def test_oldTimesAreInts(self):
             self.assertEqual(type(p.getctime()), int)
 
 
-class FakeWindowsPath(filepath.FilePath):
+class FakeWindowsPath(filepath.FilePath[AnyStr]):
     """
     A test version of FilePath which overrides listdir to raise L{WindowsError}.
     """
 
-    def listdir(self):
+    def listdir(self) -> List[AnyStr]:
         """
         @raise WindowsError: always.
         """
@@ -266,7 +269,7 @@ def test_windowsErrorExcept(self):
         self.assertRaises(filepath.UnlistableError, fwp.children)
         self.assertRaises(WindowsError, fwp.children)
 
-    def test_alwaysCatchOSError(self):
+    def test_alwaysCatchOSError(self) -> None:
         """
         Verify that in the normal case where a directory does not exist, we will
         get an OSError.
@@ -274,7 +277,7 @@ def test_alwaysCatchOSError(self):
         fp = filepath.FilePath(self.mktemp())
         self.assertRaises(OSError, fp.children)
 
-    def test_keepOriginalAttributes(self):
+    def test_keepOriginalAttributes(self) -> None:
         """
         Verify that the Unlistable exception raised will preserve the attributes of
         the previously-raised exception.
@@ -312,20 +315,20 @@ def write(self, what):
         """
         raise OSError()
 
-    def close(self):
+    def close(self) -> None:
         """
         Mark the file as having been closed.
         """
         self.closed = True
 
-    def __enter__(self):
+    def __enter__(self) -> ExplodingFile:
         return self
 
-    def __exit__(self, exc_type, exc_value, traceback):
+    def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None:
         self.close()
 
 
-class TrackingFilePath(filepath.FilePath):
+class TrackingFilePath(filepath.FilePath[AnyStr]):
     """
     A subclass of L{filepath.FilePath} which maintains a list of all other paths
     created by clonePath.
@@ -343,39 +346,52 @@ class TrackingFilePath(filepath.FilePath):
     @type openedFiles: C{list} of C{file}
     """
 
-    def __init__(self, path, alwaysCreate=False, trackingList=None):
+    def __init__(
+        self,
+        path: AnyStr,
+        alwaysCreate: bool = False,
+        trackingList: Optional[
+            List[Union[TrackingFilePath[str], TrackingFilePath[bytes]]]
+        ] = None,
+    ) -> None:
         filepath.FilePath.__init__(self, path, alwaysCreate)
         if trackingList is None:
             trackingList = []
-        self.trackingList = trackingList
-        self.openedFiles = []
+        self.trackingList: List[
+            Union[TrackingFilePath[str], TrackingFilePath[bytes]]
+        ] = trackingList
+        self.openedFiles: List[IO] = []
 
-    def open(self, *a, **k):
+    def open(self, mode: FileMode = "r") -> IO[bytes]:
         """
         Override 'open' to track all files opened by this path.
         """
-        f = filepath.FilePath.open(self, *a, **k)
+        f = filepath.FilePath.open(self, mode)
         self.openedFiles.append(f)
         return f
 
-    def openedPaths(self):
+    def openedPaths(
+        self,
+    ) -> List[Union[TrackingFilePath[str], TrackingFilePath[bytes]]]:
         """
         Return a list of all L{TrackingFilePath}s associated with this
         L{TrackingFilePath} that have had their C{open()} method called.
         """
         return [path for path in self.trackingList if path.openedFiles]
 
-    def clonePath(self, name):
+    def clonePath(
+        self, path: OtherAnyStr, alwaysCreate: bool = False
+    ) -> TrackingFilePath[OtherAnyStr]:
         """
         Override L{filepath.FilePath.clonePath} to give the new path a reference
         to the same tracking list.
         """
-        clone = TrackingFilePath(name, trackingList=self.trackingList)
+        clone = TrackingFilePath(path, trackingList=self.trackingList)
         self.trackingList.append(clone)
         return clone
 
 
-class ExplodingFilePath(filepath.FilePath):
+class ExplodingFilePath(filepath.FilePath[AnyStr]):
     """
     A specialized L{FilePath} which always returns an instance of
     L{ExplodingFile} from its C{open} method.
@@ -384,7 +400,15 @@ class ExplodingFilePath(filepath.FilePath):
         C{open} method.
     """
 
-    def __init__(self, pathName, originalExploder=None):
+    fp: ExplodingFile
+
+    def __init__(
+        self,
+        pathName: AnyStr,
+        originalExploder: Optional[
+            Union[ExplodingFilePath[str], ExplodingFilePath[bytes]]
+        ] = None,
+    ) -> None:
         """
         Initialize an L{ExplodingFilePath} with a name and a reference to the
 
@@ -400,7 +424,7 @@ def __init__(self, pathName, originalExploder=None):
             originalExploder = self
         self._originalExploder = originalExploder
 
-    def open(self, mode=None):
+    def open(self, mode: FileMode = "r") -> IO[bytes]:
         """
         Create, save, and return a new C{ExplodingFile}.
 
@@ -409,18 +433,25 @@ def open(self, mode=None):
         @return: A new C{ExplodingFile}.
         """
         f = self._originalExploder.fp = ExplodingFile()
-        return f
+        return f  # type:ignore[return-value]
 
-    def clonePath(self, name):
+    def clonePath(
+        self, name: OtherAnyStr, alwaysCreate: bool = False
+    ) -> filepath.FilePath[OtherAnyStr]:
         return ExplodingFilePath(name, self._originalExploder)
 
 
+T = TypeVar("T")
+
+
 class PermissionsTests(BytesTestCase):
     """
     Test Permissions and RWX classes
     """
 
-    def assertNotUnequal(self, first, second, msg=None):
+    def assertNotUnequal(
+        self, first: T, second: object, msg: Optional[str] = None
+    ) -> T:
         """
         Tests that C{first} != C{second} is false.  This method tests the
         __ne__ method, as opposed to L{assertEqual} (C{first} == C{second}),
@@ -439,7 +470,20 @@ def assertNotUnequal(self, first, second, msg=None):
             )
         return first
 
-    def test_rwxFromBools(self):
+    def test_test(self) -> None:
+        """
+        Self-test for assertNotUnequal to make sure the assertion works.
+        """
+        with self.assertRaises(AssertionError) as ae:
+            self.assertNotUnequal(3, 4, "custom message")
+        self.assertIn("__ne__ not implemented correctly", str(ae.exception))
+        self.assertIn("custom message", str(ae.exception))
+        with self.assertRaises(AssertionError) as ae2:
+            self.assertNotUnequal(4, 3)
+        self.assertIn("__ne__ not implemented correctly", str(ae2.exception))
+        self.assertNotUnequal(3, 3)
+
+    def test_rwxFromBools(self) -> None:
         """
         L{RWX}'s constructor takes a set of booleans
         """
@@ -453,7 +497,7 @@ def test_rwxFromBools(self):
         rwx = filepath.RWX(True, True, True)
         self.assertTrue(rwx.read and rwx.write and rwx.execute)
 
-    def test_rwxEqNe(self):
+    def test_rwxEqNe(self) -> None:
         """
         L{RWX}'s created with the same booleans are equivalent.  If booleans
         are different, they are not equal.
@@ -468,14 +512,14 @@ def test_rwxEqNe(self):
         )
         self.assertNotEqual(3, filepath.RWX(True, True, True))
 
-    def test_rwxShorthand(self):
+    def test_rwxShorthand(self) -> None:
         """
         L{RWX}'s shorthand string should be 'rwx' if read, write, and execute
         permission bits are true.  If any of those permissions bits are false,
         the character is replaced by a '-'.
         """
 
-        def getChar(val, letter):
+        def getChar(val: bool, letter: str) -> str:
             if val:
                 return letter
             return "-"
@@ -490,15 +534,16 @@ def getChar(val, letter):
                     )
         self.assertEqual(filepath.RWX(True, False, True).shorthand(), "r-x")
 
-    def test_permissionsFromStat(self):
+    def test_permissionsFromStat(self) -> None:
         """
         L{Permissions}'s constructor takes a valid permissions bitmask and
         parsaes it to produce the correct set of boolean permissions.
         """
 
-        def _rwxFromStat(statModeInt, who):
-            def getPermissionBit(what, who):
-                return (statModeInt & getattr(stat, f"S_I{what}{who}")) > 0
+        def _rwxFromStat(statModeInt: int, who: str) -> filepath.RWX:
+            def getPermissionBit(what: str, who: str) -> bool:
+                constant: int = getattr(stat, f"S_I{what}{who}")
+                return (statModeInt & constant) > 0
 
             return filepath.RWX(
                 *(getPermissionBit(what, who) for what in ("R", "W", "X"))
@@ -530,7 +575,7 @@ def getPermissionBit(what, who):
             for what in ("read", "write", "execute"):
                 self.assertTrue(getattr(getattr(perm, who), what))
 
-    def test_permissionsEq(self):
+    def test_permissionsEq(self) -> None:
         """
         Two L{Permissions}'s that are created with the same bitmask
         are equivalent
@@ -540,7 +585,7 @@ def test_permissionsEq(self):
         self.assertNotEqual(filepath.Permissions(0o777), filepath.Permissions(0o700))
         self.assertNotEqual(3, filepath.Permissions(0o777))
 
-    def test_permissionsShorthand(self):
+    def test_permissionsShorthand(self) -> None:
         """
         L{Permissions}'s shorthand string is the RWX shorthand string for its
         user permission bits, group permission bits, and other permission bits
@@ -567,7 +612,7 @@ class FilePathTests(AbstractFilePathTests):
     class are only run against L{twisted.python.filepath}.
     """
 
-    def test_chmod(self):
+    def test_chmod(self) -> None:
         """
         L{FilePath.chmod} modifies the permissions of
         the passed file as expected (using C{os.stat} to check). We use some
@@ -579,7 +624,7 @@ def test_chmod(self):
                 stat.S_IMODE(os.stat(self.path.child(b"sub1").path).st_mode), mode
             )
 
-    def createLinks(self):
+    def createLinks(self) -> None:
         """
         Create several symbolic links to files and directories.
         """
@@ -591,7 +636,7 @@ def createLinks(self):
         )
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_realpathSymlink(self):
+    def test_realpathSymlink(self) -> None:
         """
         L{FilePath.realpath} returns the path of the ultimate target of a
         symlink.
@@ -606,7 +651,7 @@ def test_realpathSymlink(self):
         )
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_realpathCyclicalSymlink(self):
+    def test_realpathCyclicalSymlink(self) -> None:
         """
         L{FilePath.realpath} raises L{filepath.LinkError} if the path is a
         symbolic link which is part of a cycle.
@@ -615,7 +660,7 @@ def test_realpathCyclicalSymlink(self):
         os.symlink(self.path.child(b"link2").path, self.path.child(b"link1").path)
         self.assertRaises(filepath.LinkError, self.path.child(b"link2").realpath)
 
-    def test_realpathNoSymlink(self):
+    def test_realpathNoSymlink(self) -> None:
         """
         L{FilePath.realpath} returns the path itself if the path is not a
         symbolic link.
@@ -623,7 +668,7 @@ def test_realpathNoSymlink(self):
         self.assertEqual(self.path.child(b"sub1").realpath(), self.path.child(b"sub1"))
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_walkCyclicalSymlink(self):
+    def test_walkCyclicalSymlink(self) -> None:
         """
         Verify that walking a path with a cyclical symlink raises an error
         """
@@ -633,13 +678,13 @@ def test_walkCyclicalSymlink(self):
             self.path.child(b"sub1").child(b"sub1.loopylink").path,
         )
 
-        def iterateOverPath():
+        def iterateOverPath() -> List[bytes]:
             return [foo.path for foo in self.path.walk()]
 
         self.assertRaises(filepath.LinkError, iterateOverPath)
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_walkObeysDescendWithCyclicalSymlinks(self):
+    def test_walkObeysDescendWithCyclicalSymlinks(self) -> None:
         """
         Verify that, after making a path with cyclical symlinks, when the
         supplied C{descend} predicate returns C{False}, the target is not
@@ -652,29 +697,29 @@ def test_walkObeysDescendWithCyclicalSymlinks(self):
             self.path.child(b"sub1").child(b"sub1.loopylink").path,
         )
 
-        def noSymLinks(path):
+        def noSymLinks(path: filepath.FilePath[bytes]) -> bool:
             return not path.islink()
 
-        def iterateOverPath():
+        def iterateOverPath() -> List[bytes]:
             return [foo.path for foo in self.path.walk(descend=noSymLinks)]
 
         self.assertTrue(iterateOverPath())
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_walkObeysDescend(self):
+    def test_walkObeysDescend(self) -> None:
         """
         Verify that when the supplied C{descend} predicate returns C{False},
         the target is not traversed.
         """
         self.createLinks()
 
-        def noSymLinks(path):
+        def noSymLinks(path: filepath.FilePath[bytes]) -> bool:
             return not path.islink()
 
         x = [foo.path for foo in self.path.walk(descend=noSymLinks)]
         self.assertEqual(set(x), set(self.all))
 
-    def test_getAndSet(self):
+    def test_getAndSet(self) -> None:
         content = b"newcontent"
         self.path.child(b"new").setContent(content)
         newcontent = self.path.child(b"new").getContent()
@@ -684,17 +729,17 @@ def test_getAndSet(self):
         newcontent = self.path.child(b"new").getContent()
         self.assertEqual(content, newcontent)
 
-    def test_getContentFileClosing(self):
+    def test_getContentFileClosing(self) -> None:
         """
         If reading from the underlying file raises an exception,
         L{FilePath.getContent} raises that exception after closing the file.
         """
-        fp = ExplodingFilePath(b"")
+        fp = ExplodingFilePath("")
         self.assertRaises(IOError, fp.getContent)
         self.assertTrue(fp.fp.closed)
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_symbolicLink(self):
+    def test_symbolicLink(self) -> None:
         """
         Verify the behavior of the C{isLink} method against links and
         non-links. Also check that the symbolic link shares the directory
@@ -709,7 +754,7 @@ def test_symbolicLink(self):
         self.assertTrue(s3.isdir())
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_linkTo(self):
+    def test_linkTo(self) -> None:
         """
         Verify that symlink creates a valid symlink that is both a link and a
         file if its target is a file, or a directory if its target is a
@@ -729,7 +774,7 @@ def test_linkTo(self):
             self.assertEqual(target.isfile(), link.isfile())
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_linkToErrors(self):
+    def test_linkToErrors(self) -> None:
         """
         Verify C{linkTo} fails in the following case:
             - the target is in a directory that doesn't exist
@@ -746,23 +791,27 @@ def test_linkToErrors(self):
             self.path.child(b"sub1").child(b"file2"),
         )
 
-    def testMultiExt(self):
+    def testMultiExt(self) -> None:
         f3 = self.path.child(b"sub3").child(b"file3")
         exts = b".foo", b".bar", b"ext1", b"ext2", b"ext3"
         self.assertFalse(f3.siblingExtensionSearch(*exts))
         f3e = f3.siblingExtension(b".foo")
         f3e.touch()
-        self.assertFalse(not f3.siblingExtensionSearch(*exts).exists())
-        self.assertFalse(not f3.siblingExtensionSearch(b"*").exists())
+        found = f3.siblingExtensionSearch(*exts)
+        assert found is not None
+        self.assertFalse(not found.exists())
+        globbed = f3.siblingExtensionSearch(b"*")
+        assert globbed is not None
+        self.assertFalse(not globbed.exists())
         f3e.remove()
         self.assertFalse(f3.siblingExtensionSearch(*exts))
 
-    def testPreauthChild(self):
+    def testPreauthChild(self) -> None:
         fp = filepath.FilePath(b".")
         fp.preauthChild(b"foo/bar")
-        self.assertRaises(filepath.InsecurePath, fp.child, "/mon\u20acy")
+        self.assertRaises(filepath.InsecurePath, fp.preauthChild, "/mon\u20acy")
 
-    def testStatCache(self):
+    def testStatCache(self) -> None:
         p = self.path.child(b"stattest")
         p.touch()
         self.assertEqual(p.getsize(), 0)
@@ -781,23 +830,23 @@ def testStatCache(self):
         self.assertFalse(p.isdir())
         self.assertFalse(p.isfile())
 
-    def testPersist(self):
+    def testPersist(self) -> None:
         newpath = pickle.loads(pickle.dumps(self.path))
         self.assertEqual(self.path.__class__, newpath.__class__)
         self.assertEqual(self.path.path, newpath.path)
 
-    def testInsecureUNIX(self):
+    def testInsecureUNIX(self) -> None:
         self.assertRaises(filepath.InsecurePath, self.path.child, b"..")
         self.assertRaises(filepath.InsecurePath, self.path.child, b"/etc")
         self.assertRaises(filepath.InsecurePath, self.path.child, b"../..")
 
     @skipIf(platform.getType() != "win32", "Test will run only on Windows.")
-    def testInsecureWin32(self):
+    def testInsecureWin32(self) -> None:
         self.assertRaises(filepath.InsecurePath, self.path.child, b"..\\..")
         self.assertRaises(filepath.InsecurePath, self.path.child, b"C:randomfile")
 
     @skipIf(platform.getType() != "win32", "Test will run only on Windows.")
-    def testInsecureWin32Whacky(self):
+    def testInsecureWin32Whacky(self) -> None:
         """
         Windows has 'special' filenames like NUL and CON and COM1 and LPR
         and PRN and ... god knows what else.  They can be located anywhere in
@@ -808,7 +857,7 @@ def testInsecureWin32Whacky(self):
         self.assertRaises(filepath.InsecurePath, self.path.child, b"C:CON")
         self.assertRaises(filepath.InsecurePath, self.path.child, r"C:\CON")
 
-    def testComparison(self):
+    def testComparison(self) -> None:
         self.assertEqual(filepath.FilePath(b"a"), filepath.FilePath(b"a"))
         self.assertTrue(filepath.FilePath(b"z") > filepath.FilePath(b"a"))
         self.assertTrue(filepath.FilePath(b"z") >= filepath.FilePath(b"a"))
@@ -821,7 +870,7 @@ def testComparison(self):
 
         self.assertFalse(filepath.FilePath(b"z") != filepath.FilePath(b"z"))
 
-    def test_descendantOnly(self):
+    def test_descendantOnly(self) -> None:
         """
         If C{".."} is in the sequence passed to L{FilePath.descendant},
         L{InsecurePath} is raised.
@@ -830,7 +879,7 @@ def test_descendantOnly(self):
             filepath.InsecurePath, self.path.descendant, ["mon\u20acy", ".."]
         )
 
-    def testSibling(self):
+    def testSibling(self) -> None:
         p = self.path.child(b"sibling_start")
         ts = p.sibling(b"sibling_test")
         self.assertEqual(ts.dirname(), p.dirname())
@@ -838,14 +887,14 @@ def testSibling(self):
         ts.createDirectory()
         self.assertIn(ts, self.path.children())
 
-    def testTemporarySibling(self):
+    def testTemporarySibling(self) -> None:
         ts = self.path.temporarySibling()
         self.assertEqual(ts.dirname(), self.path.dirname())
         self.assertNotIn(ts.basename(), self.path.listdir())
         ts.createDirectory()
         self.assertIn(ts, self.path.parent().children())
 
-    def test_temporarySiblingExtension(self):
+    def test_temporarySiblingExtension(self) -> None:
         """
         If L{FilePath.temporarySibling} is given an extension argument, it will
         produce path objects with that extension appended to their names.
@@ -854,10 +903,10 @@ def test_temporarySiblingExtension(self):
         ts = self.path.temporarySibling(testExtension)
         self.assertTrue(
             ts.basename().endswith(testExtension),
-            f"{ts.basename()} does not end with {testExtension}",
+            f"{ts.basename()!r} does not end with {testExtension!r}",
         )
 
-    def test_removeDirectory(self):
+    def test_removeDirectory(self) -> None:
         """
         L{FilePath.remove} on a L{FilePath} that refers to a directory will
         recursively delete its contents.
@@ -866,7 +915,7 @@ def test_removeDirectory(self):
         self.assertFalse(self.path.exists())
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_removeWithSymlink(self):
+    def test_removeWithSymlink(self) -> None:
         """
         For a path which is a symbolic link, L{FilePath.remove} just deletes
         the link, not the target.
@@ -878,7 +927,7 @@ def test_removeWithSymlink(self):
         self.assertFalse(link.exists())
         self.assertTrue(self.path.child(b"sub1").exists())
 
-    def test_copyToDirectory(self):
+    def test_copyToDirectory(self) -> None:
         """
         L{FilePath.copyTo} makes a copy of all the contents of the directory
         named by that L{FilePath} if it is able to do so.
@@ -893,7 +942,7 @@ def test_copyToDirectory(self):
         oldPaths.sort()
         self.assertEqual(newPaths, oldPaths)
 
-    def test_copyToMissingDestFileClosing(self):
+    def test_copyToMissingDestFileClosing(self) -> None:
         """
         If an exception is raised while L{FilePath.copyTo} is trying to open
         source file to read from, the destination file is closed and the
@@ -903,7 +952,7 @@ def test_copyToMissingDestFileClosing(self):
         # Make it look like something to copy, even though it doesn't exist.
         # This could happen if the file is deleted between the isfile check and
         # the file actually being opened.
-        nosuch.isfile = lambda: True
+        nosuch.isfile = lambda: True  # type:ignore
 
         # We won't get as far as writing to this file, but it's still useful for
         # tracking whether we closed it.
@@ -912,7 +961,7 @@ def test_copyToMissingDestFileClosing(self):
         self.assertRaises(IOError, nosuch.copyTo, destination)
         self.assertTrue(destination.fp.closed)
 
-    def test_copyToFileClosing(self):
+    def test_copyToFileClosing(self) -> None:
         """
         If an exception is raised while L{FilePath.copyTo} is copying bytes
         between two regular files, the source and destination files are closed
@@ -924,7 +973,7 @@ def test_copyToFileClosing(self):
         self.assertTrue(source.fp.closed)
         self.assertTrue(destination.fp.closed)
 
-    def test_copyToDirectoryItself(self):
+    def test_copyToDirectoryItself(self) -> None:
         """
         L{FilePath.copyTo} fails with an OSError or IOError (depending on
         platform, as it propagates errors from open() and write()) when
@@ -935,7 +984,7 @@ def test_copyToDirectoryItself(self):
         )
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_copyToWithSymlink(self):
+    def test_copyToWithSymlink(self) -> None:
         """
         Verify that copying with followLinks=True copies symlink targets
         instead of symlinks
@@ -950,7 +999,7 @@ def test_copyToWithSymlink(self):
         )
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_copyToWithoutSymlink(self):
+    def test_copyToWithoutSymlink(self) -> None:
         """
         Verify that copying with followLinks=False copies symlinks as symlinks
         """
@@ -963,7 +1012,7 @@ def test_copyToWithoutSymlink(self):
             os.readlink(fp.child(b"link1").path),
         )
 
-    def test_copyToMissingSource(self):
+    def test_copyToMissingSource(self) -> None:
         """
         If the source path is missing, L{FilePath.copyTo} raises L{OSError}.
         """
@@ -971,7 +1020,7 @@ def test_copyToMissingSource(self):
         exc = self.assertRaises(OSError, path.copyTo, b"some other path")
         self.assertEqual(exc.errno, errno.ENOENT)
 
-    def test_moveTo(self):
+    def test_moveTo(self) -> None:
         """
         Verify that moving an entire directory results into another directory
         with the same content.
@@ -985,7 +1034,7 @@ def test_moveTo(self):
         oldPaths.sort()
         self.assertEqual(newPaths, oldPaths)
 
-    def test_moveToExistsCache(self):
+    def test_moveToExistsCache(self) -> None:
         """
         A L{FilePath} that has been moved aside with L{FilePath.moveTo} no
         longer registers as existing.  Its previously non-existent target
@@ -1005,7 +1054,7 @@ def test_moveToExistsCache(self):
         self.assertFalse(fp.exists())
         self.assertTrue(fp2.exists())
 
-    def test_moveToExistsCacheCrossMount(self):
+    def test_moveToExistsCacheCrossMount(self) -> None:
         """
         The assertion of test_moveToExistsCache should hold in the case of a
         cross-mount move.
@@ -1013,7 +1062,7 @@ def test_moveToExistsCacheCrossMount(self):
         self.setUpFaultyRename()
         self.test_moveToExistsCache()
 
-    def test_moveToSizeCache(self, hook=lambda: None):
+    def test_moveToSizeCache(self, hook: Callable[[], None] = lambda: None) -> None:
         """
         L{FilePath.moveTo} clears its destination's status cache, such that
         calls to L{FilePath.getsize} after the call to C{moveTo} will report the
@@ -1044,14 +1093,18 @@ def test_moveToSizeCache(self, hook=lambda: None):
         fp.moveTo(fp2)
         self.assertEqual(fp2.getsize(), 4)
 
-    def test_moveToSizeCacheCrossMount(self):
+    def test_moveToSizeCacheCrossMount(self) -> None:
         """
         The assertion of test_moveToSizeCache should hold in the case of a
         cross-mount move.
         """
-        self.test_moveToSizeCache(hook=self.setUpFaultyRename)
 
-    def test_moveToError(self):
+        def setUp() -> None:
+            self.setUpFaultyRename()
+
+        self.test_moveToSizeCache(hook=setUp)
+
+    def test_moveToError(self) -> None:
         """
         Verify error behavior of moveTo: it should raises one of OSError or
         IOError if you want to move a path into one of its child. It's simply
@@ -1061,7 +1114,7 @@ def test_moveToError(self):
             (OSError, IOError), self.path.moveTo, self.path.child(b"file1")
         )
 
-    def setUpFaultyRename(self):
+    def setUpFaultyRename(self) -> List[Tuple[str, str]]:
         """
         Set up a C{os.rename} that will fail with L{errno.EXDEV} on first call.
         This is used to simulate a cross-device rename failure.
@@ -1071,7 +1124,7 @@ def setUpFaultyRename(self):
         """
         invokedWith = []
 
-        def faultyRename(src, dest):
+        def faultyRename(src: str, dest: str) -> None:
             invokedWith.append((src, dest))
             if len(invokedWith) == 1:
                 raise OSError(
@@ -1084,7 +1137,7 @@ def faultyRename(src, dest):
         self.patch(os, "rename", faultyRename)
         return invokedWith
 
-    def test_crossMountMoveTo(self):
+    def test_crossMountMoveTo(self) -> None:
         """
         C{moveTo} should be able to handle C{EXDEV} error raised by
         C{os.rename} when trying to move a file on a different mounted
@@ -1100,7 +1153,7 @@ def test_crossMountMoveTo(self):
         self.assertTrue(invokedWith)
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_crossMountMoveToWithSymlink(self):
+    def test_crossMountMoveToWithSymlink(self) -> None:
         """
         By default, when moving a symlink, it should follow the link and
         actually copy the content of the linked node.
@@ -1115,7 +1168,7 @@ def test_crossMountMoveToWithSymlink(self):
         self.assertTrue(invokedWith)
 
     @skipIf(symlinkSkip, "Platform does not support symlinks")
-    def test_crossMountMoveToWithoutSymlink(self):
+    def test_crossMountMoveToWithoutSymlink(self) -> None:
         """
         Verify that moveTo called with followLinks=False actually create
         another symlink.
@@ -1129,7 +1182,7 @@ def test_crossMountMoveToWithoutSymlink(self):
         self.assertEqual(f3.getContent(), b"file 1")
         self.assertTrue(invokedWith)
 
-    def test_createBinaryMode(self):
+    def test_createBinaryMode(self) -> None:
         """
         L{FilePath.create} should always open (and write to) files in binary
         mode; line-feed octets should be unmodified.
@@ -1146,7 +1199,7 @@ def test_createBinaryMode(self):
             read = fp.read()
             self.assertEqual(read, b"\n")
 
-    def testOpen(self):
+    def testOpen(self) -> None:
         # Opening a file for reading when it does not already exist is an error
         nonexistent = self.path.child(b"nonexistent")
         e = self.assertRaises(IOError, nonexistent.open)
@@ -1225,7 +1278,7 @@ def testOpen(self):
         del nonexistent
         self.assertRaises((OSError, IOError), existent.open)
 
-    def test_openWithExplicitBinaryMode(self):
+    def test_openWithExplicitBinaryMode(self) -> None:
         """
         Due to a bug in Python 2.7 on Windows including multiple 'b'
         characters in the mode passed to the built-in open() will cause an
@@ -1235,11 +1288,12 @@ def test_openWithExplicitBinaryMode(self):
         See http://bugs.python.org/issue7686 for details about the bug.
         """
         writer = self.path.child(b"explicit-binary")
-        with writer.open("wb") as file:
+        opener = writer.open("wb")  # type:ignore[arg-type]
+        with opener as file:
             file.write(b"abc\ndef")
         self.assertTrue(writer.exists)
 
-    def test_openWithRedundantExplicitBinaryModes(self):
+    def test_openWithRedundantExplicitBinaryModes(self) -> None:
         """
         Due to a bug in Python 2.7 on Windows including multiple 'b'
         characters in the mode passed to the built-in open() will cause an
@@ -1250,11 +1304,12 @@ def test_openWithRedundantExplicitBinaryModes(self):
         See http://bugs.python.org/issue7686 for details about the bug.
         """
         writer = self.path.child(b"multiple-binary")
-        with writer.open("wbb") as file:
+        opener = writer.open("wbb")  # type:ignore[arg-type]
+        with opener as file:
             file.write(b"abc\ndef")
         self.assertTrue(writer.exists)
 
-    def test_existsCache(self):
+    def test_existsCache(self) -> None:
         """
         Check that C{filepath.FilePath.exists} correctly restat the object if
         an operation has occurred in the mean time.
@@ -1265,7 +1320,7 @@ def test_existsCache(self):
         fp.makedirs()
         self.assertTrue(fp.exists())
 
-    def test_makedirsMakesDirectoriesRecursively(self):
+    def test_makedirsMakesDirectoriesRecursively(self) -> None:
         """
         C{FilePath.makedirs} creates a directory at C{path}}, including
         recursively creating all parent directories leading up to the path.
@@ -1278,7 +1333,7 @@ def test_makedirsMakesDirectoriesRecursively(self):
         self.assertTrue(fp.exists())
         self.assertTrue(fp.isdir())
 
-    def test_makedirsMakesDirectoriesWithIgnoreExistingDirectory(self):
+    def test_makedirsMakesDirectoriesWithIgnoreExistingDirectory(self) -> None:
         """
         Calling C{FilePath.makedirs} with C{ignoreExistingDirectory} set to
         C{True} has no effect if directory does not exist.
@@ -1291,7 +1346,7 @@ def test_makedirsMakesDirectoriesWithIgnoreExistingDirectory(self):
         self.assertTrue(fp.exists())
         self.assertTrue(fp.isdir())
 
-    def test_makedirsThrowsWithExistentDirectory(self):
+    def test_makedirsThrowsWithExistentDirectory(self) -> None:
         """
         C{FilePath.makedirs} throws an C{OSError} exception
         when called on a directory that already exists.
@@ -1303,7 +1358,7 @@ def test_makedirsThrowsWithExistentDirectory(self):
 
         self.assertEqual(exception.errno, errno.EEXIST)
 
-    def test_makedirsAcceptsIgnoreExistingDirectory(self):
+    def test_makedirsAcceptsIgnoreExistingDirectory(self) -> None:
         """
         C{FilePath.makedirs} succeeds when called on a directory that already
         exists and the c{ignoreExistingDirectory} argument is set to C{True}.
@@ -1316,7 +1371,7 @@ def test_makedirsAcceptsIgnoreExistingDirectory(self):
 
         self.assertTrue(fp.exists())
 
-    def test_makedirsIgnoreExistingDirectoryExistAlreadyAFile(self):
+    def test_makedirsIgnoreExistingDirectoryExistAlreadyAFile(self) -> None:
         """
         When C{FilePath.makedirs} is called with C{ignoreExistingDirectory} set
         to C{True} it throws an C{OSError} exceptions if path is a file.
@@ -1331,14 +1386,14 @@ def test_makedirsIgnoreExistingDirectoryExistAlreadyAFile(self):
 
         self.assertEqual(exception.errno, errno.EEXIST)
 
-    def test_makedirsRaisesNonEexistErrorsIgnoreExistingDirectory(self):
+    def test_makedirsRaisesNonEexistErrorsIgnoreExistingDirectory(self) -> None:
         """
         When C{FilePath.makedirs} is called with C{ignoreExistingDirectory} set
         to C{True} it raises an C{OSError} exception if exception errno is not
         EEXIST.
         """
 
-        def faultyMakedirs(path):
+        def faultyMakedirs(path: str) -> None:
             raise OSError(errno.EACCES, "Permission Denied")
 
         self.patch(os, "makedirs", faultyMakedirs)
@@ -1350,7 +1405,7 @@ def faultyMakedirs(path):
 
         self.assertEqual(exception.errno, errno.EACCES)
 
-    def test_changed(self):
+    def test_changed(self) -> None:
         """
         L{FilePath.changed} indicates that the L{FilePath} has changed, but does
         not re-read the status information from the filesystem until it is
@@ -1373,7 +1428,7 @@ def test_changed(self):
         self.assertEqual(fp.getsize(), 8)
 
     @skipIf(platform.isWindows(), "Test does not run on Windows")
-    def test_getPermissions_POSIX(self):
+    def test_getPermissions_POSIX(self) -> None:
         """
         Getting permissions for a file returns a L{Permissions} object for
         POSIX platforms (which supports separate user, group, and other
@@ -1389,7 +1444,7 @@ def test_getPermissions_POSIX(self):
             self.path.child(b"sub1").getPermissions().shorthand(), "rwxrw-r--"
         )
 
-    def test_filePathNotDeprecated(self):
+    def test_filePathNotDeprecated(self) -> None:
         """
         While accessing L{twisted.python.filepath.FilePath.statinfo} is
         deprecated, the filepath itself is not.
@@ -1399,7 +1454,7 @@ def test_filePathNotDeprecated(self):
         self.assertEqual(warningInfo, [])
 
     @skipIf(not platform.isWindows(), "Test will run only on Windows")
-    def test_getPermissions_Windows(self):
+    def test_getPermissions_Windows(self) -> None:
         """
         Getting permissions for a file returns a L{Permissions} object in
         Windows.  Windows requires a different test, because user permissions
@@ -1421,7 +1476,7 @@ def test_getPermissions_Windows(self):
             self.path.child(b"sub1").getPermissions().shorthand(), "r-xr-xr-x"
         )
 
-    def test_whetherBlockOrSocket(self):
+    def test_whetherBlockOrSocket(self) -> None:
         """
         Ensure that a file is not a block or socket
         """
@@ -1429,7 +1484,7 @@ def test_whetherBlockOrSocket(self):
         self.assertFalse(self.path.isSocket())
 
     @skipIf(not platform.isWindows(), "Test will run only on Windows")
-    def test_statinfoBitsNotImplementedInWindows(self):
+    def test_statinfoBitsNotImplementedInWindows(self) -> None:
         """
         Verify that certain file stats are not available on Windows
         """
@@ -1440,7 +1495,7 @@ def test_statinfoBitsNotImplementedInWindows(self):
         self.assertRaises(NotImplementedError, self.path.getGroupID)
 
     @skipIf(platform.isWindows(), "Test does not run on Windows")
-    def test_statinfoBitsAreNumbers(self):
+    def test_statinfoBitsAreNumbers(self) -> None:
         """
         Verify that file inode/device/nlinks/uid/gid stats are numbers in
         a POSIX environment
@@ -1456,7 +1511,7 @@ def test_statinfoBitsAreNumbers(self):
         self.assertEqual(self.path.getGroupID(), c.getGroupID())
 
     @skipIf(platform.isWindows(), "Test does not run on Windows")
-    def test_statinfoNumbersAreValid(self):
+    def test_statinfoNumbersAreValid(self) -> None:
         """
         Verify that the right numbers come back from the right accessor methods
         for file inode/device/nlinks/uid/gid (in a POSIX environment)
@@ -1472,10 +1527,10 @@ class FakeStat:
         # monkey patch in a fake restat method for self.path
         fake = FakeStat()
 
-        def fakeRestat(*args, **kwargs):
-            self.path._statinfo = fake
+        def fakeRestat(*args: object, **kwargs: object) -> None:
+            self.path._statinfo = fake  # type:ignore
 
-        self.path.restat = fakeRestat
+        self.path.restat = fakeRestat  # type:ignore
 
         # ensure that restat will need to be called to get values
         self.path._statinfo = None
@@ -1492,7 +1547,7 @@ class SetContentTests(BytesTestCase):
     Tests for L{FilePath.setContent}.
     """
 
-    def test_write(self):
+    def test_write(self) -> None:
         """
         Contents of the file referred to by a L{FilePath} can be written using
         L{FilePath.setContent}.
@@ -1504,16 +1559,16 @@ def test_write(self):
             contents = fObj.read()
         self.assertEqual(b"hello, world", contents)
 
-    def test_fileClosing(self):
+    def test_fileClosing(self) -> None:
         """
         If writing to the underlying file raises an exception,
         L{FilePath.setContent} raises that exception after closing the file.
         """
-        fp = ExplodingFilePath(b"")
+        fp = ExplodingFilePath("")
         self.assertRaises(IOError, fp.setContent, b"blah")
         self.assertTrue(fp.fp.closed)
 
-    def test_nameCollision(self):
+    def test_nameCollision(self) -> None:
         """
         L{FilePath.setContent} will use a different temporary filename on each
         invocation, so that multiple processes, threads, or reentrant
@@ -1529,7 +1584,7 @@ def test_nameCollision(self):
         self.assertEqual(len(openedSiblings), 2)
         self.assertNotEqual(openedSiblings[0], openedSiblings[1])
 
-    def _assertOneOpened(self, fp, extension):
+    def _assertOneOpened(self, fp: TrackingFilePath[AnyStr], extension: str) -> None:
         """
         Assert that the L{TrackingFilePath} C{fp} was used to open one sibling
         with the given extension.
@@ -1540,7 +1595,7 @@ def _assertOneOpened(self, fp, extension):
 
         @param extension: The extension the sibling path is expected to have
             had.
-        @type extension: L{bytes}
+        @type extension: L{str}
 
         @raise: C{self.failureException} is raised if the extension of the
             opened file is incorrect or if not exactly one file was opened
@@ -1549,22 +1604,22 @@ def _assertOneOpened(self, fp, extension):
         opened = fp.openedPaths()
         self.assertEqual(len(opened), 1, "expected exactly one opened file")
         self.assertTrue(
-            opened[0].basename().endswith(extension),
-            "{} does not end with {!r} extension".format(
+            opened[0].asTextMode().basename().endswith(extension),
+            "{!r} does not end with {!r} extension".format(
                 opened[0].basename(), extension
             ),
         )
 
-    def test_defaultExtension(self):
+    def test_defaultExtension(self) -> None:
         """
         L{FilePath.setContent} creates temporary files with the extension
         I{.new} if no alternate extension value is given.
         """
         fp = TrackingFilePath(self.mktemp())
         fp.setContent(b"hello")
-        self._assertOneOpened(fp, b".new")
+        self._assertOneOpened(fp, ".new")
 
-    def test_customExtension(self):
+    def test_customExtension(self) -> None:
         """
         L{FilePath.setContent} creates temporary files with a user-supplied
         extension so that if it is somehow interrupted while writing them the
@@ -1572,7 +1627,7 @@ def test_customExtension(self):
         """
         fp = TrackingFilePath(self.mktemp())
         fp.setContent(b"goodbye", b"-something-else")
-        self._assertOneOpened(fp, b"-something-else")
+        self._assertOneOpened(fp, "-something-else")
 
 
 class UnicodeFilePathTests(TestCase):
@@ -1581,7 +1636,7 @@ class UnicodeFilePathTests(TestCase):
     were instantiated with.
     """
 
-    def test_UnicodeInstantiation(self):
+    def test_UnicodeInstantiation(self) -> None:
         """
         L{FilePath} instantiated with a text path will return a text-mode
         FilePath.
@@ -1589,7 +1644,7 @@ def test_UnicodeInstantiation(self):
         fp = filepath.FilePath("./mon\u20acy")
         self.assertEqual(type(fp.path), str)
 
-    def test_UnicodeInstantiationBytesChild(self):
+    def test_UnicodeInstantiationBytesChild(self) -> None:
         """
         Calling L{FilePath.child} on a text-mode L{FilePath} with a L{bytes}
         subpath will return a bytes-mode FilePath.
@@ -1598,7 +1653,7 @@ def test_UnicodeInstantiationBytesChild(self):
         child = fp.child("child-mon\u20acy".encode())
         self.assertEqual(type(child.path), bytes)
 
-    def test_UnicodeInstantiationUnicodeChild(self):
+    def test_UnicodeInstantiationUnicodeChild(self) -> None:
         """
         Calling L{FilePath.child} on a text-mode L{FilePath} with a text
         subpath will return a text-mode FilePath.
@@ -1607,7 +1662,7 @@ def test_UnicodeInstantiationUnicodeChild(self):
         child = fp.child("mon\u20acy")
         self.assertEqual(type(child.path), str)
 
-    def test_UnicodeInstantiationUnicodePreauthChild(self):
+    def test_UnicodeInstantiationUnicodePreauthChild(self) -> None:
         """
         Calling L{FilePath.preauthChild} on a text-mode L{FilePath} with a text
         subpath will return a text-mode FilePath.
@@ -1616,7 +1671,7 @@ def test_UnicodeInstantiationUnicodePreauthChild(self):
         child = fp.preauthChild("mon\u20acy")
         self.assertEqual(type(child.path), str)
 
-    def test_UnicodeInstantiationBytesPreauthChild(self):
+    def test_UnicodeInstantiationBytesPreauthChild(self) -> None:
         """
         Calling L{FilePath.preauthChild} on a text-mode L{FilePath} with a bytes
         subpath will return a bytes-mode FilePath.
@@ -1625,7 +1680,7 @@ def test_UnicodeInstantiationBytesPreauthChild(self):
         child = fp.preauthChild("child-mon\u20acy".encode())
         self.assertEqual(type(child.path), bytes)
 
-    def test_BytesInstantiation(self):
+    def test_BytesInstantiation(self) -> None:
         """
         L{FilePath} instantiated with a L{bytes} path will return a bytes-mode
         FilePath.
@@ -1633,7 +1688,7 @@ def test_BytesInstantiation(self):
         fp = filepath.FilePath(b"./")
         self.assertEqual(type(fp.path), bytes)
 
-    def test_BytesInstantiationBytesChild(self):
+    def test_BytesInstantiationBytesChild(self) -> None:
         """
         Calling L{FilePath.child} on a bytes-mode L{FilePath} with a bytes
         subpath will return a bytes-mode FilePath.
@@ -1642,7 +1697,7 @@ def test_BytesInstantiationBytesChild(self):
         child = fp.child("child-mon\u20acy".encode())
         self.assertEqual(type(child.path), bytes)
 
-    def test_BytesInstantiationUnicodeChild(self):
+    def test_BytesInstantiationUnicodeChild(self) -> None:
         """
         Calling L{FilePath.child} on a bytes-mode L{FilePath} with a text
         subpath will return a text-mode FilePath.
@@ -1651,7 +1706,7 @@ def test_BytesInstantiationUnicodeChild(self):
         child = fp.child("mon\u20acy")
         self.assertEqual(type(child.path), str)
 
-    def test_BytesInstantiationBytesPreauthChild(self):
+    def test_BytesInstantiationBytesPreauthChild(self) -> None:
         """
         Calling L{FilePath.preauthChild} on a bytes-mode L{FilePath} with a
         bytes subpath will return a bytes-mode FilePath.
@@ -1660,7 +1715,7 @@ def test_BytesInstantiationBytesPreauthChild(self):
         child = fp.preauthChild("child-mon\u20acy".encode())
         self.assertEqual(type(child.path), bytes)
 
-    def test_BytesInstantiationUnicodePreauthChild(self):
+    def test_BytesInstantiationUnicodePreauthChild(self) -> None:
         """
         Calling L{FilePath.preauthChild} on a bytes-mode L{FilePath} with a text
         subpath will return a text-mode FilePath.
@@ -1670,7 +1725,7 @@ def test_BytesInstantiationUnicodePreauthChild(self):
         self.assertEqual(type(child.path), str)
 
     @skipIf(platform.isWindows(), "Test will not work on Windows")
-    def test_unicoderepr(self):
+    def test_unicoderepr(self) -> None:
         """
         The repr of a L{unicode} L{FilePath} shouldn't burst into flames.
         """
@@ -1679,7 +1734,7 @@ def test_unicoderepr(self):
         self.assertEqual("FilePath('/mon\u20acy')", reprOutput)
 
     @skipIf(platform.isWindows(), "Test will not work on Windows")
-    def test_bytesrepr(self):
+    def test_bytesrepr(self) -> None:
         """
         The repr of a L{bytes} L{FilePath} shouldn't burst into flames.
         """
@@ -1688,7 +1743,7 @@ def test_bytesrepr(self):
         self.assertEqual("FilePath(b'/parent-mon\\xe2\\x82\\xacy')", reprOutput)
 
     @skipIf(not platform.isWindows(), "Test only works on Windows")
-    def test_unicodereprWindows(self):
+    def test_unicodereprWindows(self) -> None:
         """
         The repr of a L{unicode} L{FilePath} shouldn't burst into flames.
         """
@@ -1697,7 +1752,7 @@ def test_unicodereprWindows(self):
         self.assertEqual("FilePath('C:\\\\')", reprOutput)
 
     @skipIf(not platform.isWindows(), "Test only works on Windows")
-    def test_bytesreprWindows(self):
+    def test_bytesreprWindows(self) -> None:
         """
         The repr of a L{bytes} L{FilePath} shouldn't burst into flames.
         """
@@ -1705,7 +1760,7 @@ def test_bytesreprWindows(self):
         reprOutput = repr(fp)
         self.assertEqual("FilePath(b'C:\\\\')", reprOutput)
 
-    def test_mixedTypeGlobChildren(self):
+    def test_mixedTypeGlobChildren(self) -> None:
         """
         C{globChildren} will return the same type as the pattern argument.
         """
@@ -1713,7 +1768,7 @@ def test_mixedTypeGlobChildren(self):
         children = fp.globChildren(b"*")
         self.assertIsInstance(children[0].path, bytes)
 
-    def test_unicodeGlobChildren(self):
+    def test_unicodeGlobChildren(self) -> None:
         """
         C{globChildren} works with L{unicode}.
         """
@@ -1721,21 +1776,21 @@ def test_unicodeGlobChildren(self):
         children = fp.globChildren("*")
         self.assertIsInstance(children[0].path, str)
 
-    def test_unicodeBasename(self):
+    def test_unicodeBasename(self) -> None:
         """
         Calling C{basename} on an text- L{FilePath} returns L{unicode}.
         """
         fp = filepath.FilePath("./")
         self.assertIsInstance(fp.basename(), str)
 
-    def test_unicodeDirname(self):
+    def test_unicodeDirname(self) -> None:
         """
         Calling C{dirname} on a text-mode L{FilePath} returns L{unicode}.
         """
         fp = filepath.FilePath("./")
         self.assertIsInstance(fp.dirname(), str)
 
-    def test_unicodeParent(self):
+    def test_unicodeParent(self) -> None:
         """
         Calling C{parent} on a text-mode L{FilePath} will return a text-mode
         L{FilePath}.
@@ -1744,7 +1799,7 @@ def test_unicodeParent(self):
         parent = fp.parent()
         self.assertIsInstance(parent.path, str)
 
-    def test_mixedTypeTemporarySibling(self):
+    def test_mixedTypeTemporarySibling(self) -> None:
         """
         A L{bytes} extension to C{temporarySibling} will mean a L{bytes} mode
         L{FilePath} is returned.
@@ -1753,7 +1808,7 @@ def test_mixedTypeTemporarySibling(self):
         tempSibling = fp.temporarySibling(b".txt")
         self.assertIsInstance(tempSibling.path, bytes)
 
-    def test_unicodeTemporarySibling(self):
+    def test_unicodeTemporarySibling(self) -> None:
         """
         A L{unicode} extension to C{temporarySibling} will mean a L{unicode}
         mode L{FilePath} is returned.
@@ -1762,7 +1817,7 @@ def test_unicodeTemporarySibling(self):
         tempSibling = fp.temporarySibling(".txt")
         self.assertIsInstance(tempSibling.path, str)
 
-    def test_mixedTypeSiblingExtensionSearch(self):
+    def test_mixedTypeSiblingExtensionSearch(self) -> None:
         """
         C{siblingExtensionSearch} called with L{bytes} on a L{unicode}-mode
         L{FilePath} will return a L{list} of L{bytes}-mode L{FilePath}s.
@@ -1771,11 +1826,12 @@ def test_mixedTypeSiblingExtensionSearch(self):
         sibling = filepath.FilePath(fp._asTextPath() + ".txt")
         sibling.touch()
         newPath = fp.siblingExtensionSearch(b".txt")
+        assert newPath is not None
 
         self.assertIsInstance(newPath, filepath.FilePath)
         self.assertIsInstance(newPath.path, bytes)
 
-    def test_unicodeSiblingExtensionSearch(self):
+    def test_unicodeSiblingExtensionSearch(self) -> None:
         """
         C{siblingExtensionSearch} called with L{unicode} on a L{unicode}-mode
         L{FilePath} will return a L{list} of L{unicode}-mode L{FilePath}s.
@@ -1785,11 +1841,12 @@ def test_unicodeSiblingExtensionSearch(self):
         sibling.touch()
 
         newPath = fp.siblingExtensionSearch(".txt")
+        assert newPath is not None
 
         self.assertIsInstance(newPath, filepath.FilePath)
         self.assertIsInstance(newPath.path, str)
 
-    def test_mixedTypeSiblingExtension(self):
+    def test_mixedTypeSiblingExtension(self) -> None:
         """
         C{siblingExtension} called with L{bytes} on a L{unicode}-mode
         L{FilePath} will return a L{bytes}-mode L{FilePath}.
@@ -1803,7 +1860,7 @@ def test_mixedTypeSiblingExtension(self):
         self.assertIsInstance(newPath, filepath.FilePath)
         self.assertIsInstance(newPath.path, bytes)
 
-    def test_unicodeSiblingExtension(self):
+    def test_unicodeSiblingExtension(self) -> None:
         """
         C{siblingExtension} called with L{unicode} on a L{unicode}-mode
         L{FilePath} will return a L{unicode}-mode L{FilePath}.
@@ -1817,7 +1874,20 @@ def test_unicodeSiblingExtension(self):
         self.assertIsInstance(newPath, filepath.FilePath)
         self.assertIsInstance(newPath.path, str)
 
-    def test_mixedTypeChildSearchPreauth(self):
+    def test_selfSiblingExtensionSearch(self) -> None:
+        """
+        C{siblingExtension} passed an empty string should return the same path,
+        in the type of its argument.
+        """
+        exists = filepath.FilePath(self.mktemp())
+        exists.touch()
+        notExists = filepath.FilePath(self.mktemp())
+        self.assertEqual(exists.siblingExtensionSearch(b""), exists.asBytesMode())
+        self.assertEqual(exists.siblingExtensionSearch(""), exists.asTextMode())
+        self.assertEqual(notExists.siblingExtensionSearch(""), None)
+        self.assertEqual(notExists.siblingExtensionSearch(b""), None)
+
+    def test_mixedTypeChildSearchPreauth(self) -> None:
         """
         C{childSearchPreauth} called with L{bytes} on a L{unicode}-mode
         L{FilePath} will return a L{bytes}-mode L{FilePath}.
@@ -1829,11 +1899,12 @@ def test_mixedTypeChildSearchPreauth(self):
         child.touch()
 
         newPath = fp.childSearchPreauth(b"text.txt")
+        assert newPath is not None
 
         self.assertIsInstance(newPath, filepath.FilePath)
         self.assertIsInstance(newPath.path, bytes)
 
-    def test_unicodeChildSearchPreauth(self):
+    def test_unicodeChildSearchPreauth(self) -> None:
         """
         C{childSearchPreauth} called with L{unicode} on a L{unicode}-mode
         L{FilePath} will return a L{unicode}-mode L{FilePath}.
@@ -1845,11 +1916,12 @@ def test_unicodeChildSearchPreauth(self):
         child.touch()
 
         newPath = fp.childSearchPreauth("text.txt")
+        assert newPath is not None
 
         self.assertIsInstance(newPath, filepath.FilePath)
         self.assertIsInstance(newPath.path, str)
 
-    def test_asBytesModeFromUnicode(self):
+    def test_asBytesModeFromUnicode(self) -> None:
         """
         C{asBytesMode} on a L{unicode}-mode L{FilePath} returns a new
         L{bytes}-mode L{FilePath}.
@@ -1859,7 +1931,7 @@ def test_asBytesModeFromUnicode(self):
         self.assertIsNot(fp, newfp)
         self.assertIsInstance(newfp.path, bytes)
 
-    def test_asTextModeFromBytes(self):
+    def test_asTextModeFromBytes(self) -> None:
         """
         C{asBytesMode} on a L{unicode}-mode L{FilePath} returns a new
         L{bytes}-mode L{FilePath}.
@@ -1869,7 +1941,7 @@ def test_asTextModeFromBytes(self):
         self.assertIsNot(fp, newfp)
         self.assertIsInstance(newfp.path, str)
 
-    def test_asBytesModeFromBytes(self):
+    def test_asBytesModeFromBytes(self) -> None:
         """
         C{asBytesMode} on a L{bytes}-mode L{FilePath} returns the same
         L{bytes}-mode L{FilePath}.
@@ -1879,7 +1951,7 @@ def test_asBytesModeFromBytes(self):
         self.assertIs(fp, newfp)
         self.assertIsInstance(newfp.path, bytes)
 
-    def test_asTextModeFromUnicode(self):
+    def test_asTextModeFromUnicode(self) -> None:
         """
         C{asTextMode} on a L{unicode}-mode L{FilePath} returns the same
         L{unicode}-mode L{FilePath}.
@@ -1889,7 +1961,7 @@ def test_asTextModeFromUnicode(self):
         self.assertIs(fp, newfp)
         self.assertIsInstance(newfp.path, str)
 
-    def test_asBytesModeFromUnicodeWithEncoding(self):
+    def test_asBytesModeFromUnicodeWithEncoding(self) -> None:
         """
         C{asBytesMode} with an C{encoding} argument uses that encoding when
         coercing the L{unicode}-mode L{FilePath} to a L{bytes}-mode L{FilePath}.
@@ -1898,7 +1970,7 @@ def test_asBytesModeFromUnicodeWithEncoding(self):
         newfp = fp.asBytesMode(encoding="utf-8")
         self.assertIn(b"\xe2\x98\x83", newfp.path)
 
-    def test_asTextModeFromBytesWithEncoding(self):
+    def test_asTextModeFromBytesWithEncoding(self) -> None:
         """
         C{asTextMode} with an C{encoding} argument uses that encoding when
         coercing the L{bytes}-mode L{FilePath} to a L{unicode}-mode L{FilePath}.
@@ -1907,7 +1979,7 @@ def test_asTextModeFromBytesWithEncoding(self):
         newfp = fp.asTextMode(encoding="utf-8")
         self.assertIn("\u2603", newfp.path)
 
-    def test_asBytesModeFromUnicodeWithUnusableEncoding(self):
+    def test_asBytesModeFromUnicodeWithUnusableEncoding(self) -> None:
         """
         C{asBytesMode} with an C{encoding} argument that can't be used to encode
         the unicode path raises a L{UnicodeError}.
@@ -1916,11 +1988,11 @@ def test_asBytesModeFromUnicodeWithUnusableEncoding(self):
         with self.assertRaises(UnicodeError):
             fp.asBytesMode(encoding="ascii")
 
-    def test_asTextModeFromBytesWithUnusableEncoding(self):
+    def test_asTextModeFromBytesWithUnusableEncoding(self) -> None:
         """
         C{asTextMode} with an C{encoding} argument that can't be used to encode
         the unicode path raises a L{UnicodeError}.
         """
-        fp = filepath.FilePath(br"\u2603")
+        fp = filepath.FilePath(rb"\u2603")
         with self.assertRaises(UnicodeError):
             fp.asTextMode(encoding="utf-32")
diff --git a/src/twisted/trial/test/test_script.py b/src/twisted/trial/test/test_script.py
--- a/src/twisted/trial/test/test_script.py
+++ b/src/twisted/trial/test/test_script.py
@@ -16,7 +16,7 @@
 
 from twisted.logger import Logger
 from twisted.python import util
-from twisted.python.filepath import FilePath
+from twisted.python.filepath import FilePath, IFilePath
 from twisted.python.usage import UsageError
 from twisted.scripts import trial
 from twisted.trial import unittest
@@ -91,7 +91,7 @@ def test_default(self, workingDirectory: str) -> None:
         file constructed from a default value.
         """
         config = runFromArguments(["--temp-directory", workingDirectory])
-        logPath = FilePath(workingDirectory).preauthChild(config["logfile"])
+        logPath: IFilePath = FilePath(workingDirectory).preauthChild(config["logfile"])
         assert_that(logPath, fileContents(contains_string("something")))
 
     @given(
@@ -109,7 +109,7 @@ def test_relativePath(self, logfile: str) -> None:
         logs are written there.
         """
         config = runFromArguments(["--logfile", logfile])
-        logPath = FilePath(config["temp-directory"]).preauthChild(logfile)
+        logPath: IFilePath = FilePath(config["temp-directory"]).preauthChild(logfile)
         assert_that(logPath, fileContents(contains_string("something")))
 
     @given(
@@ -131,7 +131,8 @@ def test_absolutePath(self, logfile: str) -> None:
         # directory.
         logPath = FilePath(".").preauthChild(logfile)
         runFromArguments(["--logfile", logPath.path])
-        assert_that(logPath, fileContents(contains_string("something")))
+        iPath: IFilePath = logPath
+        assert_that(iPath, fileContents(contains_string("something")))
 
 
 class ForceGarbageCollectionTests(unittest.SynchronousTestCase):
diff --git a/src/twisted/web/test/test_template.py b/src/twisted/web/test/test_template.py
--- a/src/twisted/web/test/test_template.py
+++ b/src/twisted/web/test/test_template.py
@@ -330,7 +330,7 @@ def loaderFactory(self) -> ITemplateLoader:
         """
         fp = FilePath(self.mktemp())
         fp.setContent(self.templateString.encode("utf8"))
-        return XMLFile(fp.path)  # type: ignore[arg-type]
+        return XMLFile(fp.path)
 
 
 class FlattenIntegrationTests(FlattenTestCase):