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