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