• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2009 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""A fake filesystem implementation for unit testing.
16
17:Usage:
18
19>>> from pyfakefs import fake_filesystem, fake_os
20>>> filesystem = fake_filesystem.FakeFilesystem()
21>>> os_module = fake_os.FakeOsModule(filesystem)
22>>> pathname = '/a/new/dir/new-file'
23
24Create a new file object, creating parent directory objects as needed:
25
26>>> os_module.path.exists(pathname)
27False
28>>> new_file = filesystem.create_file(pathname)
29
30File objects can't be overwritten:
31
32>>> os_module.path.exists(pathname)
33True
34>>> try:
35...   filesystem.create_file(pathname)
36... except OSError as e:
37...   assert e.errno == errno.EEXIST, 'unexpected errno: %d' % e.errno
38...   assert e.strerror == 'File exists in the fake filesystem'
39
40Remove a file object:
41
42>>> filesystem.remove_object(pathname)
43>>> os_module.path.exists(pathname)
44False
45
46Create a new file object at the previous path:
47
48>>> beatles_file = filesystem.create_file(pathname,
49...     contents='Dear Prudence\\nWon\\'t you come out to play?\\n')
50>>> os_module.path.exists(pathname)
51True
52
53Use the FakeFileOpen class to read fake file objects:
54
55>>> file_module = fake_filesystem.FakeFileOpen(filesystem)
56>>> for line in file_module(pathname):
57...     print(line.rstrip())
58...
59Dear Prudence
60Won't you come out to play?
61
62File objects cannot be treated like directory objects:
63
64>>> try:
65...   os_module.listdir(pathname)
66... except OSError as e:
67...   assert e.errno == errno.ENOTDIR, 'unexpected errno: %d' % e.errno
68...   assert e.strerror == 'Not a directory in the fake filesystem'
69
70The FakeOsModule can list fake directory objects:
71
72>>> os_module.listdir(os_module.path.dirname(pathname))
73['new-file']
74
75The FakeOsModule also supports stat operations:
76
77>>> import stat
78>>> stat.S_ISREG(os_module.stat(pathname).st_mode)
79True
80>>> stat.S_ISDIR(os_module.stat(os_module.path.dirname(pathname)).st_mode)
81True
82"""
83
84import contextlib
85import dataclasses
86import errno
87import heapq
88import os
89import random
90import sys
91import tempfile
92from collections import namedtuple, OrderedDict
93from doctest import TestResults
94from enum import Enum
95from stat import (
96    S_IFREG,
97    S_IFDIR,
98    S_ISLNK,
99    S_IFMT,
100    S_ISDIR,
101    S_IFLNK,
102    S_ISREG,
103)
104from typing import (
105    List,
106    Callable,
107    Union,
108    Any,
109    Dict,
110    Tuple,
111    cast,
112    AnyStr,
113    overload,
114    NoReturn,
115    Optional,
116)
117
118from pyfakefs import fake_file, fake_path, fake_io, fake_os, helpers, fake_open
119from pyfakefs.fake_file import AnyFileWrapper, AnyFile
120from pyfakefs.helpers import (
121    is_int_type,
122    make_string_path,
123    to_string,
124    matching_string,
125    AnyPath,
126    AnyString,
127    WINDOWS_PROPERTIES,
128    POSIX_PROPERTIES,
129    FSType,
130)
131
132if sys.platform.startswith("linux"):
133    # on newer Linux system, the default maximum recursion depth is 40
134    # we ignore older systems here
135    _MAX_LINK_DEPTH = 40
136else:
137    # on MacOS and Windows, the maximum recursion depth is 32
138    _MAX_LINK_DEPTH = 32
139
140
141class OSType(Enum):
142    """Defines the real or simulated OS of the underlying file system."""
143
144    LINUX = "linux"
145    MACOS = "macos"
146    WINDOWS = "windows"
147
148
149# definitions for backwards compatibility
150FakeFile = fake_file.FakeFile
151FakeNullFile = fake_file.FakeNullFile
152FakeFileFromRealFile = fake_file.FakeFileFromRealFile
153FakeDirectory = fake_file.FakeDirectory
154FakeDirectoryFromRealDirectory = fake_file.FakeDirectoryFromRealDirectory
155FakeFileWrapper = fake_file.FakeFileWrapper
156StandardStreamWrapper = fake_file.StandardStreamWrapper
157FakeDirWrapper = fake_file.FakeDirWrapper
158FakePipeWrapper = fake_file.FakePipeWrapper
159
160FakePathModule = fake_path.FakePathModule
161FakeOsModule = fake_os.FakeOsModule
162FakeFileOpen = fake_open.FakeFileOpen
163FakeIoModule = fake_io.FakeIoModule
164if sys.platform != "win32":
165    FakeFcntlModule = fake_io.FakeFcntlModule
166PatchMode = fake_io.PatchMode
167
168is_root = helpers.is_root
169get_uid = helpers.get_uid
170set_uid = helpers.set_uid
171get_gid = helpers.get_gid
172set_gid = helpers.set_gid
173reset_ids = helpers.reset_ids
174
175PERM_READ = helpers.PERM_READ
176PERM_WRITE = helpers.PERM_WRITE
177PERM_EXE = helpers.PERM_EXE
178PERM_DEF = helpers.PERM_DEF
179PERM_DEF_FILE = helpers.PERM_DEF_FILE
180PERM_ALL = helpers.PERM_ALL
181
182
183class FakeFilesystem:
184    """Provides the appearance of a real directory tree for unit testing.
185
186    Attributes:
187        is_case_sensitive: `True` if a case-sensitive file system is assumed.
188        root: The root :py:class:`FakeDirectory<pyfakefs.fake_file.FakeDirectory>` entry
189            of the file system.
190        umask: The umask used for newly created files, see `os.umask`.
191        patcher: Holds the Patcher object if created from it. Allows access
192            to the patcher object if using the pytest fs fixture.
193        patch_open_code: Defines how `io.open_code` will be patched;
194            patching can be on, off, or in automatic mode.
195        shuffle_listdir_results: If `True`, `os.listdir` will not sort the
196            results to match the real file system behavior.
197    """
198
199    def __init__(
200        self,
201        path_separator: str = os.path.sep,
202        total_size: Optional[int] = None,
203        patcher: Any = None,
204        create_temp_dir: bool = False,
205    ) -> None:
206        """
207        Args:
208            path_separator:  optional substitute for os.path.sep
209            total_size: if not None, the total size in bytes of the
210                root filesystem.
211            patcher: the Patcher instance if created from the Patcher
212            create_temp_dir: If True, a temp directory is created on initialization.
213                Under Posix, if the temp directory is not `/tmp`, a link to the temp
214                path is additionally created at `/tmp`.
215
216        Example usage to use the same path separator under all systems:
217
218        >>> filesystem = FakeFilesystem(path_separator='/')
219
220        """
221        self.patcher = patcher
222        self.create_temp_dir = create_temp_dir
223
224        # is_windows_fs can be used to test the behavior of pyfakefs under
225        # Windows fs on non-Windows systems and vice verse;
226        # is it used to support drive letters, UNC paths and some other
227        # Windows-specific features
228        self._is_windows_fs = sys.platform == "win32"
229
230        # can be used to test some MacOS-specific behavior under other systems
231        self._is_macos = sys.platform == "darwin"
232
233        # is_case_sensitive can be used to test pyfakefs for case-sensitive
234        # file systems on non-case-sensitive systems and vice verse
235        self.is_case_sensitive: bool = not (self._is_windows_fs or self._is_macos)
236
237        # by default, we use the configured filesystem
238        self.fs_type = FSType.DEFAULT
239        base_properties = (
240            WINDOWS_PROPERTIES if self._is_windows_fs else POSIX_PROPERTIES
241        )
242        self.fs_properties = [
243            dataclasses.replace(base_properties),
244            POSIX_PROPERTIES,
245            WINDOWS_PROPERTIES,
246        ]
247        self.path_separator = path_separator
248
249        self.root: FakeDirectory
250        self._cwd = ""
251
252        # We can't query the current value without changing it:
253        self.umask: int = os.umask(0o22)
254        os.umask(self.umask)
255
256        # A list of open file objects. Their position in the list is their
257        # file descriptor number
258        self.open_files: List[Optional[List[AnyFileWrapper]]] = []
259        # A heap containing all free positions in self.open_files list
260        self._free_fd_heap: List[int] = []
261        # last used numbers for inodes (st_ino) and devices (st_dev)
262        self.last_ino: int = 0
263        self.last_dev: int = 0
264        self.mount_points: Dict[AnyString, Dict] = OrderedDict()
265        self.dev_null: Any = None
266        self.reset(total_size=total_size, init_pathlib=False)
267
268        # set from outside if needed
269        self.patch_open_code = PatchMode.OFF
270        self.shuffle_listdir_results = False
271
272    @property
273    def is_linux(self) -> bool:
274        """Returns `True` in a real or faked Linux file system."""
275        return not self.is_windows_fs and not self.is_macos
276
277    @property
278    def is_windows_fs(self) -> bool:
279        """Returns `True` in a real or faked Windows file system."""
280        return self.fs_type == FSType.WINDOWS or (
281            self.fs_type == FSType.DEFAULT and self._is_windows_fs
282        )
283
284    @is_windows_fs.setter
285    def is_windows_fs(self, value: bool) -> None:
286        if self._is_windows_fs != value:
287            self._is_windows_fs = value
288            if value:
289                self._is_macos = False
290            self.reset()
291            FakePathModule.reset(self)
292
293    @property
294    def is_macos(self) -> bool:
295        """Returns `True` in a real or faked macOS file system."""
296        return self._is_macos
297
298    @is_macos.setter
299    def is_macos(self, value: bool) -> None:
300        if self._is_macos != value:
301            self._is_macos = value
302            if value:
303                self._is_windows_fs = False
304            self.reset()
305            FakePathModule.reset(self)
306
307    @property
308    def path_separator(self) -> str:
309        """Returns the path separator, corresponds to `os.path.sep`."""
310        return self.fs_properties[self.fs_type.value].sep
311
312    @path_separator.setter
313    def path_separator(self, value: str) -> None:
314        self.fs_properties[0].sep = value
315        if value != os.sep:
316            self.alternative_path_separator = None
317
318    @property
319    def alternative_path_separator(self) -> Optional[str]:
320        """Returns the alternative path separator, corresponds to `os.path.altsep`."""
321        return self.fs_properties[self.fs_type.value].altsep
322
323    @alternative_path_separator.setter
324    def alternative_path_separator(self, value: Optional[str]) -> None:
325        self.fs_properties[0].altsep = value
326
327    @property
328    def devnull(self) -> str:
329        return self.fs_properties[self.fs_type.value].devnull
330
331    @property
332    def pathsep(self) -> str:
333        return self.fs_properties[self.fs_type.value].pathsep
334
335    @property
336    def line_separator(self) -> str:
337        return self.fs_properties[self.fs_type.value].linesep
338
339    @property
340    def cwd(self) -> str:
341        """Return the current working directory of the fake filesystem."""
342        return self._cwd
343
344    @cwd.setter
345    def cwd(self, value: str) -> None:
346        """Set the current working directory of the fake filesystem.
347        Make sure a new drive or share is auto-mounted under Windows.
348        """
349        _cwd = make_string_path(value)
350        self._cwd = _cwd.replace(
351            matching_string(_cwd, os.sep), matching_string(_cwd, self.path_separator)
352        )
353        self._auto_mount_drive_if_needed(value)
354
355    @property
356    def root_dir(self) -> FakeDirectory:
357        """Return the root directory, which represents "/" under POSIX,
358        and the current drive under Windows."""
359        if self.is_windows_fs:
360            return self._mount_point_dir_for_cwd()
361        return self.root
362
363    @property
364    def root_dir_name(self) -> str:
365        """Return the root directory name, which is "/" under POSIX,
366        and the root path of the current drive under Windows."""
367        root_dir = to_string(self.root_dir.name)
368        if not root_dir.endswith(self.path_separator):
369            return root_dir + self.path_separator
370        return root_dir
371
372    @property
373    def os(self) -> OSType:
374        """Return the real or simulated type of operating system."""
375        return (
376            OSType.WINDOWS
377            if self.is_windows_fs
378            else OSType.MACOS
379            if self.is_macos
380            else OSType.LINUX
381        )
382
383    @os.setter
384    def os(self, value: OSType) -> None:
385        """Set the simulated type of operating system underlying the fake
386        file system."""
387        self._is_windows_fs = value == OSType.WINDOWS
388        self._is_macos = value == OSType.MACOS
389        self.is_case_sensitive = value == OSType.LINUX
390        self.fs_type = FSType.DEFAULT
391        base_properties = (
392            WINDOWS_PROPERTIES if self._is_windows_fs else POSIX_PROPERTIES
393        )
394        self.fs_properties[0] = base_properties
395        self.reset()
396        FakePathModule.reset(self)
397
398    def reset(self, total_size: Optional[int] = None, init_pathlib: bool = True):
399        """Remove all file system contents and reset the root."""
400        self.root = FakeDirectory(self.path_separator, filesystem=self)
401
402        self.dev_null = FakeNullFile(self)
403        self.open_files.clear()
404        self._free_fd_heap.clear()
405        self.last_ino = 0
406        self.last_dev = 0
407        self.mount_points.clear()
408        self._add_root_mount_point(total_size)
409        self._add_standard_streams()
410        if self.create_temp_dir:
411            self._create_temp_dir()
412        if init_pathlib:
413            from pyfakefs import fake_pathlib
414
415            fake_pathlib.init_module(self)
416
417    @contextlib.contextmanager
418    def use_fs_type(self, fs_type: FSType):
419        old_fs_type = self.fs_type
420        try:
421            self.fs_type = fs_type
422            yield
423        finally:
424            self.fs_type = old_fs_type
425
426    def _add_root_mount_point(self, total_size):
427        mount_point = "C:" if self.is_windows_fs else self.path_separator
428        self._cwd = mount_point
429        if not self.cwd.endswith(self.path_separator):
430            self._cwd += self.path_separator
431        self.add_mount_point(mount_point, total_size)
432
433    def pause(self) -> None:
434        """Pause the patching of the file system modules until :py:meth:`resume` is
435        called. After that call, all file system calls are executed in the
436        real file system.
437        Calling `pause()` twice is silently ignored.
438        Only allowed if the file system object was created by a
439        `Patcher` object. This is also the case for the pytest `fs` fixture.
440
441        Raises:
442            RuntimeError: if the file system was not created by a `Patcher`.
443        """
444        if self.patcher is None:
445            raise RuntimeError(
446                "pause() can only be called from a fake file "
447                "system object created by a Patcher object"
448            )
449        self.patcher.pause()
450
451    def resume(self) -> None:
452        """Resume the patching of the file system modules if :py:meth:`pause` has
453        been called before. After that call, all file system calls are
454        executed in the fake file system.
455        Does nothing if patching is not paused.
456        Raises:
457            RuntimeError: if the file system has not been created by `Patcher`.
458        """
459        if self.patcher is None:
460            raise RuntimeError(
461                "resume() can only be called from a fake file "
462                "system object created by a Patcher object"
463            )
464        self.patcher.resume()
465
466    def clear_cache(self) -> None:
467        """Clear the cache of non-patched modules."""
468        if self.patcher:
469            self.patcher.clear_cache()
470
471    def raise_os_error(
472        self,
473        err_no: int,
474        filename: Optional[AnyString] = None,
475        winerror: Optional[int] = None,
476    ) -> NoReturn:
477        """Raises OSError.
478        The error message is constructed from the given error code and shall
479        start with the error string issued in the real system.
480        Note: this is not true under Windows if winerror is given - in this
481        case a localized message specific to winerror will be shown in the
482        real file system.
483
484        Args:
485            err_no: A numeric error code from the C variable errno.
486            filename: The name of the affected file, if any.
487            winerror: Windows only - the specific Windows error code.
488        """
489        message = os.strerror(err_no) + " in the fake filesystem"
490        if winerror is not None and sys.platform == "win32" and self.is_windows_fs:
491            raise OSError(err_no, message, filename, winerror)
492        raise OSError(err_no, message, filename)
493
494    def get_path_separator(self, path: AnyStr) -> AnyStr:
495        """Return the path separator as the same type as path"""
496        return matching_string(path, self.path_separator)
497
498    def _alternative_path_separator(self, path: AnyStr) -> Optional[AnyStr]:
499        """Return the alternative path separator as the same type as path"""
500        return matching_string(path, self.alternative_path_separator)
501
502    def starts_with_sep(self, path: AnyStr) -> bool:
503        """Return True if path starts with a path separator."""
504        sep = self.get_path_separator(path)
505        altsep = self._alternative_path_separator(path)
506        return path.startswith(sep) or altsep is not None and path.startswith(altsep)
507
508    def add_mount_point(
509        self,
510        path: AnyStr,
511        total_size: Optional[int] = None,
512        can_exist: bool = False,
513    ) -> Dict:
514        """Add a new mount point for a filesystem device.
515        The mount point gets a new unique device number.
516
517        Args:
518            path: The root path for the new mount path.
519
520            total_size: The new total size of the added filesystem device
521                in bytes. Defaults to infinite size.
522
523            can_exist: If `True`, no error is raised if the mount point
524                already exists.
525
526        Returns:
527            The newly created mount point dict.
528
529        Raises:
530            OSError: if trying to mount an existing mount point again,
531                and `can_exist` is False.
532        """
533        path = self.normpath(self.normcase(path))
534        for mount_point in self.mount_points:
535            if (
536                self.is_case_sensitive
537                and path == matching_string(path, mount_point)
538                or not self.is_case_sensitive
539                and path.lower() == matching_string(path, mount_point.lower())
540            ):
541                if can_exist:
542                    return self.mount_points[mount_point]
543                self.raise_os_error(errno.EEXIST, path)
544
545        self.last_dev += 1
546        self.mount_points[path] = {
547            "idev": self.last_dev,
548            "total_size": total_size,
549            "used_size": 0,
550        }
551        if path == matching_string(path, self.root.name):
552            # special handling for root path: has been created before
553            root_dir = self.root
554            self.last_ino += 1
555            root_dir.st_ino = self.last_ino
556        else:
557            root_dir = self._create_mount_point_dir(path)
558        root_dir.st_dev = self.last_dev
559        return self.mount_points[path]
560
561    def _create_mount_point_dir(self, directory_path: AnyPath) -> FakeDirectory:
562        """A version of `create_dir` for the mount point directory creation,
563        which avoids circular calls and unneeded checks.
564        """
565        dir_path = self.make_string_path(directory_path)
566        path_components = self._path_components(dir_path)
567        current_dir = self.root
568
569        new_dirs = []
570        for component in [to_string(p) for p in path_components]:
571            directory = self._directory_content(current_dir, to_string(component))[1]
572            if not directory:
573                new_dir = FakeDirectory(component, filesystem=self)
574                new_dirs.append(new_dir)
575                current_dir.add_entry(new_dir)
576                current_dir = new_dir
577            else:
578                current_dir = cast(FakeDirectory, directory)
579
580        for new_dir in new_dirs:
581            new_dir.st_mode = S_IFDIR | helpers.PERM_DEF
582
583        return current_dir
584
585    def _auto_mount_drive_if_needed(self, path: AnyStr) -> Optional[Dict]:
586        """Windows only: if `path` is located on an unmounted drive or UNC
587        mount point, the drive/mount point is added to the mount points."""
588        if self.is_windows_fs:
589            drive = self.splitdrive(path)[0]
590            if drive:
591                return self.add_mount_point(path=drive, can_exist=True)
592        return None
593
594    def _mount_point_for_path(self, path: AnyStr) -> Dict:
595        path = self.absnormpath(self._original_path(path))
596        for mount_path in self.mount_points:
597            if path == matching_string(path, mount_path):
598                return self.mount_points[mount_path]
599        mount_path = matching_string(path, "")
600        drive = self.splitdrive(path)[0]
601        for root_path in self.mount_points:
602            root_path = matching_string(path, root_path)
603            if drive and not root_path.startswith(drive):
604                continue
605            if path.startswith(root_path) and len(root_path) > len(mount_path):
606                mount_path = root_path
607        if mount_path:
608            return self.mount_points[to_string(mount_path)]
609        mount_point = self._auto_mount_drive_if_needed(path)
610        assert mount_point
611        return mount_point
612
613    def _mount_point_dir_for_cwd(self) -> FakeDirectory:
614        """Return the fake directory object of the mount point where the
615        current working directory points to."""
616
617        def object_from_path(file_path) -> FakeDirectory:
618            path_components = self._path_components(file_path)
619            target = self.root
620            for component in path_components:
621                target = cast(FakeDirectory, target.get_entry(component))
622            return target
623
624        path = to_string(self.cwd)
625        for mount_path in self.mount_points:
626            if path == to_string(mount_path):
627                return object_from_path(mount_path)
628        mount_path = ""
629        drive = to_string(self.splitdrive(path)[0])
630        for root_path in self.mount_points:
631            str_root_path = to_string(root_path)
632            if drive and not str_root_path.startswith(drive):
633                continue
634            if path.startswith(str_root_path) and len(str_root_path) > len(mount_path):
635                mount_path = root_path
636        return object_from_path(mount_path)
637
638    def _mount_point_for_device(self, idev: int) -> Optional[Dict]:
639        for mount_point in self.mount_points.values():
640            if mount_point["idev"] == idev:
641                return mount_point
642        return None
643
644    def get_disk_usage(self, path: Optional[AnyStr] = None) -> Tuple[int, int, int]:
645        """Return the total, used and free disk space in bytes as named tuple,
646        or placeholder values simulating unlimited space if not set.
647
648        .. note:: This matches the return value of ``shutil.disk_usage()``.
649
650        Args:
651            path: The disk space is returned for the file system device where
652                `path` resides.
653                Defaults to the root path (e.g. '/' on Unix systems).
654        """
655        DiskUsage = namedtuple("DiskUsage", "total, used, free")
656        if path is None:
657            mount_point = next(iter(self.mount_points.values()))
658        else:
659            file_path = make_string_path(path)
660            mount_point = self._mount_point_for_path(file_path)
661        if mount_point and mount_point["total_size"] is not None:
662            return DiskUsage(
663                mount_point["total_size"],
664                mount_point["used_size"],
665                mount_point["total_size"] - mount_point["used_size"],
666            )
667        return DiskUsage(1024 * 1024 * 1024 * 1024, 0, 1024 * 1024 * 1024 * 1024)
668
669    def set_disk_usage(self, total_size: int, path: Optional[AnyStr] = None) -> None:
670        """Changes the total size of the file system, preserving the
671        used space.
672        Example usage: set the size of an auto-mounted Windows drive.
673
674        Args:
675            total_size: The new total size of the filesystem in bytes.
676
677            path: The disk space is changed for the file system device where
678                `path` resides.
679                Defaults to the root path (e.g. '/' on Unix systems).
680
681        Raises:
682            OSError: if the new space is smaller than the used size.
683        """
684        file_path: AnyStr = (
685            path if path is not None else self.root_dir_name  # type: ignore
686        )
687        mount_point = self._mount_point_for_path(file_path)
688        if (
689            mount_point["total_size"] is not None
690            and mount_point["used_size"] > total_size
691        ):
692            self.raise_os_error(errno.ENOSPC, path)
693        mount_point["total_size"] = total_size
694
695    def change_disk_usage(
696        self, usage_change: int, file_path: AnyStr, st_dev: int
697    ) -> None:
698        """Change the used disk space by the given amount.
699
700        Args:
701            usage_change: Number of bytes added to the used space.
702                If negative, the used space will be decreased.
703
704            file_path: The path of the object needing the disk space.
705
706            st_dev: The device ID for the respective file system.
707
708        Raises:
709            OSError: if `usage_change` exceeds the free file system space
710        """
711        mount_point = self._mount_point_for_device(st_dev)
712        if mount_point:
713            total_size = mount_point["total_size"]
714            if total_size is not None:
715                if total_size - mount_point["used_size"] < usage_change:
716                    self.raise_os_error(errno.ENOSPC, file_path)
717            mount_point["used_size"] += usage_change
718
719    def stat(self, entry_path: AnyStr, follow_symlinks: bool = True):
720        """Return the os.stat-like tuple for the FakeFile object of entry_path.
721
722        Args:
723            entry_path:  Path to filesystem object to retrieve.
724            follow_symlinks: If False and entry_path points to a symlink,
725                the link itself is inspected instead of the linked object.
726
727        Returns:
728            The FakeStatResult object corresponding to entry_path.
729
730        Raises:
731            OSError: if the filesystem object doesn't exist.
732        """
733        # stat should return the tuple representing return value of os.stat
734        try:
735            file_object = self.resolve(
736                entry_path,
737                follow_symlinks,
738                allow_fd=True,
739                check_read_perm=False,
740                check_exe_perm=False,
741            )
742        except TypeError:
743            file_object = self.resolve(entry_path)
744        if not is_root():
745            # make sure stat raises if a parent dir is not readable
746            parent_dir = file_object.parent_dir
747            if parent_dir:
748                self.get_object(parent_dir.path, check_read_perm=False)  # type: ignore[arg-type]
749
750        self.raise_for_filepath_ending_with_separator(
751            entry_path, file_object, follow_symlinks
752        )
753
754        return file_object.stat_result.copy()
755
756    def raise_for_filepath_ending_with_separator(
757        self,
758        entry_path: AnyStr,
759        file_object: FakeFile,
760        follow_symlinks: bool = True,
761        macos_handling: bool = False,
762    ) -> None:
763        if self.ends_with_path_separator(entry_path):
764            if S_ISLNK(file_object.st_mode):
765                try:
766                    link_object = self.resolve(entry_path)
767                except OSError as exc:
768                    if self.is_macos and exc.errno != errno.ENOENT:
769                        return
770                    if self.is_windows_fs:
771                        self.raise_os_error(errno.EINVAL, entry_path)
772                    raise
773                if not follow_symlinks or self.is_windows_fs or self.is_macos:
774                    file_object = link_object
775            if self.is_windows_fs:
776                is_error = S_ISREG(file_object.st_mode)
777            elif self.is_macos and macos_handling:
778                is_error = not S_ISLNK(file_object.st_mode)
779            else:
780                is_error = not S_ISDIR(file_object.st_mode)
781            if is_error:
782                error_nr = errno.EINVAL if self.is_windows_fs else errno.ENOTDIR
783                self.raise_os_error(error_nr, entry_path)
784
785    def chmod(
786        self,
787        path: Union[AnyStr, int],
788        mode: int,
789        follow_symlinks: bool = True,
790        force_unix_mode: bool = False,
791    ) -> None:
792        """Change the permissions of a file as encoded in integer mode.
793
794        Args:
795            path: (str | int) Path to the file or file descriptor.
796            mode: (int) Permissions.
797            follow_symlinks: If `False` and `path` points to a symlink,
798                the link itself is affected instead of the linked object.
799            force_unix_mode: if True and run under Windows, the mode is not
800                adapted for Windows to allow making dirs unreadable
801        """
802        allow_fd = not self.is_windows_fs or sys.version_info >= (3, 13)
803        file_object = self.resolve(
804            path, follow_symlinks, allow_fd=allow_fd, check_owner=True
805        )
806        if self.is_windows_fs and not force_unix_mode:
807            if mode & helpers.PERM_WRITE:
808                file_object.st_mode = file_object.st_mode | 0o222
809            else:
810                file_object.st_mode = file_object.st_mode & 0o777555
811        else:
812            file_object.st_mode = (file_object.st_mode & ~helpers.PERM_ALL) | (
813                mode & helpers.PERM_ALL
814            )
815        file_object.st_ctime = helpers.now()
816
817    def utime(
818        self,
819        path: AnyStr,
820        times: Optional[Tuple[Union[int, float], Union[int, float]]] = None,
821        *,
822        ns: Optional[Tuple[int, int]] = None,
823        follow_symlinks: bool = True,
824    ) -> None:
825        """Change the access and modified times of a file.
826
827        Args:
828            path: (str) Path to the file.
829            times: 2-tuple of int or float numbers, of the form (atime, mtime)
830                which is used to set the access and modified times in seconds.
831                If None, both times are set to the current time.
832            ns: 2-tuple of int numbers, of the form (atime, mtime)  which is
833                used to set the access and modified times in nanoseconds.
834                If `None`, both times are set to the current time.
835            follow_symlinks: If `False` and entry_path points to a symlink,
836                the link itself is queried instead of the linked object.
837
838            Raises:
839                TypeError: If anything other than the expected types is
840                    specified in the passed `times` or `ns` tuple,
841                    or if the tuple length is not equal to 2.
842                ValueError: If both times and ns are specified.
843        """
844        self._handle_utime_arg_errors(ns, times)
845
846        file_object = self.resolve(path, follow_symlinks, allow_fd=True)
847        if times is not None:
848            for file_time in times:
849                if not isinstance(file_time, (int, float)):
850                    raise TypeError("atime and mtime must be numbers")
851
852            file_object.st_atime = times[0]
853            file_object.st_mtime = times[1]
854        elif ns is not None:
855            for file_time in ns:
856                if not isinstance(file_time, int):
857                    raise TypeError("atime and mtime must be ints")
858
859            file_object.st_atime_ns = ns[0]
860            file_object.st_mtime_ns = ns[1]
861        else:
862            current_time = helpers.now()
863            file_object.st_atime = current_time
864            file_object.st_mtime = current_time
865
866    @staticmethod
867    def _handle_utime_arg_errors(
868        ns: Optional[Tuple[int, int]],
869        times: Optional[Tuple[Union[int, float], Union[int, float]]],
870    ):
871        if times is not None and ns is not None:
872            raise ValueError(
873                "utime: you may specify either 'times' or 'ns' but not both"
874            )
875        if times is not None and len(times) != 2:
876            raise TypeError("utime: 'times' must be either a tuple of two ints or None")
877        if ns is not None and len(ns) != 2:
878            raise TypeError("utime: 'ns' must be a tuple of two ints")
879
880    def add_open_file(self, file_obj: AnyFileWrapper, new_fd: int = -1) -> int:
881        """Add file_obj to the list of open files on the filesystem.
882        Used internally to manage open files.
883
884        The position in the open_files array is the file descriptor number.
885
886        Args:
887            file_obj: File object to be added to open files list.
888            new_fd: The optional new file descriptor.
889
890        Returns:
891            File descriptor number for the file object.
892        """
893        if new_fd >= 0:
894            size = len(self.open_files)
895            if new_fd < size:
896                open_files = self.open_files[new_fd]
897                if open_files:
898                    for f in open_files:
899                        try:
900                            f.close()
901                        except OSError:
902                            pass
903                if new_fd in self._free_fd_heap:
904                    self._free_fd_heap.remove(new_fd)
905                self.open_files[new_fd] = [file_obj]
906            else:
907                for fd in range(size, new_fd):
908                    self.open_files.append([])
909                    heapq.heappush(self._free_fd_heap, fd)
910                self.open_files.append([file_obj])
911            return new_fd
912
913        if self._free_fd_heap:
914            open_fd = heapq.heappop(self._free_fd_heap)
915            self.open_files[open_fd] = [file_obj]
916            return open_fd
917
918        self.open_files.append([file_obj])
919        return len(self.open_files) - 1
920
921    def close_open_file(self, file_des: int) -> None:
922        """Remove file object with given descriptor from the list
923        of open files.
924
925        Sets the entry in open_files to None.
926
927        Args:
928            file_des: Descriptor of file object to be removed from
929            open files list.
930        """
931        self.open_files[file_des] = None
932        heapq.heappush(self._free_fd_heap, file_des)
933
934    def get_open_file(self, file_des: int) -> AnyFileWrapper:
935        """Return an open file.
936
937        Args:
938            file_des: File descriptor of the open file.
939
940        Raises:
941            OSError: an invalid file descriptor.
942            TypeError: filedes is not an integer.
943
944        Returns:
945            Open file object.
946        """
947        try:
948            return self.get_open_files(file_des)[0]
949        except IndexError:
950            self.raise_os_error(errno.EBADF, str(file_des))
951
952    def get_open_files(self, file_des: int) -> List[AnyFileWrapper]:
953        """Return the list of open files for a file descriptor.
954
955        Args:
956            file_des: File descriptor of the open files.
957
958        Raises:
959            OSError: an invalid file descriptor.
960            TypeError: filedes is not an integer.
961
962        Returns:
963            List of open file objects.
964        """
965        if not is_int_type(file_des):
966            raise TypeError("an integer is required")
967        valid = file_des < len(self.open_files)
968        if valid:
969            return self.open_files[file_des] or []
970        self.raise_os_error(errno.EBADF, str(file_des))
971
972    def has_open_file(self, file_object: FakeFile) -> bool:
973        """Return True if the given file object is in the list of open files.
974
975        Args:
976            file_object: The FakeFile object to be checked.
977
978        Returns:
979            `True` if the file is open.
980        """
981        return file_object in [
982            wrappers[0].get_object() for wrappers in self.open_files if wrappers
983        ]
984
985    def _normalize_path_sep(self, path: AnyStr) -> AnyStr:
986        alt_sep = self._alternative_path_separator(path)
987        if alt_sep is not None:
988            return path.replace(alt_sep, self.get_path_separator(path))
989        return path
990
991    def normcase(self, path: AnyStr) -> AnyStr:
992        """Replace all appearances of alternative path separator
993        with path separator.
994
995        Do nothing if no alternative separator is set.
996
997        Args:
998            path: The path to be normalized.
999
1000        Returns:
1001            The normalized path that will be used internally.
1002        """
1003        file_path = make_string_path(path)
1004        return self._normalize_path_sep(file_path)
1005
1006    def normpath(self, path: AnyStr) -> AnyStr:
1007        """Mimic os.path.normpath using the specified path_separator.
1008
1009        Mimics os.path.normpath using the path_separator that was specified
1010        for this FakeFilesystem. Normalizes the path, but unlike the method
1011        absnormpath, does not make it absolute.  Eliminates dot components
1012        (. and ..) and combines repeated path separators (//).  Initial ..
1013        components are left in place for relative paths.
1014        If the result is an empty path, '.' is returned instead.
1015
1016        This also replaces alternative path separator with path separator.
1017        That is, it behaves like the real os.path.normpath on Windows if
1018        initialized with '\\' as path separator and  '/' as alternative
1019        separator.
1020
1021        Args:
1022            path:  (str) The path to normalize.
1023
1024        Returns:
1025            (str) A copy of path with empty components and dot components
1026            removed.
1027        """
1028        path_str = self.normcase(path)
1029        drive, path_str = self.splitdrive(path_str)
1030        sep = self.get_path_separator(path_str)
1031        is_absolute_path = path_str.startswith(sep)
1032        path_components: List[AnyStr] = path_str.split(
1033            sep
1034        )  # pytype: disable=invalid-annotation
1035        collapsed_path_components: List[
1036            AnyStr
1037        ] = []  # pytype: disable=invalid-annotation
1038        dot = matching_string(path_str, ".")
1039        dotdot = matching_string(path_str, "..")
1040        for component in path_components:
1041            if (not component) or (component == dot):
1042                continue
1043            if component == dotdot:
1044                if collapsed_path_components and (
1045                    collapsed_path_components[-1] != dotdot
1046                ):
1047                    # Remove an up-reference: directory/..
1048                    collapsed_path_components.pop()
1049                    continue
1050                elif is_absolute_path:
1051                    # Ignore leading .. components if starting from the
1052                    # root directory.
1053                    continue
1054            collapsed_path_components.append(component)
1055        collapsed_path = sep.join(collapsed_path_components)
1056        if is_absolute_path:
1057            collapsed_path = sep + collapsed_path
1058        return drive + collapsed_path or dot
1059
1060    def _original_path(self, path: AnyStr) -> AnyStr:
1061        """Return a normalized case version of the given path for
1062        case-insensitive file systems. For case-sensitive file systems,
1063        return path unchanged.
1064
1065        Args:
1066            path: the file path to be transformed
1067
1068        Returns:
1069            A version of path matching the case of existing path elements.
1070        """
1071
1072        def components_to_path():
1073            if len(path_components) > len(normalized_components):
1074                normalized_components.extend(
1075                    to_string(p) for p in path_components[len(normalized_components) :]
1076                )
1077            sep = self.path_separator
1078            normalized_path = sep.join(normalized_components)
1079            if self.starts_with_sep(path) and not self.starts_with_sep(normalized_path):
1080                normalized_path = sep + normalized_path
1081            if len(normalized_path) == 2 and self.starts_with_drive_letter(
1082                normalized_path
1083            ):
1084                normalized_path += sep
1085            return normalized_path
1086
1087        if self.is_case_sensitive or not path:
1088            return path
1089        path = self.replace_windows_root(path)
1090        path_components = self._path_components(path)
1091        normalized_components = []
1092        current_dir = self.root
1093        for component in path_components:
1094            if not isinstance(current_dir, FakeDirectory):
1095                return components_to_path()
1096            dir_name, directory = self._directory_content(
1097                current_dir, to_string(component)
1098            )
1099            if directory is None or (
1100                isinstance(directory, FakeDirectory)
1101                and directory._byte_contents is None
1102                and directory.st_size == 0
1103            ):
1104                return components_to_path()
1105            current_dir = cast(FakeDirectory, directory)
1106            normalized_components.append(dir_name)
1107        return components_to_path()
1108
1109    def absnormpath(self, path: AnyStr) -> AnyStr:
1110        """Absolutize and minimalize the given path.
1111
1112        Forces all relative paths to be absolute, and normalizes the path to
1113        eliminate dot and empty components.
1114
1115        Args:
1116            path:  Path to normalize.
1117
1118        Returns:
1119            The normalized path relative to the current working directory,
1120            or the root directory if path is empty.
1121        """
1122        path = self.normcase(path)
1123        cwd = matching_string(path, self.cwd)
1124        if not path:
1125            path = self.get_path_separator(path)
1126        if path == matching_string(path, "."):
1127            path = cwd
1128        elif not self._starts_with_root_path(path):
1129            # Prefix relative paths with cwd, if cwd is not root.
1130            root_name = matching_string(path, self.root.name)
1131            empty = matching_string(path, "")
1132            path = self.get_path_separator(path).join(
1133                (cwd != root_name and cwd or empty, path)
1134            )
1135        else:
1136            path = self.replace_windows_root(path)
1137        return self.normpath(path)
1138
1139    def splitpath(self, path: AnyStr) -> Tuple[AnyStr, AnyStr]:
1140        """Mimic os.path.split using the specified path_separator.
1141
1142        Mimics os.path.split using the path_separator that was specified
1143        for this FakeFilesystem.
1144
1145        Args:
1146            path:  (str) The path to split.
1147
1148        Returns:
1149            (str) A duple (pathname, basename) for which pathname does not
1150            end with a slash, and basename does not contain a slash.
1151        """
1152        path = make_string_path(path)
1153        sep = self.get_path_separator(path)
1154        alt_sep = self._alternative_path_separator(path)
1155        seps = sep if alt_sep is None else sep + alt_sep
1156        drive, path = self.splitdrive(path)
1157        i = len(path)
1158        while i and path[i - 1] not in seps:
1159            i -= 1
1160        head, tail = path[:i], path[i:]  # now tail has no slashes
1161        # remove trailing slashes from head, unless it's all slashes
1162        head = head.rstrip(seps) or head
1163        return drive + head, tail
1164
1165    def splitdrive(self, path: AnyStr) -> Tuple[AnyStr, AnyStr]:
1166        """Splits the path into the drive part and the rest of the path.
1167
1168        Taken from Windows specific implementation in Python 3.5
1169        and slightly adapted.
1170
1171        Args:
1172            path: the full path to be splitpath.
1173
1174        Returns:
1175            A tuple of the drive part and the rest of the path, or of
1176            an empty string and the full path if drive letters are
1177            not supported or no drive is present.
1178        """
1179        path_str = make_string_path(path)
1180        if self.is_windows_fs:
1181            if len(path_str) >= 2:
1182                norm_str = self.normcase(path_str)
1183                sep = self.get_path_separator(path_str)
1184                # UNC path_str handling
1185                if (norm_str[0:2] == sep * 2) and (norm_str[2:3] != sep):
1186                    # UNC path_str handling - splits off the mount point
1187                    # instead of the drive
1188                    sep_index = norm_str.find(sep, 2)
1189                    if sep_index == -1:
1190                        return path_str[:0], path_str
1191                    sep_index2 = norm_str.find(sep, sep_index + 1)
1192                    if sep_index2 == sep_index + 1:
1193                        return path_str[:0], path_str
1194                    if sep_index2 == -1:
1195                        sep_index2 = len(path_str)
1196                    return path_str[:sep_index2], path_str[sep_index2:]
1197                if path_str[1:2] == matching_string(path_str, ":"):
1198                    return path_str[:2], path_str[2:]
1199        return path_str[:0], path_str
1200
1201    def splitroot(self, path: AnyStr):
1202        """Split a pathname into drive, root and tail.
1203        Implementation taken from ntpath and posixpath.
1204        """
1205        p = os.fspath(path)
1206        if isinstance(p, bytes):
1207            sep = self.path_separator.encode()
1208            altsep = None
1209            alternative_path_separator = self.alternative_path_separator
1210            if alternative_path_separator is not None:
1211                altsep = alternative_path_separator.encode()
1212            colon = b":"
1213            unc_prefix = b"\\\\?\\UNC\\"
1214            empty = b""
1215        else:
1216            sep = self.path_separator
1217            altsep = self.alternative_path_separator
1218            colon = ":"
1219            unc_prefix = "\\\\?\\UNC\\"
1220            empty = ""
1221        if self.is_windows_fs:
1222            normp = p.replace(altsep, sep) if altsep else p
1223            if normp[:1] == sep:
1224                if normp[1:2] == sep:
1225                    # UNC drives, e.g. \\server\share or \\?\UNC\server\share
1226                    # Device drives, e.g. \\.\device or \\?\device
1227                    start = 8 if normp[:8].upper() == unc_prefix else 2
1228                    index = normp.find(sep, start)
1229                    if index == -1:
1230                        return p, empty, empty
1231                    index2 = normp.find(sep, index + 1)
1232                    if index2 == -1:
1233                        return p, empty, empty
1234                    return p[:index2], p[index2 : index2 + 1], p[index2 + 1 :]
1235                else:
1236                    # Relative path with root, e.g. \Windows
1237                    return empty, p[:1], p[1:]
1238            elif normp[1:2] == colon:
1239                if normp[2:3] == sep:
1240                    # Absolute drive-letter path, e.g. X:\Windows
1241                    return p[:2], p[2:3], p[3:]
1242                else:
1243                    # Relative path with drive, e.g. X:Windows
1244                    return p[:2], empty, p[2:]
1245            else:
1246                # Relative path, e.g. Windows
1247                return empty, empty, p
1248        else:
1249            if p[:1] != sep:
1250                # Relative path, e.g.: 'foo'
1251                return empty, empty, p
1252            elif p[1:2] != sep or p[2:3] == sep:
1253                # Absolute path, e.g.: '/foo', '///foo', '////foo', etc.
1254                return empty, sep, p[1:]
1255            else:
1256                return empty, p[:2], p[2:]
1257
1258    def _join_paths_with_drive_support(self, *all_paths: AnyStr) -> AnyStr:
1259        """Taken from Python 3.5 os.path.join() code in ntpath.py
1260        and slightly adapted"""
1261        base_path = all_paths[0]
1262        paths_to_add = all_paths[1:]
1263        sep = self.get_path_separator(base_path)
1264        seps = [sep, self._alternative_path_separator(base_path)]
1265        result_drive, result_path = self.splitdrive(base_path)
1266        for path in paths_to_add:
1267            drive_part, path_part = self.splitdrive(path)
1268            if path_part and path_part[:1] in seps:
1269                # Second path is absolute
1270                if drive_part or not result_drive:
1271                    result_drive = drive_part
1272                result_path = path_part
1273                continue
1274            elif drive_part and drive_part != result_drive:
1275                if self.is_case_sensitive or drive_part.lower() != result_drive.lower():
1276                    # Different drives => ignore the first path entirely
1277                    result_drive = drive_part
1278                    result_path = path_part
1279                    continue
1280                # Same drive in different case
1281                result_drive = drive_part
1282            # Second path is relative to the first
1283            if result_path and result_path[-1:] not in seps:
1284                result_path = result_path + sep
1285            result_path = result_path + path_part
1286        # add separator between UNC and non-absolute path
1287        colon = matching_string(base_path, ":")
1288        if (
1289            result_path
1290            and result_path[:1] not in seps
1291            and result_drive
1292            and result_drive[-1:] != colon
1293        ):
1294            return result_drive + sep + result_path
1295        return result_drive + result_path
1296
1297    def joinpaths(self, *paths: AnyStr) -> AnyStr:
1298        """Mimic os.path.join using the specified path_separator.
1299
1300        Args:
1301            *paths:  (str) Zero or more paths to join.
1302
1303        Returns:
1304            (str) The paths joined by the path separator, starting with
1305            the last absolute path in paths.
1306        """
1307        file_paths = [os.fspath(path) for path in paths]
1308        if len(file_paths) == 1:
1309            return paths[0]
1310        if self.is_windows_fs:
1311            return self._join_paths_with_drive_support(*file_paths)
1312        path = file_paths[0]
1313        sep = self.get_path_separator(file_paths[0])
1314        for path_segment in file_paths[1:]:
1315            if path_segment.startswith(sep) or not path:
1316                # An absolute path
1317                path = path_segment
1318            elif path.endswith(sep):
1319                path += path_segment
1320            else:
1321                path += sep + path_segment
1322        return path
1323
1324    @overload
1325    def _path_components(self, path: str) -> List[str]: ...
1326
1327    @overload
1328    def _path_components(self, path: bytes) -> List[bytes]: ...
1329
1330    def _path_components(self, path: AnyStr) -> List[AnyStr]:
1331        """Breaks the path into a list of component names.
1332
1333        Does not include the root directory as a component, as all paths
1334        are considered relative to the root directory for the FakeFilesystem.
1335        Callers should basically follow this pattern:
1336
1337        .. code:: python
1338
1339            file_path = self.absnormpath(file_path)
1340            path_components = self._path_components(file_path)
1341            current_dir = self.root
1342            for component in path_components:
1343                if component not in current_dir.entries:
1344                    raise OSError
1345                _do_stuff_with_component(current_dir, component)
1346                current_dir = current_dir.get_entry(component)
1347
1348        Args:
1349            path:  Path to tokenize.
1350
1351        Returns:
1352            The list of names split from path.
1353        """
1354        if not path or path == self.get_path_separator(path):
1355            return []
1356        drive, path = self.splitdrive(path)
1357        sep = self.get_path_separator(path)
1358        # handle special case of Windows emulated under POSIX
1359        if self.is_windows_fs and sys.platform != "win32":
1360            path = path.replace(matching_string(sep, "\\"), sep)
1361        path_components = path.split(sep)
1362        assert drive or path_components
1363        if not path_components[0]:
1364            if len(path_components) > 1 and not path_components[1]:
1365                path_components = []
1366            else:
1367                # This is an absolute path.
1368                path_components = path_components[1:]
1369        if drive:
1370            path_components.insert(0, drive)
1371        return path_components
1372
1373    def starts_with_drive_letter(self, file_path: AnyStr) -> bool:
1374        """Return True if file_path starts with a drive letter.
1375
1376        Args:
1377            file_path: the full path to be examined.
1378
1379        Returns:
1380            `True` if drive letter support is enabled in the filesystem and
1381            the path starts with a drive letter.
1382        """
1383        colon = matching_string(file_path, ":")
1384        if len(file_path) >= 2 and file_path[0:1].isalpha() and file_path[1:2] == colon:
1385            if self.is_windows_fs:
1386                return True
1387            if os.name == "nt":
1388                # special case if we are emulating Posix under Windows
1389                # check if the path exists because it has been mapped in
1390                # this is not foolproof, but handles most cases
1391                try:
1392                    if len(file_path) == 2:
1393                        # avoid recursion, check directly in the entries
1394                        return any(
1395                            [
1396                                entry.upper() == file_path.upper()
1397                                for entry in self.root_dir.entries
1398                            ]
1399                        )
1400                    self.get_object_from_normpath(file_path)
1401                    return True
1402                except OSError:
1403                    return False
1404        return False
1405
1406    def _starts_with_root_path(self, file_path: AnyStr) -> bool:
1407        root_name = matching_string(file_path, self.root.name)
1408        file_path = self._normalize_path_sep(file_path)
1409        return (
1410            file_path.startswith(root_name)
1411            or not self.is_case_sensitive
1412            and file_path.lower().startswith(root_name.lower())
1413            or self.starts_with_drive_letter(file_path)
1414        )
1415
1416    def replace_windows_root(self, path: AnyStr) -> AnyStr:
1417        """In windows, if a path starts with a single separator,
1418        it points to the root dir of the current mount point, usually a
1419        drive - replace it with that mount point path to get the real path.
1420        """
1421        if path and self.is_windows_fs and self.root_dir:
1422            sep = self.get_path_separator(path)
1423            # ignore UNC paths
1424            if path[0:1] == sep and (len(path) == 1 or path[1:2] != sep):
1425                # check if we already have a mount point for that path
1426                for root_path in self.mount_points:
1427                    root_path = matching_string(path, root_path)
1428                    if path.startswith(root_path):
1429                        return path
1430                # must be a pointer to the current drive - replace it
1431                mount_point = matching_string(path, self.root_dir_name)
1432                path = mount_point + path[1:]
1433        return path
1434
1435    def _is_root_path(self, file_path: AnyStr) -> bool:
1436        root_name = matching_string(file_path, self.root.name)
1437        return file_path == root_name or self.is_mount_point(file_path)
1438
1439    def is_mount_point(self, file_path: AnyStr) -> bool:
1440        """Return `True` if `file_path` points to a mount point."""
1441        for mount_point in self.mount_points:
1442            mount_point = matching_string(file_path, mount_point)
1443            if (
1444                file_path == mount_point
1445                or not self.is_case_sensitive
1446                and file_path.lower() == mount_point.lower()
1447            ):
1448                return True
1449            if (
1450                self.is_windows_fs
1451                and len(file_path) == 3
1452                and len(mount_point) == 2
1453                and self.starts_with_drive_letter(file_path)
1454                and file_path[:2].lower() == mount_point.lower()
1455            ):
1456                return True
1457        return False
1458
1459    def ends_with_path_separator(self, path: Union[int, AnyPath]) -> bool:
1460        """Return True if ``file_path`` ends with a valid path separator."""
1461        if isinstance(path, int):
1462            return False
1463        file_path = make_string_path(path)
1464        if not file_path:
1465            return False
1466        sep = self.get_path_separator(file_path)
1467        altsep = self._alternative_path_separator(file_path)
1468        return file_path not in (sep, altsep) and (
1469            file_path.endswith(sep) or altsep is not None and file_path.endswith(altsep)
1470        )
1471
1472    def is_filepath_ending_with_separator(self, path: AnyStr) -> bool:
1473        if not self.ends_with_path_separator(path):
1474            return False
1475        return self.isfile(self._path_without_trailing_separators(path))
1476
1477    def _directory_content(
1478        self, directory: FakeDirectory, component: str
1479    ) -> Tuple[Optional[str], Optional[AnyFile]]:
1480        if not isinstance(directory, FakeDirectory):
1481            return None, None
1482        if component in directory.entries:
1483            return component, directory.entries[component]
1484        if not self.is_case_sensitive:
1485            matching_content = [
1486                (subdir, directory.entries[subdir])
1487                for subdir in directory.entries
1488                if subdir.lower() == component.lower()
1489            ]
1490            if matching_content:
1491                return matching_content[0]
1492
1493        return None, None
1494
1495    def exists(self, file_path: AnyPath, check_link: bool = False) -> bool:
1496        """Return true if a path points to an existing file system object.
1497
1498        Args:
1499            file_path:  The path to examine.
1500            check_link: If True, links are not followed
1501
1502        Returns:
1503            (bool) True if the corresponding object exists.
1504
1505        Raises:
1506            TypeError: if file_path is None.
1507        """
1508        if check_link and self.islink(file_path):
1509            return True
1510        path = to_string(self.make_string_path(file_path))
1511        if path is None:
1512            raise TypeError
1513        if not path:
1514            return False
1515        if path == self.devnull:
1516            return not self.is_windows_fs or sys.version_info >= (3, 8)
1517        try:
1518            if self.is_filepath_ending_with_separator(path):
1519                return False
1520            path = self.resolve_path(path)
1521        except OSError:
1522            return False
1523        if self._is_root_path(path):
1524            return True
1525
1526        path_components: List[str] = self._path_components(path)
1527        current_dir = self.root
1528        for component in path_components:
1529            directory = self._directory_content(current_dir, to_string(component))[1]
1530            if directory is None:
1531                return False
1532            current_dir = cast(FakeDirectory, directory)
1533        return True
1534
1535    def resolve_path(self, file_path: AnyStr, allow_fd: bool = False) -> AnyStr:
1536        """Follow a path, resolving symlinks.
1537
1538        ResolvePath traverses the filesystem along the specified file path,
1539        resolving file names and symbolic links until all elements of the path
1540        are exhausted, or we reach a file which does not exist.
1541        If all the elements are not consumed, they just get appended to the
1542        path resolved so far.
1543        This gives us the path which is as resolved as it can be, even if the
1544        file does not exist.
1545
1546        This behavior mimics Unix semantics, and is best shown by example.
1547        Given a file system that looks like this:
1548
1549              /a/b/
1550              /a/b/c -> /a/b2          c is a symlink to /a/b2
1551              /a/b2/x
1552              /a/c   -> ../d
1553              /a/x   -> y
1554
1555         Then:
1556              /a/b/x      =>  /a/b/x
1557              /a/c        =>  /a/d
1558              /a/x        =>  /a/y
1559              /a/b/c/d/e  =>  /a/b2/d/e
1560
1561        Args:
1562            file_path: The path to examine.
1563            allow_fd: If `True`, `file_path` may be open file descriptor.
1564
1565        Returns:
1566            The resolved_path (str or byte).
1567
1568        Raises:
1569            TypeError: if `file_path` is `None`.
1570            OSError: if `file_path` is '' or a part of the path doesn't exist.
1571        """
1572
1573        if allow_fd and isinstance(file_path, int):
1574            return self.get_open_file(file_path).get_object().path
1575        path = make_string_path(file_path)
1576        if path is None:
1577            # file.open(None) raises TypeError, so mimic that.
1578            raise TypeError("Expected file system path string, received None")
1579        if sys.platform == "win32" and self.os != OSType.WINDOWS:
1580            path = path.replace(
1581                matching_string(path, os.sep),
1582                matching_string(path, self.path_separator),
1583            )
1584        if not path or not self._valid_relative_path(path):
1585            # file.open('') raises OSError, so mimic that, and validate that
1586            # all parts of a relative path exist.
1587            self.raise_os_error(errno.ENOENT, path)
1588        path = self.absnormpath(self._original_path(path))
1589        path = self.replace_windows_root(path)
1590        if self._is_root_path(path):
1591            return path
1592        if path == matching_string(path, self.devnull):
1593            return path
1594        path_components = self._path_components(path)
1595        resolved_components = self._resolve_components(path_components)
1596        path = self._components_to_path(resolved_components)
1597        # after resolving links, we have to check again for Windows root
1598        return self.replace_windows_root(path)  # pytype: disable=bad-return-type
1599
1600    def _components_to_path(self, component_folders):
1601        sep = (
1602            self.get_path_separator(component_folders[0])
1603            if component_folders
1604            else self.path_separator
1605        )
1606        path = sep.join(component_folders)
1607        if not self._starts_with_root_path(path):
1608            path = sep + path
1609        return path
1610
1611    def _resolve_components(self, components: List[AnyStr]) -> List[str]:
1612        current_dir = self.root
1613        link_depth = 0
1614        path_components = [to_string(comp) for comp in components]
1615        resolved_components: List[str] = []
1616        while path_components:
1617            component = path_components.pop(0)
1618            resolved_components.append(component)
1619            directory = self._directory_content(current_dir, component)[1]
1620            if directory is None:
1621                # The component of the path at this point does not actually
1622                # exist in the folder.  We can't resolve the path any more.
1623                # It is legal to link to a file that does not yet exist, so
1624                # rather than raise an error, we just append the remaining
1625                # components to what return path we have built so far and
1626                # return that.
1627                resolved_components.extend(path_components)
1628                break
1629            # Resolve any possible symlinks in the current path component.
1630            elif S_ISLNK(directory.st_mode):
1631                # This link_depth check is not really meant to be an accurate
1632                # check. It is just a quick hack to prevent us from looping
1633                # forever on cycles.
1634                if link_depth > _MAX_LINK_DEPTH:
1635                    self.raise_os_error(
1636                        errno.ELOOP,
1637                        self._components_to_path(resolved_components),
1638                    )
1639                link_path = self._follow_link(resolved_components, directory)
1640
1641                # Following the link might result in the complete replacement
1642                # of the current_dir, so we evaluate the entire resulting path.
1643                target_components = self._path_components(link_path)
1644                path_components = target_components + path_components
1645                resolved_components = []
1646                current_dir = self.root
1647                link_depth += 1
1648            else:
1649                current_dir = cast(FakeDirectory, directory)
1650        return resolved_components
1651
1652    def _valid_relative_path(self, file_path: AnyStr) -> bool:
1653        if self.is_windows_fs:
1654            return True
1655        slash_dotdot = matching_string(file_path, self.path_separator + "..")
1656        while file_path and slash_dotdot in file_path:
1657            file_path = file_path[: file_path.rfind(slash_dotdot)]
1658            if not self.exists(self.absnormpath(file_path)):
1659                return False
1660        return True
1661
1662    def _follow_link(self, link_path_components: List[str], link: AnyFile) -> str:
1663        """Follow a link w.r.t. a path resolved so far.
1664
1665        The component is either a real file, which is a no-op, or a
1666        symlink. In the case of a symlink, we have to modify the path
1667        as built up so far
1668          /a/b => ../c  should yield /a/../c (which will normalize to /a/c)
1669          /a/b => x     should yield /a/x
1670          /a/b => /x/y/z should yield /x/y/z
1671        The modified path may land us in a new spot which is itself a
1672        link, so we may repeat the process.
1673
1674        Args:
1675            link_path_components: The resolved path built up to the link
1676                so far.
1677            link: The link object itself.
1678
1679        Returns:
1680            (string) The updated path resolved after following the link.
1681
1682        Raises:
1683            OSError: if there are too many levels of symbolic link
1684        """
1685        link_path = link.contents
1686        if link_path is not None:
1687            # ignore UNC prefix for local files
1688            if self.is_windows_fs and link_path.startswith("\\\\?\\"):
1689                link_path = link_path[4:]
1690            sep = self.get_path_separator(link_path)
1691            # For links to absolute paths, we want to throw out everything
1692            # in the path built so far and replace with the link. For relative
1693            # links, we have to append the link to what we have so far,
1694            if not self._starts_with_root_path(link_path):
1695                # Relative path. Append remainder of path to what we have
1696                # processed so far, excluding the name of the link itself.
1697                # /a/b => ../c  should yield /a/../c
1698                # (which will normalize to /c)
1699                # /a/b => d should yield a/d
1700                components = link_path_components[:-1]
1701                components.append(link_path)
1702                link_path = sep.join(components)
1703            # Don't call self.NormalizePath(), as we don't want to prepend
1704            # self.cwd.
1705            return self.normpath(link_path)  # pytype: disable=bad-return-type
1706        raise ValueError("Invalid link")
1707
1708    def get_object_from_normpath(
1709        self,
1710        file_path: AnyPath,
1711        check_read_perm: bool = True,
1712        check_exe_perm: bool = True,
1713        check_owner: bool = False,
1714    ) -> AnyFile:
1715        """Search for the specified filesystem object within the fake
1716        filesystem.
1717
1718        Args:
1719            file_path: Specifies target FakeFile object to retrieve, with a
1720                path that has already been normalized/resolved.
1721            check_read_perm: If True, raises OSError if a parent directory
1722                does not have read permission
1723            check_exe_perm: If True, raises OSError if a parent directory
1724                does not have execute (e.g. search) permission
1725            check_owner: If True, and check_read_perm is also True,
1726                only checks read permission if the current user id is
1727                different from the file object user id
1728
1729        Returns:
1730            The FakeFile object corresponding to file_path.
1731
1732        Raises:
1733            OSError: if the object is not found.
1734        """
1735        path = make_string_path(file_path)
1736        if path == matching_string(path, self.root.name):
1737            return self.root
1738        if path == matching_string(path, self.devnull):
1739            return self.dev_null
1740
1741        path = self._original_path(path)
1742        path_components = self._path_components(path)
1743        target = self.root
1744        try:
1745            for component in path_components:
1746                if S_ISLNK(target.st_mode):
1747                    if target.contents:
1748                        target = cast(FakeDirectory, self.resolve(target.contents))
1749                if not S_ISDIR(target.st_mode):
1750                    if not self.is_windows_fs:
1751                        self.raise_os_error(errno.ENOTDIR, path)
1752                    self.raise_os_error(errno.ENOENT, path)
1753                target = target.get_entry(component)  # type: ignore
1754                if (
1755                    not is_root()
1756                    and (check_read_perm or check_exe_perm)
1757                    and target
1758                    and not self._can_read(
1759                        target, check_read_perm, check_exe_perm, check_owner
1760                    )
1761                ):
1762                    self.raise_os_error(errno.EACCES, target.path)
1763        except KeyError:
1764            self.raise_os_error(errno.ENOENT, path)
1765        return target
1766
1767    @staticmethod
1768    def _can_read(target, check_read_perm, check_exe_perm, owner_can_read):
1769        if owner_can_read and target.st_uid == helpers.get_uid():
1770            return True
1771        permission = helpers.PERM_READ if check_read_perm else 0
1772        if S_ISDIR(target.st_mode) and check_exe_perm:
1773            permission |= helpers.PERM_EXE
1774        if not permission:
1775            return True
1776        return target.has_permission(permission)
1777
1778    def get_object(self, file_path: AnyPath, check_read_perm: bool = True) -> FakeFile:
1779        """Search for the specified filesystem object within the fake
1780        filesystem.
1781
1782        Args:
1783            file_path: Specifies the target
1784                :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object to retrieve.
1785            check_read_perm: If True, raises OSError if a parent directory
1786                does not have read permission
1787
1788        Returns:
1789            The :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object corresponding
1790            to `file_path`.
1791
1792        Raises:
1793            OSError: if the object is not found.
1794        """
1795        path = make_string_path(file_path)
1796        path = self.absnormpath(self._original_path(path))
1797        return self.get_object_from_normpath(path, check_read_perm)
1798
1799    def resolve(
1800        self,
1801        file_path: Union[AnyStr, int],
1802        follow_symlinks: bool = True,
1803        allow_fd: bool = False,
1804        check_read_perm: bool = True,
1805        check_exe_perm: bool = True,
1806        check_owner: bool = False,
1807    ) -> FakeFile:
1808        """Search for the specified filesystem object, resolving all links.
1809
1810        Args:
1811            file_path: Specifies the target FakeFile object to retrieve.
1812            follow_symlinks: If `False`, the link itself is resolved,
1813                otherwise the object linked to.
1814            allow_fd: If `True`, `file_path` may be an open file descriptor
1815            check_read_perm: If True, raises OSError if a parent directory
1816                does not have read permission
1817            check_read_perm: If True, raises OSError if a parent directory
1818                does not have execute permission
1819            check_owner: If True, and check_read_perm is also True,
1820                only checks read permission if the current user id is
1821                different from the file object user id
1822
1823        Returns:
1824          The FakeFile object corresponding to `file_path`.
1825
1826        Raises:
1827            OSError: if the object is not found.
1828        """
1829        if isinstance(file_path, int):
1830            if allow_fd:
1831                open_file = self.get_open_file(file_path).get_object()
1832                assert isinstance(open_file, FakeFile)
1833                return open_file
1834            raise TypeError("path should be string, bytes or os.PathLike, not int")
1835
1836        if follow_symlinks:
1837            return self.get_object_from_normpath(
1838                self.resolve_path(file_path, allow_fd),
1839                check_read_perm,
1840                check_exe_perm,
1841                check_owner,
1842            )
1843        return self.lresolve(file_path)
1844
1845    def lresolve(self, path: AnyPath) -> FakeFile:
1846        """Search for the specified object, resolving only parent links.
1847
1848        This is analogous to the stat/lstat difference.  This resolves links
1849        *to* the object but not of the final object itself.
1850
1851        Args:
1852            path: Specifies target FakeFile object to retrieve.
1853
1854        Returns:
1855            The FakeFile object corresponding to path.
1856
1857        Raises:
1858            OSError: if the object is not found.
1859        """
1860        path_str = make_string_path(path)
1861        if not path_str:
1862            raise OSError(errno.ENOENT, path_str)
1863        if path_str == matching_string(path_str, self.root.name):
1864            # The root directory will never be a link
1865            return self.root
1866
1867        # remove trailing separator
1868        path_str = self._path_without_trailing_separators(path_str)
1869        if path_str == matching_string(path_str, "."):
1870            path_str = matching_string(path_str, self.cwd)
1871        path_str = self._original_path(path_str)
1872
1873        parent_directory, child_name = self.splitpath(path_str)
1874        if not parent_directory:
1875            parent_directory = matching_string(path_str, self.cwd)
1876        try:
1877            parent_obj = self.resolve(parent_directory)
1878            assert parent_obj
1879            if not isinstance(parent_obj, FakeDirectory):
1880                if not self.is_windows_fs and isinstance(parent_obj, FakeFile):
1881                    self.raise_os_error(errno.ENOTDIR, path_str)
1882                self.raise_os_error(errno.ENOENT, path_str)
1883            if not parent_obj.has_permission(helpers.PERM_READ):
1884                self.raise_os_error(errno.EACCES, parent_directory)
1885            return (
1886                parent_obj.get_entry(to_string(child_name))
1887                if child_name
1888                else parent_obj
1889            )
1890        except KeyError:
1891            pass
1892        raise OSError(errno.ENOENT, path_str)
1893
1894    def add_object(self, file_path: AnyStr, file_object: AnyFile) -> None:
1895        """Add a fake file or directory into the filesystem at file_path.
1896
1897        Args:
1898            file_path: The path to the file to be added relative to self.
1899            file_object: File or directory to add.
1900
1901        Raises:
1902            OSError: if file_path does not correspond to a
1903                directory.
1904        """
1905        if not file_path:
1906            target_directory = self.root_dir
1907        else:
1908            target_directory = cast(
1909                FakeDirectory,
1910                self.resolve(file_path, check_read_perm=False, check_exe_perm=True),
1911            )
1912            if not S_ISDIR(target_directory.st_mode):
1913                error = errno.ENOENT if self.is_windows_fs else errno.ENOTDIR
1914                self.raise_os_error(error, file_path)
1915        target_directory.add_entry(file_object)
1916
1917    def rename(
1918        self,
1919        old_file_path: AnyPath,
1920        new_file_path: AnyPath,
1921        force_replace: bool = False,
1922    ) -> None:
1923        """Renames a FakeFile object at old_file_path to new_file_path,
1924        preserving all properties.
1925
1926        Args:
1927            old_file_path: Path to filesystem object to rename.
1928            new_file_path: Path to where the filesystem object will live
1929                after this call.
1930            force_replace: If set and destination is an existing file, it
1931                will be replaced even under Windows if the user has
1932                permissions, otherwise replacement happens under Unix only.
1933
1934        Raises:
1935            OSError: if old_file_path does not exist.
1936            OSError: if new_file_path is an existing directory
1937                (Windows, or Posix if old_file_path points to a regular file)
1938            OSError: if old_file_path is a directory and new_file_path a file
1939            OSError: if new_file_path is an existing file and force_replace
1940                not set (Windows only).
1941            OSError: if new_file_path is an existing file and could not be
1942                removed (Posix, or Windows with force_replace set).
1943            OSError: if dirname(new_file_path) does not exist.
1944            OSError: if the file would be moved to another filesystem
1945                (e.g. mount point).
1946        """
1947        old_path = make_string_path(old_file_path)
1948        new_path = make_string_path(new_file_path)
1949        ends_with_sep = self.ends_with_path_separator(old_path)
1950        old_path = self.absnormpath(old_path)
1951        new_path = self.absnormpath(new_path)
1952        if not self.exists(old_path, check_link=True):
1953            self.raise_os_error(errno.ENOENT, old_path, 2)
1954        if ends_with_sep:
1955            self._handle_broken_link_with_trailing_sep(old_path)
1956
1957        old_object = self.lresolve(old_path)
1958        if not self.is_windows_fs:
1959            self._handle_posix_dir_link_errors(new_path, old_path, ends_with_sep)
1960
1961        if self.exists(new_path, check_link=True):
1962            renamed_path = self._rename_to_existing_path(
1963                force_replace, new_path, old_path, old_object, ends_with_sep
1964            )
1965
1966            if renamed_path is None:
1967                return
1968            else:
1969                new_path = renamed_path
1970
1971        old_dir, old_name = self.splitpath(old_path)
1972        new_dir, new_name = self.splitpath(new_path)
1973        if not self.exists(new_dir):
1974            self.raise_os_error(errno.ENOENT, new_dir)
1975        old_dir_object = self.resolve(old_dir)
1976        new_dir_object = self.resolve(new_dir)
1977        if old_dir_object.st_dev != new_dir_object.st_dev:
1978            self.raise_os_error(errno.EXDEV, old_path)
1979        if not S_ISDIR(new_dir_object.st_mode):
1980            self.raise_os_error(
1981                errno.EACCES if self.is_windows_fs else errno.ENOTDIR, new_path
1982            )
1983        if new_dir_object.has_parent_object(old_object):
1984            self.raise_os_error(errno.EINVAL, new_path)
1985
1986        self._do_rename(old_dir_object, old_name, new_dir_object, new_name)
1987
1988    def _do_rename(self, old_dir_object, old_name, new_dir_object, new_name):
1989        object_to_rename = old_dir_object.get_entry(old_name)
1990        old_dir_object.remove_entry(old_name, recursive=False)
1991        object_to_rename.name = new_name
1992        new_name = new_dir_object._normalized_entryname(new_name)
1993        old_entry = (
1994            new_dir_object.get_entry(new_name)
1995            if new_name in new_dir_object.entries
1996            else None
1997        )
1998        try:
1999            if old_entry:
2000                # in case of overwriting remove the old entry first
2001                new_dir_object.remove_entry(new_name)
2002            new_dir_object.add_entry(object_to_rename)
2003        except OSError:
2004            # adding failed, roll back the changes before re-raising
2005            if old_entry and new_name not in new_dir_object.entries:
2006                new_dir_object.add_entry(old_entry)
2007            object_to_rename.name = old_name
2008            old_dir_object.add_entry(object_to_rename)
2009            raise
2010
2011    def _handle_broken_link_with_trailing_sep(self, path: AnyStr) -> None:
2012        # note that the check for trailing sep has to be done earlier
2013        if self.islink(path):
2014            if not self.exists(path):
2015                error = (
2016                    errno.ENOENT
2017                    if self.is_macos
2018                    else errno.EINVAL
2019                    if self.is_windows_fs
2020                    else errno.ENOTDIR
2021                )
2022                self.raise_os_error(error, path)
2023
2024    def _handle_posix_dir_link_errors(
2025        self, new_file_path: AnyStr, old_file_path: AnyStr, ends_with_sep: bool
2026    ) -> None:
2027        if self.isdir(old_file_path, follow_symlinks=False) and self.islink(
2028            new_file_path
2029        ):
2030            self.raise_os_error(errno.ENOTDIR, new_file_path)
2031        if self.isdir(new_file_path, follow_symlinks=False) and self.islink(
2032            old_file_path
2033        ):
2034            if ends_with_sep and self.is_macos:
2035                return
2036            error = errno.ENOTDIR if ends_with_sep else errno.EISDIR
2037            self.raise_os_error(error, new_file_path)
2038        if (
2039            ends_with_sep
2040            and self.islink(old_file_path)
2041            and old_file_path == new_file_path
2042            and not self.is_windows_fs
2043        ):
2044            self.raise_os_error(errno.ENOTDIR, new_file_path)
2045
2046    def _rename_to_existing_path(
2047        self,
2048        force_replace: bool,
2049        new_file_path: AnyStr,
2050        old_file_path: AnyStr,
2051        old_object: FakeFile,
2052        ends_with_sep: bool,
2053    ) -> Optional[AnyStr]:
2054        new_object = self.get_object(new_file_path)
2055        if old_file_path == new_file_path:
2056            if not S_ISLNK(new_object.st_mode) and ends_with_sep:
2057                error = errno.EINVAL if self.is_windows_fs else errno.ENOTDIR
2058                self.raise_os_error(error, old_file_path)
2059            return None  # Nothing to do here
2060
2061        if old_object == new_object:
2062            return self._rename_same_object(new_file_path, old_file_path)
2063        if S_ISDIR(new_object.st_mode) or S_ISLNK(new_object.st_mode):
2064            self._handle_rename_error_for_dir_or_link(
2065                force_replace,
2066                new_file_path,
2067                new_object,
2068                old_object,
2069                ends_with_sep,
2070            )
2071        elif S_ISDIR(old_object.st_mode):
2072            error = errno.EEXIST if self.is_windows_fs else errno.ENOTDIR
2073            self.raise_os_error(error, new_file_path)
2074        elif self.is_windows_fs and not force_replace:
2075            self.raise_os_error(errno.EEXIST, new_file_path)
2076        else:
2077            self.remove_object(new_file_path)
2078        return new_file_path
2079
2080    def _handle_rename_error_for_dir_or_link(
2081        self,
2082        force_replace: bool,
2083        new_file_path: AnyStr,
2084        new_object: FakeFile,
2085        old_object: FakeFile,
2086        ends_with_sep: bool,
2087    ) -> None:
2088        if self.is_windows_fs:
2089            if force_replace:
2090                self.raise_os_error(errno.EACCES, new_file_path)
2091            else:
2092                self.raise_os_error(errno.EEXIST, new_file_path)
2093        if not S_ISLNK(new_object.st_mode):
2094            if new_object.entries:
2095                if (
2096                    not S_ISLNK(old_object.st_mode)
2097                    or not ends_with_sep
2098                    or not self.is_macos
2099                ):
2100                    self.raise_os_error(errno.ENOTEMPTY, new_file_path)
2101            if S_ISREG(old_object.st_mode):
2102                self.raise_os_error(errno.EISDIR, new_file_path)
2103
2104    def _rename_same_object(
2105        self, new_file_path: AnyStr, old_file_path: AnyStr
2106    ) -> Optional[AnyStr]:
2107        do_rename = old_file_path.lower() == new_file_path.lower()
2108        if not do_rename:
2109            try:
2110                real_old_path = self.resolve_path(old_file_path)
2111                original_old_path = self._original_path(real_old_path)
2112                real_new_path = self.resolve_path(new_file_path)
2113                if real_new_path == original_old_path and (
2114                    new_file_path == real_old_path
2115                ) == (new_file_path.lower() == real_old_path.lower()):
2116                    real_object = self.resolve(old_file_path, follow_symlinks=False)
2117                    do_rename = (
2118                        os.path.basename(old_file_path) == real_object.name
2119                        or not self.is_macos
2120                    )
2121                else:
2122                    do_rename = real_new_path.lower() == real_old_path.lower()
2123                if do_rename:
2124                    # only case is changed in case-insensitive file
2125                    # system - do the rename
2126                    parent, file_name = self.splitpath(new_file_path)
2127                    new_file_path = self.joinpaths(
2128                        self._original_path(parent), file_name
2129                    )
2130            except OSError:
2131                # ResolvePath may fail due to symlink loop issues or
2132                # similar - in this case just assume the paths
2133                # to be different
2134                pass
2135        if not do_rename:
2136            # hard links to the same file - nothing to do
2137            return None
2138        return new_file_path
2139
2140    def remove_object(self, file_path: AnyStr) -> None:
2141        """Remove an existing file or directory.
2142
2143        Args:
2144            file_path: The path to the file relative to self.
2145
2146        Raises:
2147            OSError: if file_path does not correspond to an existing file, or
2148                if part of the path refers to something other than a directory.
2149            OSError: if the directory is in use (eg, if it is '/').
2150        """
2151        file_path = self.absnormpath(self._original_path(file_path))
2152        if self._is_root_path(file_path):
2153            self.raise_os_error(errno.EBUSY, file_path)
2154        try:
2155            dirname, basename = self.splitpath(file_path)
2156            target_directory = self.resolve(dirname, check_read_perm=False)
2157            target_directory.remove_entry(basename)
2158        except KeyError:
2159            self.raise_os_error(errno.ENOENT, file_path)
2160        except AttributeError:
2161            self.raise_os_error(errno.ENOTDIR, file_path)
2162
2163    def make_string_path(self, path: AnyPath) -> AnyStr:  # type: ignore[type-var]
2164        path_str = make_string_path(path)
2165        os_sep = matching_string(path_str, os.sep)
2166        fake_sep = self.get_path_separator(path_str)
2167        return path_str.replace(os_sep, fake_sep)  # type: ignore[return-value]
2168
2169    def create_dir(
2170        self,
2171        directory_path: AnyPath,
2172        perm_bits: int = helpers.PERM_DEF,
2173        apply_umask: bool = True,
2174    ) -> FakeDirectory:
2175        """Create `directory_path` and all the parent directories, and return
2176        the created :py:class:`FakeDirectory<pyfakefs.fake_file.FakeDirectory>` object.
2177
2178        Helper method to set up your test faster.
2179
2180        Args:
2181            directory_path: The full directory path to create.
2182            perm_bits: The permission bits as set by ``chmod``.
2183            apply_umask: If `True` (default), the current umask is applied
2184                to `perm_bits`.
2185
2186        Returns:
2187            The newly created
2188            :py:class:`FakeDirectory<pyfakefs.fake_file.FakeDirectory>` object.
2189
2190        Raises:
2191            OSError: if the directory already exists.
2192        """
2193        dir_path = self.make_string_path(directory_path)
2194        dir_path = self.absnormpath(dir_path)
2195        self._auto_mount_drive_if_needed(dir_path)
2196        if self.exists(dir_path, check_link=True) and dir_path not in self.mount_points:
2197            self.raise_os_error(errno.EEXIST, dir_path)
2198        path_components = self._path_components(dir_path)
2199        current_dir = self.root
2200
2201        new_dirs = []
2202        for component in [to_string(p) for p in path_components]:
2203            directory = self._directory_content(current_dir, to_string(component))[1]
2204            if not directory:
2205                new_dir = FakeDirectory(component, filesystem=self)
2206                new_dirs.append(new_dir)
2207                if self.is_windows_fs and current_dir == self.root:
2208                    current_dir = self.root_dir
2209                current_dir.add_entry(new_dir)
2210                current_dir = new_dir
2211            else:
2212                if S_ISLNK(directory.st_mode):
2213                    assert directory.contents
2214                    directory = self.resolve(directory.contents)
2215                    assert directory
2216                current_dir = cast(FakeDirectory, directory)
2217                if directory.st_mode & S_IFDIR != S_IFDIR:
2218                    self.raise_os_error(errno.ENOTDIR, current_dir.path)
2219
2220        # set the permission after creating the directories
2221        # to allow directory creation inside a read-only directory
2222        for new_dir in new_dirs:
2223            if apply_umask:
2224                perm_bits &= ~self.umask
2225            new_dir.st_mode = S_IFDIR | perm_bits
2226
2227        return current_dir
2228
2229    def create_file(
2230        self,
2231        file_path: AnyPath,
2232        st_mode: int = S_IFREG | helpers.PERM_DEF_FILE,
2233        contents: AnyString = "",
2234        st_size: Optional[int] = None,
2235        create_missing_dirs: bool = True,
2236        apply_umask: bool = True,
2237        encoding: Optional[str] = None,
2238        errors: Optional[str] = None,
2239        side_effect: Optional[Callable] = None,
2240    ) -> FakeFile:
2241        """Create `file_path`, including all the parent directories along
2242        the way, and return the created
2243        :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object.
2244
2245        This helper method can be used to set up tests more easily.
2246
2247        Args:
2248            file_path: The path to the file to create.
2249            st_mode: The `stat` constant representing the file type.
2250            contents: the contents of the file. If not given and `st_size` is
2251                `None`, an empty file is assumed.
2252            st_size: file size; only valid if contents not given. If given,
2253                the file is considered to be in "large file mode" and trying
2254                to read from or write to the file will result in an exception.
2255            create_missing_dirs: If `True`, auto create missing directories.
2256            apply_umask: If `True` (default), the current umask is applied
2257                to `st_mode`.
2258            encoding: If `contents` is of type `str`, the encoding used
2259                for serialization.
2260            errors: The error mode used for encoding/decoding errors.
2261            side_effect: function handle that is executed when the file is written,
2262                must accept the file object as an argument.
2263
2264        Returns:
2265            The newly created :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object.
2266
2267        Raises:
2268            OSError: if the file already exists.
2269            OSError: if the containing directory is required and missing.
2270        """
2271        return self.create_file_internally(
2272            file_path,
2273            st_mode,
2274            contents,
2275            st_size,
2276            create_missing_dirs,
2277            apply_umask,
2278            encoding,
2279            errors,
2280            side_effect=side_effect,
2281        )
2282
2283    def add_real_file(
2284        self,
2285        source_path: AnyPath,
2286        read_only: bool = True,
2287        target_path: Optional[AnyPath] = None,
2288    ) -> FakeFile:
2289        """Create `file_path`, including all the parent directories along the
2290        way, for an existing real file, and return the created
2291        :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object.
2292        The contents of the real file are read only on demand.
2293
2294        Args:
2295            source_path: Path to an existing file in the real file system
2296            read_only: If `True` (the default), writing to the fake file
2297                raises an exception.  Otherwise, writing to the file changes
2298                the fake file only.
2299            target_path: If given, the path of the target direction,
2300                otherwise it is equal to `source_path`.
2301
2302        Returns:
2303            the newly created :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object.
2304
2305        Raises:
2306            OSError: if the file does not exist in the real file system.
2307            OSError: if the file already exists in the fake file system.
2308
2309        .. note:: On most systems, accessing the fake file's contents may
2310            update both the real and fake files' `atime` (access time).
2311            In this particular case, `add_real_file()` violates the rule
2312            that `pyfakefs` must not modify the real file system.
2313        """
2314        target_path = target_path or source_path
2315        source_path_str = make_string_path(source_path)
2316        real_stat = os.stat(source_path_str)
2317        fake_file = self.create_file_internally(target_path, read_from_real_fs=True)
2318
2319        # for read-only mode, remove the write/executable permission bits
2320        fake_file.stat_result.set_from_stat_result(real_stat)
2321        if read_only:
2322            fake_file.st_mode &= 0o777444
2323        fake_file.file_path = source_path_str
2324        self.change_disk_usage(fake_file.size, fake_file.name, fake_file.st_dev)
2325        return fake_file
2326
2327    def add_real_symlink(
2328        self, source_path: AnyPath, target_path: Optional[AnyPath] = None
2329    ) -> FakeFile:
2330        """Create a symlink at `source_path` (or `target_path`, if given) and return
2331        the created :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object.
2332        It will point to the same path as the symlink on the real filesystem.
2333        Relative symlinks will point relative to their new location.  Absolute symlinks
2334        will point to the same, absolute path as on the real filesystem.
2335
2336        Args:
2337            source_path: The path to the existing symlink.
2338            target_path: If given, the name of the symlink in the fake
2339                filesystem, otherwise, the same as `source_path`.
2340
2341        Returns:
2342            the newly created :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object.
2343
2344        Raises:
2345            OSError: if the directory does not exist in the real file system.
2346            OSError: if the symlink could not be created
2347                (see :py:meth:`create_file`).
2348            OSError: if the directory already exists in the fake file system.
2349        """
2350        source_path_str = make_string_path(source_path)  # TODO: add test
2351        source_path_str = self._path_without_trailing_separators(source_path_str)
2352        if not os.path.exists(source_path_str) and not os.path.islink(source_path_str):
2353            self.raise_os_error(errno.ENOENT, source_path_str)
2354
2355        target = os.readlink(source_path_str)
2356
2357        if target_path:
2358            return self.create_symlink(target_path, target)
2359        else:
2360            return self.create_symlink(source_path_str, target)
2361
2362    def add_real_directory(
2363        self,
2364        source_path: AnyPath,
2365        read_only: bool = True,
2366        lazy_read: bool = True,
2367        target_path: Optional[AnyPath] = None,
2368    ) -> FakeDirectory:
2369        """Create a fake directory corresponding to the real directory at the
2370        specified path, and return the created
2371        :py:class:`FakeDirectory<pyfakefs.fake_file.FakeDirectory>` object.
2372        Add entries in the fake directory corresponding to
2373        the entries in the real directory.  Symlinks are supported.
2374        If the target directory already exists in the fake filesystem, the directory
2375        contents are merged. Overwriting existing files is not allowed.
2376
2377        Args:
2378            source_path: The path to the existing directory.
2379            read_only: If set, all files under the directory are treated as
2380                read-only, e.g. a write access raises an exception;
2381                otherwise, writing to the files changes the fake files only
2382                as usually.
2383            lazy_read: If set (default), directory contents are only read when
2384                accessed, and only until the needed subdirectory level.
2385
2386                .. note:: This means that the file system size is only updated
2387                  at the time the directory contents are read; set this to
2388                  `False` only if you are dependent on accurate file system
2389                  size in your test
2390            target_path: If given, the target directory, otherwise,
2391                the target directory is the same as `source_path`.
2392
2393        Returns:
2394            the newly created
2395            :py:class:`FakeDirectory<pyfakefs.fake_file.FakeDirectory>` object.
2396
2397        Raises:
2398            OSError: if the directory does not exist in the real filesystem.
2399            OSError: if a file or link exists in the fake filesystem where a real
2400                file or directory shall be mapped.
2401        """
2402        source_path_str = make_string_path(source_path)
2403        source_path_str = self._path_without_trailing_separators(source_path_str)
2404        if not os.path.exists(source_path_str):
2405            self.raise_os_error(errno.ENOENT, source_path_str)
2406        target_path_str = make_string_path(target_path or source_path_str)
2407
2408        # get rid of inconsistencies between real and fake path separators
2409        if os.altsep is not None:
2410            target_path_str = os.path.normpath(target_path_str)
2411        if os.sep != self.path_separator:
2412            target_path_str = target_path_str.replace(os.sep, self.path_separator)
2413
2414        self._auto_mount_drive_if_needed(target_path_str)
2415        if lazy_read:
2416            self._create_fake_from_real_dir_lazily(
2417                source_path_str, target_path_str, read_only
2418            )
2419        else:
2420            self._create_fake_from_real_dir(source_path_str, target_path_str, read_only)
2421        return cast(FakeDirectory, self.get_object(target_path_str))
2422
2423    def _create_fake_from_real_dir(self, source_path_str, target_path_str, read_only):
2424        if not self.exists(target_path_str):
2425            self.create_dir(target_path_str)
2426        for base, _, files in os.walk(source_path_str):
2427            new_base = os.path.join(
2428                target_path_str,
2429                os.path.relpath(base, source_path_str),
2430            )
2431            for file_entry in os.listdir(base):
2432                file_path = os.path.join(base, file_entry)
2433                if os.path.islink(file_path):
2434                    self.add_real_symlink(file_path, os.path.join(new_base, file_entry))
2435            for file_entry in files:
2436                path = os.path.join(base, file_entry)
2437                if not os.path.islink(path):
2438                    self.add_real_file(
2439                        path, read_only, os.path.join(new_base, file_entry)
2440                    )
2441
2442    def _create_fake_from_real_dir_lazily(
2443        self, source_path_str, target_path_str, read_only
2444    ):
2445        if self.exists(target_path_str):
2446            if not self.isdir(target_path_str):
2447                raise OSError(errno.ENOTDIR, "Mapping target is not a directory")
2448            for entry in os.listdir(source_path_str):
2449                src_entry_path = os.path.join(source_path_str, entry)
2450                target_entry_path = os.path.join(target_path_str, entry)
2451                if os.path.isdir(src_entry_path):
2452                    self.add_real_directory(
2453                        src_entry_path, read_only, True, target_entry_path
2454                    )
2455                elif os.path.islink(src_entry_path):
2456                    self.add_real_symlink(src_entry_path, target_entry_path)
2457                elif os.path.isfile(src_entry_path):
2458                    self.add_real_file(src_entry_path, read_only, target_entry_path)
2459            return self.get_object(target_path_str)
2460
2461        parent_path = os.path.split(target_path_str)[0]
2462        if self.exists(parent_path):
2463            parent_dir = self.get_object(parent_path)
2464        else:
2465            parent_dir = self.create_dir(parent_path)
2466        new_dir = FakeDirectoryFromRealDirectory(
2467            source_path_str, self, read_only, target_path_str
2468        )
2469        parent_dir.add_entry(new_dir)
2470        return new_dir
2471
2472    def add_real_paths(
2473        self,
2474        path_list: List[AnyStr],
2475        read_only: bool = True,
2476        lazy_dir_read: bool = True,
2477    ) -> None:
2478        """This convenience method adds multiple files and/or directories from
2479        the real file system to the fake file system. See :py:meth:`add_real_file` and
2480        :py:meth:`add_real_directory`.
2481
2482        Args:
2483            path_list: List of file and directory paths in the real file
2484                system.
2485            read_only: If set, all files and files under the directories
2486                are treated as read-only, e.g. a write access raises an
2487                exception; otherwise, writing to the files changes the fake
2488                files only as usually.
2489            lazy_dir_read: Uses lazy reading of directory contents if set
2490                (see :py:meth:`add_real_directory`)
2491
2492        Raises:
2493            OSError: if any of the files and directories in the list
2494                does not exist in the real file system.
2495            OSError: if a file or link exists in the fake filesystem where a real
2496                file or directory shall be mapped.
2497        """
2498        for path in path_list:
2499            if os.path.isdir(path):
2500                self.add_real_directory(path, read_only, lazy_dir_read)
2501            else:
2502                self.add_real_file(path, read_only)
2503
2504    def create_file_internally(
2505        self,
2506        file_path: AnyPath,
2507        st_mode: int = S_IFREG | helpers.PERM_DEF_FILE,
2508        contents: AnyString = "",
2509        st_size: Optional[int] = None,
2510        create_missing_dirs: bool = True,
2511        apply_umask: bool = True,
2512        encoding: Optional[str] = None,
2513        errors: Optional[str] = None,
2514        read_from_real_fs: bool = False,
2515        side_effect: Optional[Callable] = None,
2516    ) -> FakeFile:
2517        """Internal fake file creator that supports both normal fake files
2518        and fake files based on real files.
2519
2520        Args:
2521            file_path: path to the file to create.
2522            st_mode: the stat.S_IF constant representing the file type.
2523            contents: the contents of the file. If not given and st_size is
2524                None, an empty file is assumed.
2525            st_size: file size; only valid if contents not given. If given,
2526                the file is considered to be in "large file mode" and trying
2527                to read from or write to the file will result in an exception.
2528            create_missing_dirs: if True, auto create missing directories.
2529            apply_umask: whether or not the current umask must be applied
2530                on st_mode.
2531            encoding: if contents is a unicode string, the encoding used for
2532                serialization.
2533            errors: the error mode used for encoding/decoding errors
2534            read_from_real_fs: if True, the contents are read from the real
2535                file system on demand.
2536            side_effect: function handle that is executed when file is written,
2537                must accept the file object as an argument.
2538        """
2539        path = self.make_string_path(file_path)
2540        path = self.absnormpath(path)
2541        if not is_int_type(st_mode):
2542            raise TypeError(
2543                "st_mode must be of int type - did you mean to set contents?"
2544            )
2545
2546        if self.exists(path, check_link=True):
2547            self.raise_os_error(errno.EEXIST, path)
2548        parent_directory, new_file = self.splitpath(path)
2549        if not parent_directory:
2550            parent_directory = matching_string(path, self.cwd)
2551        self._auto_mount_drive_if_needed(parent_directory)
2552        if not self.exists(parent_directory):
2553            if not create_missing_dirs:
2554                self.raise_os_error(errno.ENOENT, parent_directory)
2555            parent_directory = matching_string(
2556                path,
2557                self.create_dir(parent_directory).path,  # type: ignore
2558            )
2559        else:
2560            parent_directory = self._original_path(parent_directory)
2561        if apply_umask:
2562            st_mode &= ~self.umask
2563        file_object: FakeFile
2564        if read_from_real_fs:
2565            file_object = FakeFileFromRealFile(
2566                to_string(path), filesystem=self, side_effect=side_effect
2567            )
2568        else:
2569            file_object = FakeFile(
2570                new_file,
2571                st_mode,
2572                filesystem=self,
2573                encoding=encoding,
2574                errors=errors,
2575                side_effect=side_effect,
2576            )
2577
2578        self.add_object(parent_directory, file_object)
2579
2580        if st_size is None and contents is None:
2581            contents = ""
2582        if not read_from_real_fs and (contents is not None or st_size is not None):
2583            try:
2584                if st_size is not None:
2585                    file_object.set_large_file_size(st_size)
2586                else:
2587                    file_object.set_initial_contents(contents)  # type: ignore
2588            except OSError:
2589                self.remove_object(path)
2590                raise
2591
2592        return file_object
2593
2594    def create_symlink(
2595        self,
2596        file_path: AnyPath,
2597        link_target: AnyPath,
2598        create_missing_dirs: bool = True,
2599    ) -> FakeFile:
2600        """Create the specified symlink, pointed at the specified link target,
2601        and return the created :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object
2602        representing the link.
2603
2604        Args:
2605            file_path:  path to the symlink to create
2606            link_target:  the target of the symlink
2607            create_missing_dirs: If `True`, any missing parent directories of
2608                `file_path` will be created
2609
2610        Returns:
2611            The newly created :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object.
2612
2613        Raises:
2614            OSError: if the symlink could not be created
2615                (see :py:meth:`create_file`).
2616        """
2617        link_path = self.make_string_path(file_path)
2618        link_target_path = self.make_string_path(link_target)
2619        link_path = self.normcase(link_path)
2620        # the link path cannot end with a path separator
2621        if self.ends_with_path_separator(link_path):
2622            if self.exists(link_path):
2623                self.raise_os_error(errno.EEXIST, link_path)
2624            if self.exists(link_target_path):
2625                if not self.is_windows_fs:
2626                    self.raise_os_error(errno.ENOENT, link_path)
2627            else:
2628                if self.is_windows_fs:
2629                    self.raise_os_error(errno.EINVAL, link_target_path)
2630                if not self.exists(
2631                    self._path_without_trailing_separators(link_path),
2632                    check_link=True,
2633                ):
2634                    self.raise_os_error(errno.ENOENT, link_target_path)
2635                if self.is_macos:
2636                    # to avoid EEXIST exception, remove the link
2637                    # if it already exists
2638                    if self.exists(link_path, check_link=True):
2639                        self.remove_object(link_path)
2640                else:
2641                    self.raise_os_error(errno.EEXIST, link_target_path)
2642
2643        # resolve the link path only if it is not a link itself
2644        if not self.islink(link_path):
2645            link_path = self.resolve_path(link_path)
2646        permission = helpers.PERM_DEF_FILE if self.is_windows_fs else helpers.PERM_DEF
2647        return self.create_file_internally(
2648            link_path,
2649            st_mode=S_IFLNK | permission,
2650            contents=link_target_path,
2651            create_missing_dirs=create_missing_dirs,
2652            apply_umask=self.is_macos,
2653        )
2654
2655    def create_link(
2656        self,
2657        old_path: AnyPath,
2658        new_path: AnyPath,
2659        follow_symlinks: bool = True,
2660        create_missing_dirs: bool = True,
2661    ) -> FakeFile:
2662        """Create a hard link at `new_path`, pointing at `old_path`,
2663        and return the created :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object
2664        representing the link.
2665
2666        Args:
2667            old_path: An existing link to the target file.
2668            new_path: The destination path to create a new link at.
2669            follow_symlinks: If `False` and `old_path` is a symlink, link the
2670                symlink instead of the object it points to.
2671            create_missing_dirs: If `True`, any missing parent directories of
2672                `file_path` will be created
2673
2674        Returns:
2675            The :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object referred to
2676            by `old_path`.
2677
2678        Raises:
2679            OSError:  if something already exists at `new_path`.
2680            OSError:  if `old_path` is a directory.
2681            OSError:  if the parent directory doesn't exist.
2682        """
2683        old_path_str = make_string_path(old_path)
2684        new_path_str = make_string_path(new_path)
2685        new_path_normalized = self.absnormpath(new_path_str)
2686        if self.exists(new_path_normalized, check_link=True):
2687            self.raise_os_error(errno.EEXIST, new_path_str)
2688
2689        new_parent_directory, new_basename = self.splitpath(new_path_normalized)
2690        if not new_parent_directory:
2691            new_parent_directory = matching_string(new_path_str, self.cwd)
2692
2693        if not self.exists(new_parent_directory):
2694            if create_missing_dirs:
2695                self.create_dir(new_parent_directory)
2696            else:
2697                self.raise_os_error(errno.ENOENT, new_parent_directory)
2698
2699        if self.ends_with_path_separator(old_path_str):
2700            error = errno.EINVAL if self.is_windows_fs else errno.ENOTDIR
2701            self.raise_os_error(error, old_path_str)
2702
2703        if not self.is_windows_fs and self.ends_with_path_separator(new_path):
2704            self.raise_os_error(errno.ENOENT, old_path_str)
2705
2706        # Retrieve the target file
2707        try:
2708            old_file = self.resolve(old_path_str, follow_symlinks=follow_symlinks)
2709        except OSError:
2710            self.raise_os_error(errno.ENOENT, old_path_str)
2711
2712        if old_file.st_mode & S_IFDIR:
2713            self.raise_os_error(
2714                errno.EACCES if self.is_windows_fs else errno.EPERM,
2715                old_path_str,
2716            )
2717
2718        # abuse the name field to control the filename of the
2719        # newly created link
2720        old_file.name = new_basename  # type: ignore[assignment]
2721        self.add_object(new_parent_directory, old_file)
2722        return old_file
2723
2724    def link(
2725        self,
2726        old_path: AnyPath,
2727        new_path: AnyPath,
2728        follow_symlinks: bool = True,
2729    ) -> FakeFile:
2730        """Create a hard link at new_path, pointing at old_path.
2731
2732        Args:
2733            old_path: An existing link to the target file.
2734            new_path: The destination path to create a new link at.
2735            follow_symlinks: If False and old_path is a symlink, link the
2736                symlink instead of the object it points to.
2737
2738        Returns:
2739            The FakeFile object referred to by old_path.
2740
2741        Raises:
2742            OSError:  if something already exists at new_path.
2743            OSError:  if old_path is a directory.
2744            OSError:  if the parent directory doesn't exist.
2745        """
2746        return self.create_link(
2747            old_path, new_path, follow_symlinks, create_missing_dirs=False
2748        )
2749
2750    def _is_circular_link(self, link_obj: FakeFile) -> bool:
2751        try:
2752            assert link_obj.contents
2753            self.resolve_path(link_obj.contents)
2754        except OSError as exc:
2755            return exc.errno == errno.ELOOP
2756        return False
2757
2758    def readlink(self, path: AnyPath) -> str:
2759        """Read the target of a symlink.
2760
2761        Args:
2762            path:  symlink to read the target of.
2763
2764        Returns:
2765            the string representing the path to which the symbolic link points.
2766
2767        Raises:
2768            TypeError: if path is None
2769            OSError: (with errno=ENOENT) if path is not a valid path, or
2770                (with errno=EINVAL) if path is valid, but is not a symlink,
2771                or if the path ends with a path separator (Posix only)
2772        """
2773        if path is None:
2774            raise TypeError
2775        link_path = make_string_path(path)
2776        link_obj = self.lresolve(link_path)
2777        if S_IFMT(link_obj.st_mode) != S_IFLNK:
2778            self.raise_os_error(errno.EINVAL, link_path)
2779
2780        if self.ends_with_path_separator(link_path):
2781            if not self.is_windows_fs and self.exists(link_path):
2782                self.raise_os_error(errno.EINVAL, link_path)
2783            if not self.exists(link_obj.path):  # type: ignore
2784                if self.is_windows_fs:
2785                    error = errno.EINVAL
2786                elif self._is_circular_link(link_obj):
2787                    if self.is_macos:
2788                        return link_obj.path  # type: ignore[return-value]
2789                    error = errno.ELOOP
2790                else:
2791                    error = errno.ENOENT
2792                self.raise_os_error(error, link_obj.path)
2793
2794        assert link_obj.contents
2795        return link_obj.contents
2796
2797    def makedir(self, dir_path: AnyPath, mode: int = helpers.PERM_DEF) -> None:
2798        """Create a leaf Fake directory.
2799
2800        Args:
2801            dir_path: (str) Name of directory to create.
2802                Relative paths are assumed to be relative to '/'.
2803            mode: (int) Mode to create directory with.  This argument defaults
2804                to 0o777. The umask is applied to this mode.
2805
2806        Raises:
2807            OSError: if the directory name is invalid or parent directory is
2808                read only or as per :py:meth:`add_object`.
2809        """
2810        dir_name = make_string_path(dir_path)
2811        ends_with_sep = self.ends_with_path_separator(dir_name)
2812        dir_name = self._path_without_trailing_separators(dir_name)
2813        if not dir_name:
2814            self.raise_os_error(errno.ENOENT, "")
2815
2816        if self.is_windows_fs:
2817            dir_name = self.absnormpath(dir_name)
2818        parent_dir, rest = self.splitpath(dir_name)
2819        if parent_dir:
2820            base_dir = self.normpath(parent_dir)
2821            ellipsis = matching_string(parent_dir, self.path_separator + "..")
2822            if parent_dir.endswith(ellipsis) and not self.is_windows_fs:
2823                base_dir, dummy_dotdot, _ = parent_dir.partition(ellipsis)
2824            if self.is_windows_fs and not rest and not self.exists(base_dir):
2825                # under Windows, the parent dir may be a drive or UNC path
2826                # which has to be mounted
2827                self._auto_mount_drive_if_needed(parent_dir)
2828            if not self.exists(base_dir):
2829                self.raise_os_error(errno.ENOENT, base_dir)
2830
2831        dir_name = self.absnormpath(dir_name)
2832        if self.exists(dir_name, check_link=True):
2833            if self.is_windows_fs and dir_name == self.root_dir_name:
2834                error_nr = errno.EACCES
2835            else:
2836                error_nr = errno.EEXIST
2837            if ends_with_sep and self.is_macos and not self.exists(dir_name):
2838                # to avoid EEXIST exception, remove the link
2839                self.remove_object(dir_name)
2840            else:
2841                self.raise_os_error(error_nr, dir_name)
2842        head, tail = self.splitpath(dir_name)
2843
2844        self.add_object(
2845            to_string(head),
2846            FakeDirectory(to_string(tail), mode & ~self.umask, filesystem=self),
2847        )
2848
2849    def _path_without_trailing_separators(self, path: AnyStr) -> AnyStr:
2850        while self.ends_with_path_separator(path):
2851            path = path[:-1]
2852        return path
2853
2854    def makedirs(
2855        self, dir_name: AnyStr, mode: int = helpers.PERM_DEF, exist_ok: bool = False
2856    ) -> None:
2857        """Create a leaf Fake directory and create any non-existent
2858        parent dirs.
2859
2860        Args:
2861            dir_name: (str) Name of directory to create.
2862            mode: (int) Mode to create directory (and any necessary parent
2863                directories) with. This argument defaults to 0o777.
2864                The umask is applied to this mode.
2865          exist_ok: (boolean) If exist_ok is False (the default), an OSError is
2866                raised if the target directory already exists.
2867
2868        Raises:
2869            OSError: if the directory already exists and exist_ok=False,
2870                or as per :py:meth:`create_dir`.
2871        """
2872        if not dir_name:
2873            self.raise_os_error(errno.ENOENT, "")
2874        ends_with_sep = self.ends_with_path_separator(dir_name)
2875        dir_name = self.absnormpath(dir_name)
2876        if (
2877            ends_with_sep
2878            and self.is_macos
2879            and self.exists(dir_name, check_link=True)
2880            and not self.exists(dir_name)
2881        ):
2882            # to avoid EEXIST exception, remove the link
2883            self.remove_object(dir_name)
2884
2885        dir_name_str = to_string(dir_name)
2886        path_components = self._path_components(dir_name_str)
2887
2888        # Raise a permission denied error if the first existing directory
2889        # is not writeable.
2890        current_dir = self.root_dir
2891        for component in path_components:
2892            if (
2893                not hasattr(current_dir, "entries")
2894                or component not in current_dir.entries
2895            ):
2896                break
2897            else:
2898                current_dir = cast(FakeDirectory, current_dir.entries[component])
2899        try:
2900            self.create_dir(dir_name, mode)
2901        except OSError as e:
2902            if e.errno == errno.EACCES:
2903                # permission denied - propagate exception
2904                raise
2905            if not exist_ok or not isinstance(self.resolve(dir_name), FakeDirectory):
2906                if self.is_windows_fs and e.errno == errno.ENOTDIR:
2907                    e.errno = errno.ENOENT
2908                # mypy thinks that errno may be None
2909                self.raise_os_error(cast(int, e.errno), e.filename)
2910
2911    def _is_of_type(
2912        self,
2913        path: AnyPath,
2914        st_flag: int,
2915        follow_symlinks: bool = True,
2916        check_read_perm: bool = True,
2917    ) -> bool:
2918        """Helper function to implement isdir(), islink(), etc.
2919
2920        See the stat(2) man page for valid stat.S_I* flag values
2921
2922        Args:
2923            path: Path to file to stat and test
2924            st_flag: The stat.S_I* flag checked for the file's st_mode
2925            check_read_perm: If True (default) False is returned for
2926                existing but unreadable file paths.
2927
2928        Returns:
2929            (boolean) `True` if the st_flag is set in path's st_mode.
2930
2931        Raises:
2932          TypeError: if path is None
2933        """
2934        if path is None:
2935            raise TypeError
2936        file_path = make_string_path(path)
2937        try:
2938            obj = self.resolve(
2939                file_path, follow_symlinks, check_read_perm=check_read_perm
2940            )
2941            if obj:
2942                self.raise_for_filepath_ending_with_separator(
2943                    file_path, obj, macos_handling=not follow_symlinks
2944                )
2945                return S_IFMT(obj.st_mode) == st_flag
2946        except OSError:
2947            return False
2948        return False
2949
2950    def isdir(self, path: AnyPath, follow_symlinks: bool = True) -> bool:
2951        """Determine if path identifies a directory.
2952
2953        Args:
2954            path: Path to filesystem object.
2955
2956        Returns:
2957            `True` if path points to a directory (following symlinks).
2958
2959        Raises:
2960            TypeError: if path is None.
2961        """
2962        return self._is_of_type(path, S_IFDIR, follow_symlinks)
2963
2964    def isfile(self, path: AnyPath, follow_symlinks: bool = True) -> bool:
2965        """Determine if path identifies a regular file.
2966
2967        Args:
2968            path: Path to filesystem object.
2969
2970        Returns:
2971            `True` if path points to a regular file (following symlinks).
2972
2973        Raises:
2974            TypeError: if path is None.
2975        """
2976        return self._is_of_type(path, S_IFREG, follow_symlinks, check_read_perm=False)
2977
2978    def islink(self, path: AnyPath) -> bool:
2979        """Determine if path identifies a symbolic link.
2980
2981        Args:
2982            path: Path to filesystem object.
2983
2984        Returns:
2985            `True` if path points to a symlink (S_IFLNK set in st_mode)
2986
2987        Raises:
2988            TypeError: if path is None.
2989        """
2990        return self._is_of_type(path, S_IFLNK, follow_symlinks=False)
2991
2992    if sys.version_info >= (3, 12):
2993
2994        def isjunction(self, path: AnyPath) -> bool:
2995            """Returns False. Junctions are never faked."""
2996            return False
2997
2998    def confirmdir(
2999        self,
3000        target_directory: AnyStr,
3001        check_read_perm: bool = True,
3002        check_exe_perm: bool = True,
3003        check_owner: bool = False,
3004    ) -> FakeDirectory:
3005        """Test that the target is actually a directory, raising OSError
3006        if not.
3007
3008        Args:
3009            target_directory: Path to the target directory within the fake
3010                filesystem.
3011            check_read_perm: If True, raises OSError if the directory
3012                does not have read permission
3013            check_exe_perm: If True, raises OSError if the directory
3014                does not have execute (e.g. search) permission
3015            check_owner: If True, only checks read permission if the current
3016                user id is different from the file object user id
3017
3018        Returns:
3019            The FakeDirectory object corresponding to target_directory.
3020
3021        Raises:
3022            OSError: if the target is not a directory.
3023        """
3024        directory = cast(
3025            FakeDirectory,
3026            self.resolve(
3027                target_directory,
3028                check_read_perm=check_read_perm,
3029                check_exe_perm=check_exe_perm,
3030                check_owner=check_owner,
3031            ),
3032        )
3033        if not directory.st_mode & S_IFDIR:
3034            self.raise_os_error(errno.ENOTDIR, target_directory, 267)
3035        return directory
3036
3037    def remove(self, path: AnyStr) -> None:
3038        """Remove the FakeFile object at the specified file path.
3039
3040        Args:
3041            path: Path to file to be removed.
3042
3043        Raises:
3044            OSError: if path points to a directory.
3045            OSError: if path does not exist.
3046            OSError: if removal failed.
3047        """
3048        norm_path = make_string_path(path)
3049        norm_path = self.absnormpath(norm_path)
3050        if self.ends_with_path_separator(path):
3051            self._handle_broken_link_with_trailing_sep(norm_path)
3052        if self.exists(norm_path):
3053            obj = self.resolve(norm_path, check_read_perm=False)
3054            if S_IFMT(obj.st_mode) == S_IFDIR:
3055                link_obj = self.lresolve(norm_path)
3056                if S_IFMT(link_obj.st_mode) != S_IFLNK:
3057                    if self.is_windows_fs:
3058                        error = errno.EACCES
3059                    elif self.is_macos:
3060                        error = errno.EPERM
3061                    else:
3062                        error = errno.EISDIR
3063                    self.raise_os_error(error, norm_path)
3064
3065                if path.endswith(self.get_path_separator(path)):
3066                    if self.is_windows_fs:
3067                        error = errno.EACCES
3068                    elif self.is_macos:
3069                        error = errno.EPERM
3070                    else:
3071                        error = errno.ENOTDIR
3072                    self.raise_os_error(error, norm_path)
3073            else:
3074                self.raise_for_filepath_ending_with_separator(path, obj)
3075
3076        self.remove_object(norm_path)
3077
3078    def rmdir(self, target_directory: AnyStr, allow_symlink: bool = False) -> None:
3079        """Remove a leaf Fake directory.
3080
3081        Args:
3082            target_directory: (str) Name of directory to remove.
3083            allow_symlink: (bool) if `target_directory` is a symlink,
3084                the function just returns, otherwise it raises (Posix only)
3085
3086        Raises:
3087            OSError: if target_directory does not exist.
3088            OSError: if target_directory does not point to a directory.
3089            OSError: if removal failed per FakeFilesystem.RemoveObject.
3090                Cannot remove '.'.
3091        """
3092        if target_directory == matching_string(target_directory, "."):
3093            error_nr = errno.EACCES if self.is_windows_fs else errno.EINVAL
3094            self.raise_os_error(error_nr, target_directory)
3095        ends_with_sep = self.ends_with_path_separator(target_directory)
3096        target_directory = self.absnormpath(target_directory)
3097        if self.confirmdir(target_directory, check_owner=True):
3098            if not self.is_windows_fs and self.islink(target_directory):
3099                if allow_symlink:
3100                    return
3101                if not ends_with_sep or not self.is_macos:
3102                    self.raise_os_error(errno.ENOTDIR, target_directory)
3103
3104            dir_object = self.resolve(target_directory, check_owner=True)
3105            if dir_object.entries:
3106                self.raise_os_error(errno.ENOTEMPTY, target_directory)
3107            self.remove_object(target_directory)
3108
3109    def listdir(self, target_directory: AnyStr) -> List[AnyStr]:
3110        """Return a list of file names in target_directory.
3111
3112        Args:
3113            target_directory: Path to the target directory within the
3114                fake filesystem.
3115
3116        Returns:
3117            A list of file names within the target directory in arbitrary
3118            order. If `shuffle_listdir_results` is set, the order is not the
3119            same in subsequent calls to avoid tests relying on any ordering.
3120
3121        Raises:
3122            OSError: if the target is not a directory.
3123        """
3124        target_directory = self.resolve_path(target_directory, allow_fd=True)
3125        directory = self.confirmdir(target_directory, check_exe_perm=False)
3126        directory_contents = list(directory.entries.keys())
3127        if self.shuffle_listdir_results:
3128            random.shuffle(directory_contents)
3129        return directory_contents  # type: ignore[return-value]
3130
3131    def __str__(self) -> str:
3132        return str(self.root_dir)
3133
3134    if sys.version_info >= (3, 13):
3135        # used for emulating Windows
3136        _WIN_RESERVED_NAMES = frozenset(
3137            {"CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$"}
3138            | {f"COM{c}" for c in "123456789\xb9\xb2\xb3"}
3139            | {f"LPT{c}" for c in "123456789\xb9\xb2\xb3"}
3140        )
3141        _WIN_RESERVED_CHARS = frozenset(
3142            {chr(i) for i in range(32)} | {'"', "*", ":", "<", ">", "?", "|", "/", "\\"}
3143        )
3144
3145        def isreserved(self, path):
3146            if not self.is_windows_fs:
3147                return False
3148
3149            def is_reserved_name(name):
3150                if sys.platform == "win32":
3151                    from os.path import _isreservedname  # type: ignore[import-error]
3152
3153                    return _isreservedname(name)
3154
3155                if name[-1:] in (".", " "):
3156                    return name not in (".", "..")
3157                if self._WIN_RESERVED_CHARS.intersection(name):
3158                    return True
3159                name = name.partition(".")[0].rstrip(" ").upper()
3160                return name in self._WIN_RESERVED_NAMES
3161
3162            path = os.fsdecode(self.splitroot(path)[2])
3163            if self.alternative_path_separator is not None:
3164                path = path.replace(
3165                    self.alternative_path_separator, self.path_separator
3166                )
3167
3168            return any(
3169                is_reserved_name(name)
3170                for name in reversed(path.split(self.path_separator))
3171            )
3172
3173    def _add_standard_streams(self) -> None:
3174        self.add_open_file(StandardStreamWrapper(sys.stdin))
3175        self.add_open_file(StandardStreamWrapper(sys.stdout))
3176        self.add_open_file(StandardStreamWrapper(sys.stderr))
3177
3178    def _tempdir_name(self):
3179        """This logic is extracted from tempdir._candidate_tempdir_list.
3180        We cannot rely on tempdir.gettempdir() in an empty filesystem, as it tries
3181        to write to the filesystem to ensure that the tempdir is valid.
3182        """
3183        # reset the cached tempdir in tempfile
3184        tempfile.tempdir = None
3185        for env_name in "TMPDIR", "TEMP", "TMP":
3186            dir_name = os.getenv(env_name)
3187            if dir_name:
3188                return dir_name
3189        # we have to check the real OS temp path here, as this is what
3190        # tempfile assumes
3191        if os.name == "nt":
3192            return os.path.expanduser(r"~\AppData\Local\Temp")
3193        return "/tmp"
3194
3195    def _create_temp_dir(self):
3196        # the temp directory is assumed to exist at least in `tempfile`,
3197        # so we create it here for convenience
3198        temp_dir = self._tempdir_name()
3199        if not self.exists(temp_dir):
3200            self.create_dir(temp_dir)
3201        if sys.platform != "win32" and not self.exists("/tmp"):
3202            # under Posix, we also create a link in /tmp if the path does not exist
3203            self.create_symlink("/tmp", temp_dir)
3204            # reset the used size to 0 to avoid having the link size counted
3205            # which would make disk size tests more complicated
3206            next(iter(self.mount_points.values()))["used_size"] = 0
3207
3208
3209def _run_doctest() -> TestResults:
3210    import doctest
3211    import pyfakefs
3212
3213    return doctest.testmod(pyfakefs.fake_filesystem)
3214
3215
3216def __getattr__(name):
3217    # backwards compatibility for read access to globals moved to helpers
3218    if name == "USER_ID":
3219        return helpers.USER_ID
3220    if name == "GROUP_ID":
3221        return helpers.GROUP_ID
3222    raise AttributeError(f"No attribute {name!r}.")
3223
3224
3225if __name__ == "__main__":
3226    _run_doctest()
3227