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