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