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