1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5#      http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10# See the License for the specific language governing permissions and
11# limitations under the License.
12
13"""A fake implementation for pathlib working with FakeFilesystem.
14New in pyfakefs 3.0.
15
16Usage:
17
18* With fake_filesystem_unittest:
19  If using fake_filesystem_unittest.TestCase, pathlib gets replaced
20  by fake_pathlib together with other file system related modules.
21
22* Stand-alone with FakeFilesystem:
23  `filesystem = fake_filesystem.FakeFilesystem()`
24  `fake_pathlib_module = fake_filesystem.FakePathlibModule(filesystem)`
25  `path = fake_pathlib_module.Path('/foo/bar')`
26
27Note: as the implementation is based on FakeFilesystem, all faked classes
28(including PurePosixPath, PosixPath, PureWindowsPath and WindowsPath)
29get the properties of the underlying fake filesystem.
30"""
31import errno
32import fnmatch
33import functools
34import inspect
35import ntpath
36import os
37import pathlib
38import posixpath
39import re
40import sys
41from pathlib import PurePath
42from typing import Callable
43from urllib.parse import quote_from_bytes as urlquote_from_bytes
44
45from pyfakefs import fake_scandir
46from pyfakefs.extra_packages import use_scandir
47from pyfakefs.fake_filesystem import FakeFilesystem
48from pyfakefs.fake_open import FakeFileOpen
49from pyfakefs.fake_os import FakeOsModule, use_original_os
50from pyfakefs.helpers import IS_PYPY
51
52
53def init_module(filesystem):
54    """Initializes the fake module with the fake file system."""
55    # pylint: disable=protected-access
56    FakePath.filesystem = filesystem
57    if sys.version_info < (3, 12):
58        FakePathlibModule.PureWindowsPath._flavour = _FakeWindowsFlavour(filesystem)
59        FakePathlibModule.PurePosixPath._flavour = _FakePosixFlavour(filesystem)
60    else:
61        # in Python 3.12, the flavour is no longer an own class,
62        # but points to the os-specific path module (posixpath/ntpath)
63        fake_os = FakeOsModule(filesystem)
64        fake_path = fake_os.path
65        FakePathlibModule.PureWindowsPath._flavour = fake_path
66        FakePathlibModule.PurePosixPath._flavour = fake_path
67
68
69def _wrap_strfunc(strfunc):
70    @functools.wraps(strfunc)
71    def _wrapped(pathobj, *args, **kwargs):
72        return strfunc(pathobj.filesystem, str(pathobj), *args, **kwargs)
73
74    return staticmethod(_wrapped)
75
76
77def _wrap_binary_strfunc(strfunc):
78    @functools.wraps(strfunc)
79    def _wrapped(pathobj1, pathobj2, *args):
80        return strfunc(pathobj1.filesystem, str(pathobj1), str(pathobj2), *args)
81
82    return staticmethod(_wrapped)
83
84
85def _wrap_binary_strfunc_reverse(strfunc):
86    @functools.wraps(strfunc)
87    def _wrapped(pathobj1, pathobj2, *args):
88        return strfunc(pathobj2.filesystem, str(pathobj2), str(pathobj1), *args)
89
90    return staticmethod(_wrapped)
91
92
93try:
94    accessor = pathlib._Accessor  # type: ignore[attr-defined]
95except AttributeError:
96    accessor = object
97
98
99class _FakeAccessor(accessor):  # type: ignore[valid-type, misc]
100    """Accessor which forwards some of the functions to FakeFilesystem
101    methods.
102    """
103
104    stat = _wrap_strfunc(FakeFilesystem.stat)
105
106    lstat = _wrap_strfunc(
107        lambda fs, path: FakeFilesystem.stat(fs, path, follow_symlinks=False)
108    )
109
110    listdir = _wrap_strfunc(FakeFilesystem.listdir)
111
112    if use_scandir:
113        scandir = _wrap_strfunc(fake_scandir.scandir)
114
115    if hasattr(os, "lchmod"):
116        lchmod = _wrap_strfunc(
117            lambda fs, path, mode: FakeFilesystem.chmod(
118                fs, path, mode, follow_symlinks=False
119            )
120        )
121    else:
122
123        def lchmod(self, pathobj, *args, **kwargs):
124            """Raises not implemented for Windows systems."""
125            raise NotImplementedError("lchmod() not available on this system")
126
127    def chmod(self, pathobj, *args, **kwargs):
128        if "follow_symlinks" in kwargs:
129            if sys.version_info < (3, 10):
130                raise TypeError(
131                    "chmod() got an unexpected keyword " "argument 'follow_symlinks'"
132                )
133
134            if not kwargs["follow_symlinks"] and (
135                os.chmod not in os.supports_follow_symlinks or IS_PYPY
136            ):
137                raise NotImplementedError(
138                    "`follow_symlinks` for chmod() is not available " "on this system"
139                )
140        return pathobj.filesystem.chmod(str(pathobj), *args, **kwargs)
141
142    mkdir = _wrap_strfunc(FakeFilesystem.makedir)
143
144    unlink = _wrap_strfunc(FakeFilesystem.remove)
145
146    rmdir = _wrap_strfunc(FakeFilesystem.rmdir)
147
148    rename = _wrap_binary_strfunc(FakeFilesystem.rename)
149
150    replace = _wrap_binary_strfunc(
151        lambda fs, old_path, new_path: FakeFilesystem.rename(
152            fs, old_path, new_path, force_replace=True
153        )
154    )
155
156    symlink = _wrap_binary_strfunc_reverse(
157        lambda fs, fpath, target, target_is_dir: FakeFilesystem.create_symlink(
158            fs, fpath, target, create_missing_dirs=False
159        )
160    )
161
162    if (3, 8) <= sys.version_info:
163        link_to = _wrap_binary_strfunc(
164            lambda fs, file_path, link_target: FakeFilesystem.link(
165                fs, file_path, link_target
166            )
167        )
168
169    if sys.version_info >= (3, 10):
170        link = _wrap_binary_strfunc(
171            lambda fs, file_path, link_target: FakeFilesystem.link(
172                fs, file_path, link_target
173            )
174        )
175
176        # this will use the fake filesystem because os is patched
177        def getcwd(self):
178            return os.getcwd()
179
180    readlink = _wrap_strfunc(FakeFilesystem.readlink)
181
182    utime = _wrap_strfunc(FakeFilesystem.utime)
183
184
185_fake_accessor = _FakeAccessor()
186
187if sys.version_info < (3, 12):
188    flavour = pathlib._Flavour  # type: ignore[attr-defined]
189
190    class _FakeFlavour(flavour):  # type: ignore[valid-type, misc]
191        """Fake Flavour implementation used by PurePath and _Flavour"""
192
193        filesystem = None
194        sep = "/"
195        altsep = None
196        has_drv = False
197
198        ext_namespace_prefix = "\\\\?\\"
199
200        drive_letters = {chr(x) for x in range(ord("a"), ord("z") + 1)} | {
201            chr(x) for x in range(ord("A"), ord("Z") + 1)
202        }
203
204        def __init__(self, filesystem):
205            self.filesystem = filesystem
206            self.sep = filesystem.path_separator
207            self.altsep = filesystem.alternative_path_separator
208            self.has_drv = filesystem.is_windows_fs
209            super(_FakeFlavour, self).__init__()
210
211        @staticmethod
212        def _split_extended_path(path, ext_prefix=ext_namespace_prefix):
213            prefix = ""
214            if path.startswith(ext_prefix):
215                prefix = path[:4]
216                path = path[4:]
217                if path.startswith("UNC\\"):
218                    prefix += path[:3]
219                    path = "\\" + path[3:]
220            return prefix, path
221
222        def _splitroot_with_drive(self, path, sep):
223            first = path[0:1]
224            second = path[1:2]
225            if second == sep and first == sep:
226                # extended paths should also disable the collapsing of "."
227                # components (according to MSDN docs).
228                prefix, path = self._split_extended_path(path)
229                first = path[0:1]
230                second = path[1:2]
231            else:
232                prefix = ""
233            third = path[2:3]
234            if second == sep and first == sep and third != sep:
235                # is a UNC path:
236                # vvvvvvvvvvvvvvvvvvvvv root
237                # \\machine\mountpoint\directory\etc\...
238                #            directory ^^^^^^^^^^^^^^
239                index = path.find(sep, 2)
240                if index != -1:
241                    index2 = path.find(sep, index + 1)
242                    # a UNC path can't have two slashes in a row
243                    # (after the initial two)
244                    if index2 != index + 1:
245                        if index2 == -1:
246                            index2 = len(path)
247                        if prefix:
248                            return prefix + path[1:index2], sep, path[index2 + 1 :]
249                        return path[:index2], sep, path[index2 + 1 :]
250            drv = root = ""
251            if second == ":" and first in self.drive_letters:
252                drv = path[:2]
253                path = path[2:]
254                first = third
255            if first == sep:
256                root = first
257                path = path.lstrip(sep)
258            return prefix + drv, root, path
259
260        @staticmethod
261        def _splitroot_posix(path, sep):
262            if path and path[0] == sep:
263                stripped_part = path.lstrip(sep)
264                if len(path) - len(stripped_part) == 2:
265                    return "", sep * 2, stripped_part
266                return "", sep, stripped_part
267            else:
268                return "", "", path
269
270        def splitroot(self, path, sep=None):
271            """Split path into drive, root and rest."""
272            if sep is None:
273                sep = self.filesystem.path_separator
274            if self.filesystem.is_windows_fs:
275                return self._splitroot_with_drive(path, sep)
276            return self._splitroot_posix(path, sep)
277
278        def casefold(self, path):
279            """Return the lower-case version of s for a Windows filesystem."""
280            if self.filesystem.is_windows_fs:
281                return path.lower()
282            return path
283
284        def casefold_parts(self, parts):
285            """Return the lower-case version of parts for a Windows filesystem."""
286            if self.filesystem.is_windows_fs:
287                return [p.lower() for p in parts]
288            return parts
289
290        def _resolve_posix(self, path, strict):
291            sep = self.sep
292            seen = {}
293
294            def _resolve(path, rest):
295                if rest.startswith(sep):
296                    path = ""
297
298                for name in rest.split(sep):
299                    if not name or name == ".":
300                        # current dir
301                        continue
302                    if name == "..":
303                        # parent dir
304                        path, _, _ = path.rpartition(sep)
305                        continue
306                    newpath = path + sep + name
307                    if newpath in seen:
308                        # Already seen this path
309                        path = seen[newpath]
310                        if path is not None:
311                            # use cached value
312                            continue
313                        # The symlink is not resolved, so we must have
314                        # a symlink loop.
315                        raise RuntimeError("Symlink loop from %r" % newpath)
316                    # Resolve the symbolic link
317                    try:
318                        target = self.filesystem.readlink(newpath)
319                    except OSError as e:
320                        if e.errno != errno.EINVAL and strict:
321                            raise
322                        # Not a symlink, or non-strict mode. We just leave the path
323                        # untouched.
324                        path = newpath
325                    else:
326                        seen[newpath] = None  # not resolved symlink
327                        path = _resolve(path, target)
328                        seen[newpath] = path  # resolved symlink
329
330                return path
331
332            # NOTE: according to POSIX, getcwd() cannot contain path components
333            # which are symlinks.
334            base = "" if path.is_absolute() else self.filesystem.cwd
335            return _resolve(base, str(path)) or sep
336
337        def _resolve_windows(self, path, strict):
338            path = str(path)
339            if not path:
340                return os.getcwd()
341            previous_s = None
342            if strict:
343                if not self.filesystem.exists(path):
344                    self.filesystem.raise_os_error(errno.ENOENT, path)
345                return self.filesystem.resolve_path(path)
346            else:
347                while True:
348                    try:
349                        path = self.filesystem.resolve_path(path)
350                    except OSError:
351                        previous_s = path
352                        path = self.filesystem.splitpath(path)[0]
353                    else:
354                        if previous_s is None:
355                            return path
356                        return self.filesystem.joinpaths(
357                            path, os.path.basename(previous_s)
358                        )
359
360        def resolve(self, path, strict):
361            """Make the path absolute, resolving any symlinks."""
362            if self.filesystem.is_windows_fs:
363                return self._resolve_windows(path, strict)
364            return self._resolve_posix(path, strict)
365
366        def gethomedir(self, username):
367            """Return the home directory of the current user."""
368            if not username:
369                try:
370                    return os.environ["HOME"]
371                except KeyError:
372                    import pwd
373
374                    return pwd.getpwuid(os.getuid()).pw_dir
375            else:
376                import pwd
377
378                try:
379                    return pwd.getpwnam(username).pw_dir
380                except KeyError:
381                    raise RuntimeError(
382                        "Can't determine home directory " "for %r" % username
383                    )
384
385    class _FakeWindowsFlavour(_FakeFlavour):
386        """Flavour used by PureWindowsPath with some Windows specific
387        implementations independent of FakeFilesystem properties.
388        """
389
390        reserved_names = (
391            {"CON", "PRN", "AUX", "NUL"}
392            | {"COM%d" % i for i in range(1, 10)}
393            | {"LPT%d" % i for i in range(1, 10)}
394        )
395        pathmod = ntpath
396
397        def is_reserved(self, parts):
398            """Return True if the path is considered reserved under Windows."""
399
400            # NOTE: the rules for reserved names seem somewhat complicated
401            # (e.g. r"..\NUL" is reserved but not r"foo\NUL").
402            # We err on the side of caution and return True for paths which are
403            # not considered reserved by Windows.
404            if not parts:
405                return False
406            if self.filesystem.is_windows_fs and parts[0].startswith("\\\\"):
407                # UNC paths are never reserved
408                return False
409            return parts[-1].partition(".")[0].upper() in self.reserved_names
410
411        def make_uri(self, path):
412            """Return a file URI for the given path"""
413
414            # Under Windows, file URIs use the UTF-8 encoding.
415            # original version, not faked
416            drive = path.drive
417            if len(drive) == 2 and drive[1] == ":":
418                # It's a path on a local drive => 'file:///c:/a/b'
419                rest = path.as_posix()[2:].lstrip("/")
420                return "file:///%s/%s" % (
421                    drive,
422                    urlquote_from_bytes(rest.encode("utf-8")),
423                )
424            else:
425                # It's a path on a network drive => 'file://host/share/a/b'
426                return "file:" + urlquote_from_bytes(path.as_posix().encode("utf-8"))
427
428        def gethomedir(self, username):
429            """Return the home directory of the current user."""
430
431            # original version, not faked
432            if "HOME" in os.environ:
433                userhome = os.environ["HOME"]
434            elif "USERPROFILE" in os.environ:
435                userhome = os.environ["USERPROFILE"]
436            elif "HOMEPATH" in os.environ:
437                try:
438                    drv = os.environ["HOMEDRIVE"]
439                except KeyError:
440                    drv = ""
441                userhome = drv + os.environ["HOMEPATH"]
442            else:
443                raise RuntimeError("Can't determine home directory")
444
445            if username:
446                # Try to guess user home directory.  By default all users
447                # directories are located in the same place and are named by
448                # corresponding usernames.  If current user home directory points
449                # to nonstandard place, this guess is likely wrong.
450                if os.environ["USERNAME"] != username:
451                    drv, root, parts = self.parse_parts((userhome,))
452                    if parts[-1] != os.environ["USERNAME"]:
453                        raise RuntimeError(
454                            "Can't determine home directory " "for %r" % username
455                        )
456                    parts[-1] = username
457                    if drv or root:
458                        userhome = drv + root + self.join(parts[1:])
459                    else:
460                        userhome = self.join(parts)
461            return userhome
462
463        def compile_pattern(self, pattern):
464            return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch
465
466    class _FakePosixFlavour(_FakeFlavour):
467        """Flavour used by PurePosixPath with some Unix specific implementations
468        independent of FakeFilesystem properties.
469        """
470
471        pathmod = posixpath
472
473        def is_reserved(self, parts):
474            return False
475
476        def make_uri(self, path):
477            # We represent the path using the local filesystem encoding,
478            # for portability to other applications.
479            bpath = bytes(path)
480            return "file://" + urlquote_from_bytes(bpath)
481
482        def gethomedir(self, username):
483            # original version, not faked
484            if not username:
485                try:
486                    return os.environ["HOME"]
487                except KeyError:
488                    import pwd
489
490                    return pwd.getpwuid(os.getuid()).pw_dir
491            else:
492                import pwd
493
494                try:
495                    return pwd.getpwnam(username).pw_dir
496                except KeyError:
497                    raise RuntimeError(
498                        "Can't determine home directory " "for %r" % username
499                    )
500
501        def compile_pattern(self, pattern):
502            return re.compile(fnmatch.translate(pattern)).fullmatch
503
504
505class FakePath(pathlib.Path):
506    """Replacement for pathlib.Path. Reimplement some methods to use
507    fake filesystem. The rest of the methods work as they are, as they will
508    use the fake accessor.
509    New in pyfakefs 3.0.
510    """
511
512    # the underlying fake filesystem
513    filesystem = None
514
515    def __new__(cls, *args, **kwargs):
516        """Creates the correct subclass based on OS."""
517        if cls is FakePathlibModule.Path:
518            cls = (
519                FakePathlibModule.WindowsPath
520                if cls.filesystem.is_windows_fs
521                else FakePathlibModule.PosixPath
522            )
523        if sys.version_info < (3, 12):
524            return cls._from_parts(args)  # pytype: disable=attribute-error
525        else:
526            return object.__new__(cls)
527
528    if sys.version_info[:2] == (3, 10):
529        # Overwritten class methods to call _init to set the fake accessor,
530        # which is not done in Python 3.10, and not needed from Python 3.11 on
531        @classmethod
532        def _from_parts(cls, args):
533            self = object.__new__(cls)
534            self._init()
535            drv, root, parts = self._parse_args(args)  # pytype: disable=attribute-error
536            self._drv = drv
537            self._root = root
538            self._parts = parts
539            return self
540
541        @classmethod
542        def _from_parsed_parts(cls, drv, root, parts):
543            self = object.__new__(cls)
544            self._drv = drv
545            self._root = root
546            self._parts = parts
547            self._init()
548            return self
549
550    if sys.version_info < (3, 11):
551
552        def _init(self, template=None):
553            """Initializer called from base class."""
554            # only needed until Python 3.10
555            self._accessor = _fake_accessor
556            # only needed until Python 3.8
557            self._closed = False
558
559    def _path(self):
560        """Returns the underlying path string as used by the fake
561        filesystem.
562        """
563        return str(self)
564
565    @classmethod
566    def cwd(cls):
567        """Return a new path pointing to the current working directory
568        (as returned by os.getcwd()).
569        """
570        return cls(cls.filesystem.cwd)
571
572    if sys.version_info < (3, 12):  # in 3.12, we can use the pathlib implementation
573
574        def resolve(self, strict=None):
575            """Make the path absolute, resolving all symlinks on the way and also
576            normalizing it (for example turning slashes into backslashes
577            under Windows).
578
579            Args:
580                strict: If False (default) no exception is raised if the path
581                    does not exist.
582                    New in Python 3.6.
583
584            Raises:
585                OSError: if the path doesn't exist (strict=True or Python < 3.6)
586            """
587            if sys.version_info >= (3, 6):
588                if strict is None:
589                    strict = False
590            else:
591                if strict is not None:
592                    raise TypeError(
593                        "resolve() got an unexpected keyword argument 'strict'"
594                    )
595                strict = True
596            self._raise_on_closed()
597            path = self._flavour.resolve(
598                self, strict=strict
599            )  # pytype: disable=attribute-error
600            if path is None:
601                self.stat()
602                path = str(self.absolute())
603            path = self.filesystem.absnormpath(path)
604            return FakePath(path)
605
606    def open(self, mode="r", buffering=-1, encoding=None, errors=None, newline=None):
607        """Open the file pointed by this path and return a fake file object.
608
609        Raises:
610            OSError: if the target object is a directory, the path is invalid
611                or permission is denied.
612        """
613        self._raise_on_closed()
614        return FakeFileOpen(self.filesystem)(
615            self._path(), mode, buffering, encoding, errors, newline
616        )
617
618    def read_bytes(self):
619        """Open the fake file in bytes mode, read it, and close the file.
620
621        Raises:
622            OSError: if the target object is a directory, the path is
623                invalid or permission is denied.
624        """
625        with FakeFileOpen(self.filesystem)(
626            self._path(), mode="rb"
627        ) as f:  # pytype: disable=attribute-error
628            return f.read()
629
630    def read_text(self, encoding=None, errors=None):
631        """
632        Open the fake file in text mode, read it, and close the file.
633        """
634        with FakeFileOpen(self.filesystem)(  # pytype: disable=attribute-error
635            self._path(), mode="r", encoding=encoding, errors=errors
636        ) as f:
637            return f.read()
638
639    def write_bytes(self, data):
640        """Open the fake file in bytes mode, write to it, and close the file.
641        Args:
642            data: the bytes to be written
643        Raises:
644            OSError: if the target object is a directory, the path is
645                invalid or permission is denied.
646        """
647        # type-check for the buffer interface before truncating the file
648        view = memoryview(data)
649        with FakeFileOpen(self.filesystem)(
650            self._path(), mode="wb"
651        ) as f:  # pytype: disable=attribute-error
652            return f.write(view)
653
654    def write_text(self, data, encoding=None, errors=None, newline=None):
655        """Open the fake file in text mode, write to it, and close
656        the file.
657
658        Args:
659            data: the string to be written
660            encoding: the encoding used for the string; if not given, the
661                default locale encoding is used
662            errors: (str) Defines how encoding errors are handled.
663            newline: Controls universal newlines, passed to stream object.
664                New in Python 3.10.
665        Raises:
666            TypeError: if data is not of type 'str'.
667            OSError: if the target object is a directory, the path is
668                invalid or permission is denied.
669        """
670        if not isinstance(data, str):
671            raise TypeError("data must be str, not %s" % data.__class__.__name__)
672        if newline is not None and sys.version_info < (3, 10):
673            raise TypeError(
674                "write_text() got an unexpected " "keyword argument 'newline'"
675            )
676        with FakeFileOpen(self.filesystem)(  # pytype: disable=attribute-error
677            self._path(),
678            mode="w",
679            encoding=encoding,
680            errors=errors,
681            newline=newline,
682        ) as f:
683            return f.write(data)
684
685    @classmethod
686    def home(cls):
687        """Return a new path pointing to the user's home directory (as
688        returned by os.path.expanduser('~')).
689        """
690        home = os.path.expanduser("~")
691        if cls.filesystem.is_windows_fs != (os.name == "nt"):
692            username = os.path.split(home)[1]
693            if cls.filesystem.is_windows_fs:
694                home = os.path.join("C:", "Users", username)
695            else:
696                home = os.path.join("home", username)
697            if not cls.filesystem.exists(home):
698                cls.filesystem.create_dir(home)
699        return cls(home.replace(os.sep, cls.filesystem.path_separator))
700
701    def samefile(self, other_path):
702        """Return whether other_path is the same or not as this file
703        (as returned by os.path.samefile()).
704
705        Args:
706            other_path: A path object or string of the file object
707            to be compared with
708
709        Raises:
710            OSError: if the filesystem object doesn't exist.
711        """
712        st = self.stat()
713        try:
714            other_st = other_path.stat()
715        except AttributeError:
716            other_st = self.filesystem.stat(other_path)
717        return st.st_ino == other_st.st_ino and st.st_dev == other_st.st_dev
718
719    def expanduser(self):
720        """Return a new path with expanded ~ and ~user constructs
721        (as returned by os.path.expanduser)
722        """
723        return FakePath(
724            os.path.expanduser(self._path()).replace(
725                os.path.sep, self.filesystem.path_separator
726            )
727        )
728
729    def _raise_on_closed(self):
730        if sys.version_info < (3, 9) and self._closed:
731            self._raise_closed()
732
733    def touch(self, mode=0o666, exist_ok=True):
734        """Create a fake file for the path with the given access mode,
735        if it doesn't exist.
736
737        Args:
738            mode: the file mode for the file if it does not exist
739            exist_ok: if the file already exists and this is True, nothing
740                happens, otherwise FileExistError is raised
741
742        Raises:
743            FileExistsError: if the file exists and exits_ok is False.
744        """
745        self._raise_on_closed()
746        if self.exists():
747            if exist_ok:
748                self.filesystem.utime(self._path(), times=None)
749            else:
750                self.filesystem.raise_os_error(errno.EEXIST, self._path())
751        else:
752            fake_file = self.open("w")
753            fake_file.close()
754            self.chmod(mode)
755
756    if sys.version_info >= (3, 12):
757        """These are reimplemented for now because the original implementation
758        checks the flavour against ntpath/posixpath.
759        """
760
761        def is_absolute(self):
762            if self.filesystem.is_windows_fs:
763                return self.drive and self.root
764            return os.path.isabs(self._path())
765
766        def is_reserved(self):
767            if not self.filesystem.is_windows_fs or not self._tail:
768                return False
769            if self._tail[0].startswith("\\\\"):
770                # UNC paths are never reserved.
771                return False
772            name = self._tail[-1].partition(".")[0].partition(":")[0].rstrip(" ")
773            return name.upper() in pathlib._WIN_RESERVED_NAMES
774
775
776class FakePathlibModule:
777    """Uses FakeFilesystem to provide a fake pathlib module replacement.
778    Can be used to replace both the standard `pathlib` module and the
779    `pathlib2` package available on PyPi.
780
781    You need a fake_filesystem to use this:
782    `filesystem = fake_filesystem.FakeFilesystem()`
783    `fake_pathlib_module = fake_filesystem.FakePathlibModule(filesystem)`
784    """
785
786    def __init__(self, filesystem):
787        """
788        Initializes the module with the given filesystem.
789
790        Args:
791            filesystem: FakeFilesystem used to provide file system information
792        """
793        init_module(filesystem)
794        self._pathlib_module = pathlib
795
796    class PurePosixPath(PurePath):
797        """A subclass of PurePath, that represents non-Windows filesystem
798        paths"""
799
800        __slots__ = ()
801
802    class PureWindowsPath(PurePath):
803        """A subclass of PurePath, that represents Windows filesystem paths"""
804
805        __slots__ = ()
806
807    class WindowsPath(FakePath, PureWindowsPath):
808        """A subclass of Path and PureWindowsPath that represents
809        concrete Windows filesystem paths.
810        """
811
812        __slots__ = ()
813
814        def owner(self):
815            raise NotImplementedError("Path.owner() is unsupported on this system")
816
817        def group(self):
818            raise NotImplementedError("Path.group() is unsupported on this system")
819
820        def is_mount(self):
821            raise NotImplementedError("Path.is_mount() is unsupported on this system")
822
823    class PosixPath(FakePath, PurePosixPath):
824        """A subclass of Path and PurePosixPath that represents
825        concrete non-Windows filesystem paths.
826        """
827
828        __slots__ = ()
829
830        def owner(self):
831            """Return the username of the file owner.
832            It is assumed that `st_uid` is related to a real user,
833            otherwise `KeyError` is raised.
834            """
835            import pwd
836
837            return pwd.getpwuid(self.stat().st_uid).pw_name
838
839        def group(self):
840            """Return the group name of the file group.
841            It is assumed that `st_gid` is related to a real group,
842            otherwise `KeyError` is raised.
843            """
844            import grp
845
846            return grp.getgrgid(self.stat().st_gid).gr_name
847
848    Path = FakePath
849
850    def __getattr__(self, name):
851        """Forwards any unfaked calls to the standard pathlib module."""
852        return getattr(self._pathlib_module, name)
853
854
855class FakePathlibPathModule:
856    """Patches `pathlib.Path` by passing all calls to FakePathlibModule."""
857
858    fake_pathlib = None
859
860    def __init__(self, filesystem=None):
861        if self.fake_pathlib is None:
862            self.__class__.fake_pathlib = FakePathlibModule(filesystem)
863
864    def __call__(self, *args, **kwargs):
865        return self.fake_pathlib.Path(*args, **kwargs)
866
867    def __getattr__(self, name):
868        return getattr(self.fake_pathlib.Path, name)
869
870    @classmethod
871    def __instancecheck__(cls, instance):
872        # fake the inheritance to pass isinstance checks - see #666
873        return isinstance(instance, PurePath)
874
875
876class RealPath(pathlib.Path):
877    """Replacement for `pathlib.Path` if it shall not be faked.
878    Needed because `Path` in `pathlib` is always faked, even if `pathlib`
879    itself is not.
880    """
881
882    if sys.version_info < (3, 12):
883        _flavour = (
884            pathlib._WindowsFlavour()  # type:ignore
885            if os.name == "nt"
886            else pathlib._PosixFlavour()  # type:ignore
887        )  # type:ignore
888    else:
889        _flavour = ntpath if os.name == "nt" else posixpath
890
891    def __new__(cls, *args, **kwargs):
892        """Creates the correct subclass based on OS."""
893        if cls is RealPathlibModule.Path:
894            cls = (
895                RealPathlibModule.WindowsPath  # pytype: disable=attribute-error
896                if os.name == "nt"
897                else RealPathlibModule.PosixPath  # pytype: disable=attribute-error
898            )
899        if sys.version_info < (3, 12):
900            return cls._from_parts(args)  # pytype: disable=attribute-error
901        else:
902            return object.__new__(cls)
903
904
905if sys.version_info > (3, 10):
906
907    def with_original_os(f: Callable) -> Callable:
908        """Decorator used for real pathlib Path methods to ensure that
909        real os functions instead of faked ones are used."""
910
911        @functools.wraps(f)
912        def wrapped(*args, **kwargs):
913            with use_original_os():
914                return f(*args, **kwargs)
915
916        return wrapped
917
918    for name, fn in inspect.getmembers(RealPath, inspect.isfunction):
919        if not name.startswith("__"):
920            setattr(RealPath, name, with_original_os(fn))
921
922
923class RealPathlibPathModule:
924    """Patches `pathlib.Path` by passing all calls to RealPathlibModule."""
925
926    real_pathlib = None
927
928    @classmethod
929    def __instancecheck__(cls, instance):
930        # as we cannot derive from pathlib.Path, we fake
931        # the inheritance to pass isinstance checks - see #666
932        return isinstance(instance, PurePath)
933
934    def __init__(self):
935        if self.real_pathlib is None:
936            self.__class__.real_pathlib = RealPathlibModule()
937
938    def __call__(self, *args, **kwargs):
939        return RealPath(*args, **kwargs)
940
941    def __getattr__(self, name):
942        return getattr(self.real_pathlib.Path, name)
943
944
945class RealPathlibModule:
946    """Used to replace `pathlib` for skipped modules.
947    As the original `pathlib` is always patched to use the fake path,
948    we need to provide a version which does not do this.
949    """
950
951    def __init__(self):
952        self._pathlib_module = pathlib
953
954    class PurePosixPath(PurePath):
955        """A subclass of PurePath, that represents Posix filesystem paths"""
956
957        __slots__ = ()
958
959    class PureWindowsPath(PurePath):
960        """A subclass of PurePath, that represents Windows filesystem paths"""
961
962        __slots__ = ()
963
964    if sys.platform == "win32":
965
966        class WindowsPath(RealPath, PureWindowsPath):
967            """A subclass of Path and PureWindowsPath that represents
968            concrete Windows filesystem paths.
969            """
970
971            __slots__ = ()
972
973    else:
974
975        class PosixPath(RealPath, PurePosixPath):
976            """A subclass of Path and PurePosixPath that represents
977            concrete non-Windows filesystem paths.
978            """
979
980            __slots__ = ()
981
982    Path = RealPath
983
984    def __getattr__(self, name):
985        """Forwards any unfaked calls to the standard pathlib module."""
986        return getattr(self._pathlib_module, name)
987