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 fnmatch 32import os 33import re 34 35try: 36 from urllib.parse import quote_from_bytes as urlquote_from_bytes 37except ImportError: 38 from urllib import quote as urlquote_from_bytes 39 40import sys 41 42import functools 43 44import errno 45 46from pyfakefs import fake_scandir 47from pyfakefs.extra_packages import use_scandir, pathlib, pathlib2 48from pyfakefs.fake_filesystem import FakeFileOpen, FakeFilesystem 49 50 51def init_module(filesystem): 52 """Initializes the fake module with the fake file system.""" 53 # pylint: disable=protected-access 54 FakePath.filesystem = filesystem 55 FakePathlibModule.PureWindowsPath._flavour = _FakeWindowsFlavour( 56 filesystem) 57 FakePathlibModule.PurePosixPath._flavour = _FakePosixFlavour(filesystem) 58 59 60def _wrap_strfunc(strfunc): 61 @functools.wraps(strfunc) 62 def _wrapped(pathobj, *args): 63 return strfunc(pathobj.filesystem, str(pathobj), *args) 64 65 return staticmethod(_wrapped) 66 67 68def _wrap_binary_strfunc(strfunc): 69 @functools.wraps(strfunc) 70 def _wrapped(pathobj1, pathobj2, *args): 71 return strfunc( 72 pathobj1.filesystem, str(pathobj1), str(pathobj2), *args) 73 74 return staticmethod(_wrapped) 75 76 77def _wrap_binary_strfunc_reverse(strfunc): 78 @functools.wraps(strfunc) 79 def _wrapped(pathobj1, pathobj2, *args): 80 return strfunc( 81 pathobj2.filesystem, str(pathobj2), str(pathobj1), *args) 82 83 return staticmethod(_wrapped) 84 85 86try: 87 accessor = pathlib._Accessor 88except AttributeError: 89 accessor = object 90 91 92class _FakeAccessor(accessor): 93 """Accessor which forwards some of the functions to FakeFilesystem methods. 94 """ 95 96 stat = _wrap_strfunc(FakeFilesystem.stat) 97 98 lstat = _wrap_strfunc( 99 lambda fs, path: FakeFilesystem.stat(fs, path, follow_symlinks=False)) 100 101 listdir = _wrap_strfunc(FakeFilesystem.listdir) 102 103 chmod = _wrap_strfunc(FakeFilesystem.chmod) 104 105 if use_scandir: 106 scandir = _wrap_strfunc(fake_scandir.scandir) 107 108 if hasattr(os, "lchmod"): 109 lchmod = _wrap_strfunc(lambda fs, path, mode: FakeFilesystem.chmod( 110 fs, path, mode, follow_symlinks=False)) 111 else: 112 def lchmod(self, pathobj, mode): 113 """Raises not implemented for Windows systems.""" 114 raise NotImplementedError("lchmod() not available on this system") 115 116 mkdir = _wrap_strfunc(FakeFilesystem.makedir) 117 118 unlink = _wrap_strfunc(FakeFilesystem.remove) 119 120 rmdir = _wrap_strfunc(FakeFilesystem.rmdir) 121 122 rename = _wrap_binary_strfunc(FakeFilesystem.rename) 123 124 replace = _wrap_binary_strfunc( 125 lambda fs, old_path, new_path: FakeFilesystem.rename( 126 fs, old_path, new_path, force_replace=True)) 127 128 symlink = _wrap_binary_strfunc_reverse( 129 lambda fs, file_path, link_target, target_is_directory: 130 FakeFilesystem.create_symlink(fs, file_path, link_target, 131 create_missing_dirs=False)) 132 133 utime = _wrap_strfunc(FakeFilesystem.utime) 134 135 136_fake_accessor = _FakeAccessor() 137 138flavour = pathlib._Flavour if pathlib else object 139 140 141class _FakeFlavour(flavour): 142 """Fake Flavour implementation used by PurePath and _Flavour""" 143 144 filesystem = None 145 sep = '/' 146 altsep = None 147 has_drv = False 148 149 ext_namespace_prefix = '\\\\?\\' 150 151 drive_letters = ( 152 set(chr(x) for x in range(ord('a'), ord('z') + 1)) | 153 set(chr(x) for x in range(ord('A'), ord('Z') + 1)) 154 ) 155 156 def __init__(self, filesystem): 157 self.filesystem = filesystem 158 self.sep = filesystem.path_separator 159 self.altsep = filesystem.alternative_path_separator 160 self.has_drv = filesystem.is_windows_fs 161 super(_FakeFlavour, self).__init__() 162 163 @staticmethod 164 def _split_extended_path(path, ext_prefix=ext_namespace_prefix): 165 prefix = '' 166 if path.startswith(ext_prefix): 167 prefix = path[:4] 168 path = path[4:] 169 if path.startswith('UNC\\'): 170 prefix += path[:3] 171 path = '\\' + path[3:] 172 return prefix, path 173 174 def _splitroot_with_drive(self, path, sep): 175 first = path[0:1] 176 second = path[1:2] 177 if second == sep and first == sep: 178 # extended paths should also disable the collapsing of "." 179 # components (according to MSDN docs). 180 prefix, path = self._split_extended_path(path) 181 first = path[0:1] 182 second = path[1:2] 183 else: 184 prefix = '' 185 third = path[2:3] 186 if second == sep and first == sep and third != sep: 187 # is a UNC path: 188 # vvvvvvvvvvvvvvvvvvvvv root 189 # \\machine\mountpoint\directory\etc\... 190 # directory ^^^^^^^^^^^^^^ 191 index = path.find(sep, 2) 192 if index != -1: 193 index2 = path.find(sep, index + 1) 194 # a UNC path can't have two slashes in a row 195 # (after the initial two) 196 if index2 != index + 1: 197 if index2 == -1: 198 index2 = len(path) 199 if prefix: 200 return prefix + path[1:index2], sep, path[index2 + 1:] 201 return path[:index2], sep, path[index2 + 1:] 202 drv = root = '' 203 if second == ':' and first in self.drive_letters: 204 drv = path[:2] 205 path = path[2:] 206 first = third 207 if first == sep: 208 root = first 209 path = path.lstrip(sep) 210 return prefix + drv, root, path 211 212 @staticmethod 213 def _splitroot_posix(path, sep): 214 if path and path[0] == sep: 215 stripped_part = path.lstrip(sep) 216 if len(path) - len(stripped_part) == 2: 217 return '', sep * 2, stripped_part 218 return '', sep, stripped_part 219 else: 220 return '', '', path 221 222 def splitroot(self, path, sep=None): 223 """Split path into drive, root and rest.""" 224 if sep is None: 225 sep = self.filesystem.path_separator 226 if self.filesystem.is_windows_fs: 227 return self._splitroot_with_drive(path, sep) 228 return self._splitroot_posix(path, sep) 229 230 def casefold(self, path): 231 """Return the lower-case version of s for a Windows filesystem.""" 232 if self.filesystem.is_windows_fs: 233 return path.lower() 234 return path 235 236 def casefold_parts(self, parts): 237 """Return the lower-case version of parts for a Windows filesystem.""" 238 if self.filesystem.is_windows_fs: 239 return [p.lower() for p in parts] 240 return parts 241 242 def _resolve_posix(self, path, strict): 243 sep = self.sep 244 seen = {} 245 246 def _resolve(path, rest): 247 if rest.startswith(sep): 248 path = '' 249 250 for name in rest.split(sep): 251 if not name or name == '.': 252 # current dir 253 continue 254 if name == '..': 255 # parent dir 256 path, _, _ = path.rpartition(sep) 257 continue 258 newpath = path + sep + name 259 if newpath in seen: 260 # Already seen this path 261 path = seen[newpath] 262 if path is not None: 263 # use cached value 264 continue 265 # The symlink is not resolved, so we must have 266 # a symlink loop. 267 raise RuntimeError("Symlink loop from %r" % newpath) 268 # Resolve the symbolic link 269 try: 270 target = self.filesystem.readlink(newpath) 271 except OSError as e: 272 if e.errno != errno.EINVAL and strict: 273 raise 274 # Not a symlink, or non-strict mode. We just leave the path 275 # untouched. 276 path = newpath 277 else: 278 seen[newpath] = None # not resolved symlink 279 path = _resolve(path, target) 280 seen[newpath] = path # resolved symlink 281 282 return path 283 284 # NOTE: according to POSIX, getcwd() cannot contain path components 285 # which are symlinks. 286 base = '' if path.is_absolute() else self.filesystem.cwd 287 return _resolve(base, str(path)) or sep 288 289 def _resolve_windows(self, path, strict): 290 path = str(path) 291 if not path: 292 return os.getcwd() 293 previous_s = None 294 if strict: 295 if not self.filesystem.exists(path): 296 self.filesystem.raise_os_error(errno.ENOENT, path) 297 return self.filesystem.resolve_path(path) 298 else: 299 while True: 300 try: 301 path = self.filesystem.resolve_path(path) 302 except OSError: 303 previous_s = path 304 path = self.filesystem.splitpath(path)[0] 305 else: 306 if previous_s is None: 307 return path 308 return self.filesystem.joinpaths( 309 path, os.path.basename(previous_s)) 310 311 def resolve(self, path, strict): 312 """Make the path absolute, resolving any symlinks.""" 313 if self.filesystem.is_windows_fs: 314 return self._resolve_windows(path, strict) 315 return self._resolve_posix(path, strict) 316 317 def gethomedir(self, username): 318 """Return the home directory of the current user.""" 319 if not username: 320 try: 321 return os.environ['HOME'] 322 except KeyError: 323 import pwd 324 return pwd.getpwuid(os.getuid()).pw_dir 325 else: 326 import pwd 327 try: 328 return pwd.getpwnam(username).pw_dir 329 except KeyError: 330 raise RuntimeError("Can't determine home directory " 331 "for %r" % username) 332 333 334class _FakeWindowsFlavour(_FakeFlavour): 335 """Flavour used by PureWindowsPath with some Windows specific 336 implementations independent of FakeFilesystem properties. 337 """ 338 reserved_names = ( 339 {'CON', 'PRN', 'AUX', 'NUL'} | 340 {'COM%d' % i for i in range(1, 10)} | 341 {'LPT%d' % i for i in range(1, 10)} 342 ) 343 344 def is_reserved(self, parts): 345 """Return True if the path is considered reserved under Windows.""" 346 347 # NOTE: the rules for reserved names seem somewhat complicated 348 # (e.g. r"..\NUL" is reserved but not r"foo\NUL"). 349 # We err on the side of caution and return True for paths which are 350 # not considered reserved by Windows. 351 if not parts: 352 return False 353 if self.filesystem.is_windows_fs and parts[0].startswith('\\\\'): 354 # UNC paths are never reserved 355 return False 356 return parts[-1].partition('.')[0].upper() in self.reserved_names 357 358 def make_uri(self, path): 359 """Return a file URI for the given path""" 360 361 # Under Windows, file URIs use the UTF-8 encoding. 362 # original version, not faked 363 drive = path.drive 364 if len(drive) == 2 and drive[1] == ':': 365 # It's a path on a local drive => 'file:///c:/a/b' 366 rest = path.as_posix()[2:].lstrip('/') 367 return 'file:///%s/%s' % ( 368 drive, urlquote_from_bytes(rest.encode('utf-8'))) 369 else: 370 # It's a path on a network drive => 'file://host/share/a/b' 371 return ('file:' + 372 urlquote_from_bytes(path.as_posix().encode('utf-8'))) 373 374 def gethomedir(self, username): 375 """Return the home directory of the current user.""" 376 377 # original version, not faked 378 if 'HOME' in os.environ: 379 userhome = os.environ['HOME'] 380 elif 'USERPROFILE' in os.environ: 381 userhome = os.environ['USERPROFILE'] 382 elif 'HOMEPATH' in os.environ: 383 try: 384 drv = os.environ['HOMEDRIVE'] 385 except KeyError: 386 drv = '' 387 userhome = drv + os.environ['HOMEPATH'] 388 else: 389 raise RuntimeError("Can't determine home directory") 390 391 if username: 392 # Try to guess user home directory. By default all users 393 # directories are located in the same place and are named by 394 # corresponding usernames. If current user home directory points 395 # to nonstandard place, this guess is likely wrong. 396 if os.environ['USERNAME'] != username: 397 drv, root, parts = self.parse_parts((userhome,)) 398 if parts[-1] != os.environ['USERNAME']: 399 raise RuntimeError("Can't determine home directory " 400 "for %r" % username) 401 parts[-1] = username 402 if drv or root: 403 userhome = drv + root + self.join(parts[1:]) 404 else: 405 userhome = self.join(parts) 406 return userhome 407 408 def compile_pattern(self, pattern): 409 return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch 410 411 412class _FakePosixFlavour(_FakeFlavour): 413 """Flavour used by PurePosixPath with some Unix specific implementations 414 independent of FakeFilesystem properties. 415 """ 416 417 def is_reserved(self, parts): 418 return False 419 420 def make_uri(self, path): 421 # We represent the path using the local filesystem encoding, 422 # for portability to other applications. 423 bpath = bytes(path) 424 return 'file://' + urlquote_from_bytes(bpath) 425 426 def gethomedir(self, username): 427 # original version, not faked 428 if not username: 429 try: 430 return os.environ['HOME'] 431 except KeyError: 432 import pwd 433 return pwd.getpwuid(os.getuid()).pw_dir 434 else: 435 import pwd 436 try: 437 return pwd.getpwnam(username).pw_dir 438 except KeyError: 439 raise RuntimeError("Can't determine home directory " 440 "for %r" % username) 441 442 def compile_pattern(self, pattern): 443 return re.compile(fnmatch.translate(pattern)).fullmatch 444 445 446path_module = pathlib.Path if pathlib else object 447 448 449class FakePath(path_module): 450 """Replacement for pathlib.Path. Reimplement some methods to use 451 fake filesystem. The rest of the methods work as they are, as they will 452 use the fake accessor. 453 New in pyfakefs 3.0. 454 """ 455 456 # the underlying fake filesystem 457 filesystem = None 458 459 def __new__(cls, *args, **kwargs): 460 """Creates the correct subclass based on OS.""" 461 if cls is FakePathlibModule.Path: 462 cls = (FakePathlibModule.WindowsPath if os.name == 'nt' 463 else FakePathlibModule.PosixPath) 464 self = cls._from_parts(args, init=True) 465 return self 466 467 def _path(self): 468 """Returns the underlying path string as used by the fake filesystem. 469 """ 470 return str(self) 471 472 def _init(self, template=None): 473 """Initializer called from base class.""" 474 self._accessor = _fake_accessor 475 self._closed = False 476 477 @classmethod 478 def cwd(cls): 479 """Return a new path pointing to the current working directory 480 (as returned by os.getcwd()). 481 """ 482 return cls(cls.filesystem.cwd) 483 484 def resolve(self, strict=None): 485 """Make the path absolute, resolving all symlinks on the way and also 486 normalizing it (for example turning slashes into backslashes 487 under Windows). 488 489 Args: 490 strict: If False (default) no exception is raised if the path 491 does not exist. 492 New in Python 3.6. 493 494 Raises: 495 OSError: if the path doesn't exist (strict=True or Python < 3.6) 496 """ 497 if sys.version_info >= (3, 6) or pathlib2: 498 if strict is None: 499 strict = False 500 else: 501 if strict is not None: 502 raise TypeError( 503 "resolve() got an unexpected keyword argument 'strict'") 504 strict = True 505 if self._closed: 506 self._raise_closed() 507 path = self._flavour.resolve(self, strict=strict) 508 if path is None: 509 self.stat() 510 path = str(self.absolute()) 511 path = self.filesystem.absnormpath(path) 512 return FakePath(path) 513 514 def open(self, mode='r', buffering=-1, encoding=None, 515 errors=None, newline=None): 516 """Open the file pointed by this path and return a fake file object. 517 518 Raises: 519 OSError: if the target object is a directory, the path is invalid 520 or permission is denied. 521 """ 522 if self._closed: 523 self._raise_closed() 524 return FakeFileOpen(self.filesystem)( 525 self._path(), mode, buffering, encoding, errors, newline) 526 527 def read_bytes(self): 528 """Open the fake file in bytes mode, read it, and close the file. 529 530 Raises: 531 OSError: if the target object is a directory, the path is 532 invalid or permission is denied. 533 """ 534 with FakeFileOpen(self.filesystem)(self._path(), mode='rb') as f: 535 return f.read() 536 537 def read_text(self, encoding=None, errors=None): 538 """ 539 Open the fake file in text mode, read it, and close the file. 540 """ 541 with FakeFileOpen(self.filesystem)(self._path(), mode='r', 542 encoding=encoding, 543 errors=errors) as f: 544 return f.read() 545 546 def write_bytes(self, data): 547 """Open the fake file in bytes mode, write to it, and close the file. 548 Args: 549 data: the bytes to be written 550 Raises: 551 OSError: if the target object is a directory, the path is 552 invalid or permission is denied. 553 """ 554 # type-check for the buffer interface before truncating the file 555 view = memoryview(data) 556 with FakeFileOpen(self.filesystem)(self._path(), mode='wb') as f: 557 return f.write(view) 558 559 def write_text(self, data, encoding=None, errors=None): 560 """Open the fake file in text mode, write to it, and close 561 the file. 562 563 Args: 564 data: the string to be written 565 encoding: the encoding used for the string; if not given, the 566 default locale encoding is used 567 errors: ignored 568 Raises: 569 TypeError: if data is not of type 'str'. 570 OSError: if the target object is a directory, the path is 571 invalid or permission is denied. 572 """ 573 if not isinstance(data, str): 574 raise TypeError('data must be str, not %s' % 575 data.__class__.__name__) 576 with FakeFileOpen(self.filesystem)(self._path(), 577 mode='w', 578 encoding=encoding, 579 errors=errors) as f: 580 return f.write(data) 581 582 @classmethod 583 def home(cls): 584 """Return a new path pointing to the user's home directory (as 585 returned by os.path.expanduser('~')). 586 """ 587 return cls(cls()._flavour.gethomedir(None). 588 replace(os.sep, cls.filesystem.path_separator)) 589 590 def samefile(self, other_path): 591 """Return whether other_path is the same or not as this file 592 (as returned by os.path.samefile()). 593 594 Args: 595 other_path: A path object or string of the file object 596 to be compared with 597 598 Raises: 599 OSError: if the filesystem object doesn't exist. 600 """ 601 st = self.stat() 602 try: 603 other_st = other_path.stat() 604 except AttributeError: 605 other_st = self.filesystem.stat(other_path) 606 return (st.st_ino == other_st.st_ino and 607 st.st_dev == other_st.st_dev) 608 609 def expanduser(self): 610 """ Return a new path with expanded ~ and ~user constructs 611 (as returned by os.path.expanduser) 612 """ 613 return FakePath(os.path.expanduser(self._path()) 614 .replace(os.path.sep, 615 self.filesystem.path_separator)) 616 617 def touch(self, mode=0o666, exist_ok=True): 618 """Create a fake file for the path with the given access mode, 619 if it doesn't exist. 620 621 Args: 622 mode: the file mode for the file if it does not exist 623 exist_ok: if the file already exists and this is True, nothing 624 happens, otherwise FileExistError is raised 625 626 Raises: 627 FileExistsError: if the file exists and exits_ok is False. 628 """ 629 if self._closed: 630 self._raise_closed() 631 if self.exists(): 632 if exist_ok: 633 self.filesystem.utime(self._path(), times=None) 634 else: 635 self.filesystem.raise_os_error(errno.EEXIST, self._path()) 636 else: 637 fake_file = self.open('w') 638 fake_file.close() 639 self.chmod(mode) 640 641 642class FakePathlibModule: 643 """Uses FakeFilesystem to provide a fake pathlib module replacement. 644 Can be used to replace both the standard `pathlib` module and the 645 `pathlib2` package available on PyPi. 646 647 You need a fake_filesystem to use this: 648 `filesystem = fake_filesystem.FakeFilesystem()` 649 `fake_pathlib_module = fake_filesystem.FakePathlibModule(filesystem)` 650 """ 651 652 PurePath = pathlib.PurePath if pathlib else object 653 654 def __init__(self, filesystem): 655 """ 656 Initializes the module with the given filesystem. 657 658 Args: 659 filesystem: FakeFilesystem used to provide file system information 660 """ 661 init_module(filesystem) 662 self._pathlib_module = pathlib 663 664 class PurePosixPath(PurePath): 665 """A subclass of PurePath, that represents non-Windows filesystem 666 paths""" 667 __slots__ = () 668 669 class PureWindowsPath(PurePath): 670 """A subclass of PurePath, that represents Windows filesystem paths""" 671 __slots__ = () 672 673 if sys.platform == 'win32': 674 class WindowsPath(FakePath, PureWindowsPath): 675 """A subclass of Path and PureWindowsPath that represents 676 concrete Windows filesystem paths. 677 """ 678 __slots__ = () 679 else: 680 class PosixPath(FakePath, PurePosixPath): 681 """A subclass of Path and PurePosixPath that represents 682 concrete non-Windows filesystem paths. 683 """ 684 __slots__ = () 685 686 Path = FakePath 687 688 def __getattr__(self, name): 689 """Forwards any unfaked calls to the standard pathlib module.""" 690 return getattr(self._pathlib_module, name) 691 692 693class FakePathlibPathModule: 694 """Patches `pathlib.Path` by passing all calls to FakePathlibModule.""" 695 fake_pathlib = None 696 697 def __init__(self, filesystem): 698 if self.fake_pathlib is None: 699 self.__class__.fake_pathlib = FakePathlibModule(filesystem) 700 701 def __call__(self, *args, **kwargs): 702 return self.fake_pathlib.Path(*args, **kwargs) 703 704 def __getattr__(self, name): 705 return getattr(self.fake_pathlib.Path, name) 706