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