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