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"""Faked ``os.path`` module replacement. See ``fake_filesystem`` for usage.""" 16 17import errno 18import functools 19import inspect 20import os 21import sys 22from stat import ( 23 S_IFDIR, 24 S_IFMT, 25) 26from types import ModuleType 27from typing import ( 28 Callable, 29 List, 30 Optional, 31 Union, 32 Any, 33 Dict, 34 Tuple, 35 AnyStr, 36 overload, 37 ClassVar, 38 TYPE_CHECKING, 39) 40 41from pyfakefs.helpers import ( 42 is_called_from_skipped_module, 43 make_string_path, 44 to_string, 45 matching_string, 46 to_bytes, 47) 48 49if TYPE_CHECKING: 50 from pyfakefs.fake_filesystem import FakeFilesystem 51 from pyfakefs.fake_os import FakeOsModule 52 53 54def _copy_module(old: ModuleType) -> ModuleType: 55 """Recompiles and creates new module object.""" 56 saved = sys.modules.pop(old.__name__, None) 57 new = __import__(old.__name__) 58 if saved is not None: 59 sys.modules[old.__name__] = saved 60 return new 61 62 63class FakePathModule: 64 """Faked os.path module replacement. 65 66 FakePathModule should *only* be instantiated by FakeOsModule. See the 67 FakeOsModule docstring for details. 68 """ 69 70 _OS_PATH_COPY: Any = _copy_module(os.path) 71 72 devnull: ClassVar[str] = "" 73 sep: ClassVar[str] = "" 74 altsep: ClassVar[Optional[str]] = None 75 linesep: ClassVar[str] = "" 76 pathsep: ClassVar[str] = "" 77 78 @staticmethod 79 def dir() -> List[str]: 80 """Return the list of patched function names. Used for patching 81 functions imported from the module. 82 """ 83 dir_list = [ 84 "abspath", 85 "dirname", 86 "exists", 87 "expanduser", 88 "getatime", 89 "getctime", 90 "getmtime", 91 "getsize", 92 "isabs", 93 "isdir", 94 "isfile", 95 "islink", 96 "ismount", 97 "join", 98 "lexists", 99 "normcase", 100 "normpath", 101 "realpath", 102 "relpath", 103 "split", 104 "splitdrive", 105 "samefile", 106 ] 107 if sys.version_info >= (3, 12): 108 dir_list += ["isjunction", "splitroot"] 109 return dir_list 110 111 def __init__(self, filesystem: "FakeFilesystem", os_module: "FakeOsModule"): 112 """Init. 113 114 Args: 115 filesystem: FakeFilesystem used to provide file system information 116 """ 117 self.filesystem = filesystem 118 self._os_path = self._OS_PATH_COPY 119 self._os_path.os = self.os = os_module # type: ignore[attr-defined] 120 self.reset(filesystem) 121 122 @classmethod 123 def reset(cls, filesystem: "FakeFilesystem") -> None: 124 cls.sep = filesystem.path_separator 125 cls.altsep = filesystem.alternative_path_separator 126 cls.linesep = filesystem.line_separator 127 cls.devnull = filesystem.devnull 128 cls.pathsep = filesystem.pathsep 129 130 def exists(self, path: AnyStr) -> bool: 131 """Determine whether the file object exists within the fake filesystem. 132 133 Args: 134 path: The path to the file object. 135 136 Returns: 137 (bool) `True` if the file exists. 138 """ 139 return self.filesystem.exists(path) 140 141 def lexists(self, path: AnyStr) -> bool: 142 """Test whether a path exists. Returns True for broken symbolic links. 143 144 Args: 145 path: path to the symlink object. 146 147 Returns: 148 bool (if file exists). 149 """ 150 return self.filesystem.exists(path, check_link=True) 151 152 def getsize(self, path: AnyStr): 153 """Return the file object size in bytes. 154 155 Args: 156 path: path to the file object. 157 158 Returns: 159 file size in bytes. 160 """ 161 file_obj = self.filesystem.resolve(path) 162 if ( 163 self.filesystem.ends_with_path_separator(path) 164 and S_IFMT(file_obj.st_mode) != S_IFDIR 165 ): 166 error_nr = errno.EINVAL if self.filesystem.is_windows_fs else errno.ENOTDIR 167 self.filesystem.raise_os_error(error_nr, path) 168 return file_obj.st_size 169 170 def isabs(self, path: AnyStr) -> bool: 171 """Return True if path is an absolute pathname.""" 172 empty = matching_string(path, "") 173 if self.filesystem.is_windows_fs: 174 drive, path = self.splitdrive(path) 175 else: 176 drive = empty 177 path = make_string_path(path) 178 if not self.filesystem.starts_with_sep(path): 179 return False 180 if self.filesystem.is_windows_fs and sys.version_info >= (3, 13): 181 # from Python 3.13 on, a path under Windows starting with a single separator 182 # (e.g. not a drive and not an UNC path) is no more considered absolute 183 return drive != empty 184 return True 185 186 def isdir(self, path: AnyStr) -> bool: 187 """Determine if path identifies a directory.""" 188 return self.filesystem.isdir(path) 189 190 def isfile(self, path: AnyStr) -> bool: 191 """Determine if path identifies a regular file.""" 192 return self.filesystem.isfile(path) 193 194 def islink(self, path: AnyStr) -> bool: 195 """Determine if path identifies a symbolic link. 196 197 Args: 198 path: Path to filesystem object. 199 200 Returns: 201 `True` if path points to a symbolic link. 202 203 Raises: 204 TypeError: if path is None. 205 """ 206 return self.filesystem.islink(path) 207 208 if sys.version_info >= (3, 12): 209 210 def isjunction(self, path: AnyStr) -> bool: 211 """Returns False. Junctions are never faked.""" 212 return self.filesystem.isjunction(path) 213 214 def splitroot(self, path: AnyStr): 215 """Split a pathname into drive, root and tail. 216 Implementation taken from ntpath and posixpath. 217 """ 218 return self.filesystem.splitroot(path) 219 220 if sys.version_info >= (3, 13): 221 222 def isreserved(self, path): 223 if not self.filesystem.is_windows_fs: 224 raise AttributeError("module 'os' has no attribute 'isreserved'") 225 226 return self.filesystem.isreserved(path) 227 228 def getmtime(self, path: AnyStr) -> float: 229 """Returns the modification time of the fake file. 230 231 Args: 232 path: the path to fake file. 233 234 Returns: 235 (int, float) the modification time of the fake file 236 in number of seconds since the epoch. 237 238 Raises: 239 OSError: if the file does not exist. 240 """ 241 try: 242 file_obj = self.filesystem.resolve(path) 243 return file_obj.st_mtime 244 except OSError: 245 self.filesystem.raise_os_error( 246 errno.ENOENT, winerror=3 247 ) # pytype: disable=bad-return-type 248 249 def getatime(self, path: AnyStr) -> float: 250 """Returns the last access time of the fake file. 251 252 Note: Access time is not set automatically in fake filesystem 253 on access. 254 255 Args: 256 path: the path to fake file. 257 258 Returns: 259 (int, float) the access time of the fake file in number of seconds 260 since the epoch. 261 262 Raises: 263 OSError: if the file does not exist. 264 """ 265 try: 266 file_obj = self.filesystem.resolve(path) 267 except OSError: 268 self.filesystem.raise_os_error(errno.ENOENT) 269 return file_obj.st_atime # pytype: disable=name-error 270 271 def getctime(self, path: AnyStr) -> float: 272 """Returns the creation time of the fake file. 273 274 Args: 275 path: the path to fake file. 276 277 Returns: 278 (int, float) the creation time of the fake file in number of 279 seconds since the epoch. 280 281 Raises: 282 OSError: if the file does not exist. 283 """ 284 try: 285 file_obj = self.filesystem.resolve(path) 286 except OSError: 287 self.filesystem.raise_os_error(errno.ENOENT) 288 return file_obj.st_ctime # pytype: disable=name-error 289 290 def abspath(self, path: AnyStr) -> AnyStr: 291 """Return the absolute version of a path.""" 292 293 def getcwd(): 294 """Return the current working directory.""" 295 # pylint: disable=undefined-variable 296 if isinstance(path, bytes): 297 return self.os.getcwdb() 298 else: 299 return self.os.getcwd() 300 301 path = make_string_path(path) 302 if not self.isabs(path): 303 path = self.join(getcwd(), path) 304 elif self.filesystem.is_windows_fs and self.filesystem.starts_with_sep(path): 305 cwd = getcwd() 306 if self.filesystem.starts_with_drive_letter(cwd): 307 path = self.join(cwd[:2], path) 308 return self.normpath(path) 309 310 def join(self, *p: AnyStr) -> AnyStr: 311 """Return the completed path with a separator of the parts.""" 312 return self.filesystem.joinpaths(*p) 313 314 def split(self, path: AnyStr) -> Tuple[AnyStr, AnyStr]: 315 """Split the path into the directory and the filename of the path.""" 316 return self.filesystem.splitpath(path) 317 318 def splitdrive(self, path: AnyStr) -> Tuple[AnyStr, AnyStr]: 319 """Split the path into the drive part and the rest of the path, if 320 supported.""" 321 return self.filesystem.splitdrive(path) 322 323 def normpath(self, path: AnyStr) -> AnyStr: 324 """Normalize path, eliminating double slashes, etc.""" 325 return self.filesystem.normpath(path) 326 327 def normcase(self, path: AnyStr) -> AnyStr: 328 """Convert to lower case under windows, replaces additional path 329 separator.""" 330 path = self.filesystem.normcase(path) 331 if self.filesystem.is_windows_fs: 332 path = path.lower() 333 return path 334 335 def relpath(self, path: AnyStr, start: Optional[AnyStr] = None) -> AnyStr: 336 """We mostly rely on the native implementation and adapt the 337 path separator.""" 338 if not path: 339 raise ValueError("no path specified") 340 path = make_string_path(path) 341 path = self.filesystem.replace_windows_root(path) 342 sep = matching_string(path, self.filesystem.path_separator) 343 if start is not None: 344 start = make_string_path(start) 345 else: 346 start = matching_string(path, self.filesystem.cwd) 347 start = self.filesystem.replace_windows_root(start) 348 system_sep = matching_string(path, self._os_path.sep) 349 if self.filesystem.alternative_path_separator is not None: 350 altsep = matching_string(path, self.filesystem.alternative_path_separator) 351 path = path.replace(altsep, system_sep) 352 start = start.replace(altsep, system_sep) 353 path = path.replace(sep, system_sep) 354 start = start.replace(sep, system_sep) 355 path = self._os_path.relpath(path, start) 356 return path.replace(system_sep, sep) 357 358 def realpath(self, filename: AnyStr, strict: Optional[bool] = None) -> AnyStr: 359 """Return the canonical path of the specified filename, eliminating any 360 symbolic links encountered in the path. 361 """ 362 if strict is not None and sys.version_info < (3, 10): 363 raise TypeError("realpath() got an unexpected keyword argument 'strict'") 364 if strict: 365 # raises in strict mode if the file does not exist 366 self.filesystem.resolve(filename) 367 if self.filesystem.is_windows_fs: 368 return self.abspath(filename) 369 filename = make_string_path(filename) 370 path, ok = self._join_real_path(filename[:0], filename, {}) 371 path = self.abspath(path) 372 return path 373 374 def samefile(self, path1: AnyStr, path2: AnyStr) -> bool: 375 """Return whether path1 and path2 point to the same file. 376 377 Args: 378 path1: first file path or path object 379 path2: second file path or path object 380 381 Raises: 382 OSError: if one of the paths does not point to an existing 383 file system object. 384 """ 385 stat1 = self.filesystem.stat(path1) 386 stat2 = self.filesystem.stat(path2) 387 return stat1.st_ino == stat2.st_ino and stat1.st_dev == stat2.st_dev 388 389 @overload 390 def _join_real_path( 391 self, path: str, rest: str, seen: Dict[str, Optional[str]] 392 ) -> Tuple[str, bool]: ... 393 394 @overload 395 def _join_real_path( 396 self, path: bytes, rest: bytes, seen: Dict[bytes, Optional[bytes]] 397 ) -> Tuple[bytes, bool]: ... 398 399 def _join_real_path( 400 self, path: AnyStr, rest: AnyStr, seen: Dict[AnyStr, Optional[AnyStr]] 401 ) -> Tuple[AnyStr, bool]: 402 """Join two paths, normalizing and eliminating any symbolic links 403 encountered in the second path. 404 Taken from Python source and adapted. 405 """ 406 curdir = matching_string(path, ".") 407 pardir = matching_string(path, "..") 408 409 sep = self.filesystem.get_path_separator(path) 410 if self.isabs(rest): 411 rest = rest[1:] 412 path = sep 413 414 while rest: 415 name, _, rest = rest.partition(sep) 416 if not name or name == curdir: 417 # current dir 418 continue 419 if name == pardir: 420 # parent dir 421 if path: 422 path, name = self.filesystem.splitpath(path) 423 if name == pardir: 424 path = self.filesystem.joinpaths(path, pardir, pardir) 425 else: 426 path = pardir 427 continue 428 newpath = self.filesystem.joinpaths(path, name) 429 if not self.filesystem.islink(newpath): 430 path = newpath 431 continue 432 # Resolve the symbolic link 433 if newpath in seen: 434 # Already seen this path 435 seen_path = seen[newpath] 436 if seen_path is not None: 437 # use cached value 438 path = seen_path 439 continue 440 # The symlink is not resolved, so we must have a symlink loop. 441 # Return already resolved part + rest of the path unchanged. 442 return self.filesystem.joinpaths(newpath, rest), False 443 seen[newpath] = None # not resolved symlink 444 path, ok = self._join_real_path( 445 path, 446 matching_string(path, self.filesystem.readlink(newpath)), 447 seen, 448 ) 449 if not ok: 450 return self.filesystem.joinpaths(path, rest), False 451 seen[newpath] = path # resolved symlink 452 return path, True 453 454 def dirname(self, path: AnyStr) -> AnyStr: 455 """Returns the first part of the result of `split()`.""" 456 return self.split(path)[0] 457 458 def expanduser(self, path: AnyStr) -> AnyStr: 459 """Return the argument with an initial component of ~ or ~user 460 replaced by that user's home directory. 461 """ 462 path = self._os_path.expanduser(path) 463 return path.replace( 464 matching_string(path, self._os_path.sep), 465 matching_string(path, self.sep), 466 ) 467 468 def ismount(self, path: AnyStr) -> bool: 469 """Return true if the given path is a mount point. 470 471 Args: 472 path: Path to filesystem object to be checked 473 474 Returns: 475 `True` if path is a mount point added to the fake file system. 476 Under Windows also returns True for drive and UNC roots 477 (independent of their existence). 478 """ 479 if not path: 480 return False 481 path_str = to_string(make_string_path(path)) 482 normed_path = self.filesystem.absnormpath(path_str) 483 sep = self.filesystem.path_separator 484 if self.filesystem.is_windows_fs: 485 path_seps: Union[Tuple[str, Optional[str]], Tuple[str]] 486 if self.filesystem.alternative_path_separator is not None: 487 path_seps = (sep, self.filesystem.alternative_path_separator) 488 else: 489 path_seps = (sep,) 490 drive, rest = self.filesystem.splitdrive(normed_path) 491 if drive and drive[:1] in path_seps: 492 return (not rest) or (rest in path_seps) 493 if rest in path_seps: 494 return True 495 for mount_point in self.filesystem.mount_points: 496 if to_string(normed_path).rstrip(sep) == to_string(mount_point).rstrip(sep): 497 return True 498 return False 499 500 def __getattr__(self, name: str) -> Any: 501 """Forwards any non-faked calls to the real os.path.""" 502 return getattr(self._os_path, name) 503 504 505if sys.platform == "win32": 506 507 class FakeNtModule: 508 """Under windows, a few function of `os.path` are taken from the `nt` module 509 for performance reasons. These are patched here. 510 """ 511 512 @staticmethod 513 def dir(): 514 if sys.version_info >= (3, 12): 515 return ["_path_exists", "_path_isfile", "_path_isdir", "_path_islink"] 516 else: 517 return ["_isdir"] 518 519 def __init__(self, filesystem: "FakeFilesystem"): 520 """Init. 521 522 Args: 523 filesystem: FakeFilesystem used to provide file system information 524 """ 525 import nt # type:ignore[import] 526 527 self.filesystem = filesystem 528 self.nt_module: Any = nt 529 530 def getcwd(self) -> str: 531 """Return current working directory.""" 532 return to_string(self.filesystem.cwd) 533 534 def getcwdb(self) -> bytes: 535 """Return current working directory as bytes.""" 536 return to_bytes(self.filesystem.cwd) 537 538 if sys.version_info >= (3, 12): 539 540 def _path_isdir(self, path: AnyStr) -> bool: 541 return self.filesystem.isdir(path) 542 543 def _path_isfile(self, path: AnyStr) -> bool: 544 return self.filesystem.isfile(path) 545 546 def _path_islink(self, path: AnyStr) -> bool: 547 return self.filesystem.islink(path) 548 549 def _path_exists(self, path: AnyStr) -> bool: 550 return self.filesystem.exists(path) 551 552 else: 553 554 def _isdir(self, path: AnyStr) -> bool: 555 return self.filesystem.isdir(path) 556 557 def __getattr__(self, name: str) -> Any: 558 """Forwards any non-faked calls to the real nt module.""" 559 return getattr(self.nt_module, name) 560 561 562def handle_original_call(f: Callable) -> Callable: 563 """Decorator used for real pathlib Path methods to ensure that 564 real os functions instead of faked ones are used. 565 Applied to all non-private methods of `FakePathModule`.""" 566 567 @functools.wraps(f) 568 def wrapped(*args, **kwargs): 569 if args: 570 self = args[0] 571 should_use_original = self.os.use_original 572 if not should_use_original and self.filesystem.patcher: 573 skip_names = self.filesystem.patcher.skip_names 574 if is_called_from_skipped_module( 575 skip_names=skip_names, 576 case_sensitive=self.filesystem.is_case_sensitive, 577 ): 578 should_use_original = True 579 580 if should_use_original: 581 # remove the `self` argument for FakePathModule methods 582 if args and isinstance(args[0], FakePathModule): 583 args = args[1:] 584 return getattr(os.path, f.__name__)(*args, **kwargs) 585 586 return f(*args, **kwargs) 587 588 return wrapped 589 590 591for name, fn in inspect.getmembers(FakePathModule, inspect.isfunction): 592 if not fn.__name__.startswith("_"): 593 setattr(FakePathModule, name, handle_original_call(fn)) 594