diff options
Diffstat (limited to 'pyfakefs/fake_pathlib.py')
-rw-r--r-- | pyfakefs/fake_pathlib.py | 1044 |
1 files changed, 584 insertions, 460 deletions
diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index 8868558..6e096d1 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -31,25 +31,39 @@ get the properties of the underlying fake filesystem. import errno import fnmatch import functools +import inspect +import ntpath import os import pathlib -from pathlib import PurePath +import posixpath import re import sys +from pathlib import PurePath +from typing import Callable from urllib.parse import quote_from_bytes as urlquote_from_bytes from pyfakefs import fake_scandir from pyfakefs.extra_packages import use_scandir -from pyfakefs.fake_filesystem import FakeFileOpen, FakeFilesystem +from pyfakefs.fake_filesystem import FakeFilesystem +from pyfakefs.fake_open import FakeFileOpen +from pyfakefs.fake_os import FakeOsModule, use_original_os +from pyfakefs.helpers import IS_PYPY def init_module(filesystem): """Initializes the fake module with the fake file system.""" # pylint: disable=protected-access FakePath.filesystem = filesystem - FakePathlibModule.PureWindowsPath._flavour = _FakeWindowsFlavour( - filesystem) - FakePathlibModule.PurePosixPath._flavour = _FakePosixFlavour(filesystem) + if sys.version_info < (3, 12): + FakePathlibModule.PureWindowsPath._flavour = _FakeWindowsFlavour(filesystem) + FakePathlibModule.PurePosixPath._flavour = _FakePosixFlavour(filesystem) + else: + # in Python 3.12, the flavour is no longer an own class, + # but points to the os-specific path module (posixpath/ntpath) + fake_os = FakeOsModule(filesystem) + fake_path = fake_os.path + FakePathlibModule.PureWindowsPath._flavour = fake_path + FakePathlibModule.PurePosixPath._flavour = fake_path def _wrap_strfunc(strfunc): @@ -63,8 +77,7 @@ def _wrap_strfunc(strfunc): def _wrap_binary_strfunc(strfunc): @functools.wraps(strfunc) def _wrapped(pathobj1, pathobj2, *args): - return strfunc( - pathobj1.filesystem, str(pathobj1), str(pathobj2), *args) + return strfunc(pathobj1.filesystem, str(pathobj1), str(pathobj2), *args) return staticmethod(_wrapped) @@ -72,53 +85,59 @@ def _wrap_binary_strfunc(strfunc): def _wrap_binary_strfunc_reverse(strfunc): @functools.wraps(strfunc) def _wrapped(pathobj1, pathobj2, *args): - return strfunc( - pathobj2.filesystem, str(pathobj2), str(pathobj1), *args) + return strfunc(pathobj2.filesystem, str(pathobj2), str(pathobj1), *args) return staticmethod(_wrapped) try: - accessor = pathlib._Accessor # type: ignore [attr-defined] + accessor = pathlib._Accessor # type: ignore[attr-defined] except AttributeError: accessor = object -class _FakeAccessor(accessor): # type: ignore [valid-type, misc] - """Accessor which forwards some of the functions to FakeFilesystem methods. +class _FakeAccessor(accessor): # type: ignore[valid-type, misc] + """Accessor which forwards some of the functions to FakeFilesystem + methods. """ stat = _wrap_strfunc(FakeFilesystem.stat) lstat = _wrap_strfunc( - lambda fs, path: FakeFilesystem.stat(fs, path, follow_symlinks=False)) + lambda fs, path: FakeFilesystem.stat(fs, path, follow_symlinks=False) + ) listdir = _wrap_strfunc(FakeFilesystem.listdir) if use_scandir: scandir = _wrap_strfunc(fake_scandir.scandir) - chmod = _wrap_strfunc(FakeFilesystem.chmod) - if hasattr(os, "lchmod"): - lchmod = _wrap_strfunc(lambda fs, path, mode: FakeFilesystem.chmod( - fs, path, mode, follow_symlinks=False)) + lchmod = _wrap_strfunc( + lambda fs, path, mode: FakeFilesystem.chmod( + fs, path, mode, follow_symlinks=False + ) + ) else: - def lchmod(self, pathobj, *args, **kwargs): + + def lchmod(self, pathobj, *args, **kwargs): """Raises not implemented for Windows systems.""" raise NotImplementedError("lchmod() not available on this system") - def chmod(self, pathobj, *args, **kwargs): - if "follow_symlinks" in kwargs: - if sys.version_info < (3, 10): - raise TypeError("chmod() got an unexpected keyword " - "argument 'follow_synlinks'") - if (not kwargs["follow_symlinks"] and - os.chmod not in os.supports_follow_symlinks): - raise NotImplementedError( - "`follow_symlinks` for chmod() is not available " - "on this system") - return pathobj.filesystem.chmod(str(pathobj), *args, **kwargs) + def chmod(self, pathobj, *args, **kwargs): + if "follow_symlinks" in kwargs: + if sys.version_info < (3, 10): + raise TypeError( + "chmod() got an unexpected keyword " "argument 'follow_symlinks'" + ) + + if not kwargs["follow_symlinks"] and ( + os.chmod not in os.supports_follow_symlinks or IS_PYPY + ): + raise NotImplementedError( + "`follow_symlinks` for chmod() is not available " "on this system" + ) + return pathobj.filesystem.chmod(str(pathobj), *args, **kwargs) mkdir = _wrap_strfunc(FakeFilesystem.makedir) @@ -130,22 +149,29 @@ class _FakeAccessor(accessor): # type: ignore [valid-type, misc] replace = _wrap_binary_strfunc( lambda fs, old_path, new_path: FakeFilesystem.rename( - fs, old_path, new_path, force_replace=True)) + fs, old_path, new_path, force_replace=True + ) + ) symlink = _wrap_binary_strfunc_reverse( - lambda fs, file_path, link_target, target_is_directory: - FakeFilesystem.create_symlink(fs, file_path, link_target, - create_missing_dirs=False)) + lambda fs, fpath, target, target_is_dir: FakeFilesystem.create_symlink( + fs, fpath, target, create_missing_dirs=False + ) + ) if (3, 8) <= sys.version_info: link_to = _wrap_binary_strfunc( - lambda fs, file_path, link_target: - FakeFilesystem.link(fs, file_path, link_target)) + lambda fs, file_path, link_target: FakeFilesystem.link( + fs, file_path, link_target + ) + ) if sys.version_info >= (3, 10): link = _wrap_binary_strfunc( - lambda fs, file_path, link_target: - FakeFilesystem.link(fs, file_path, link_target)) + lambda fs, file_path, link_target: FakeFilesystem.link( + fs, file_path, link_target + ) + ) # this will use the fake filesystem because os is patched def getcwd(self): @@ -158,312 +184,322 @@ class _FakeAccessor(accessor): # type: ignore [valid-type, misc] _fake_accessor = _FakeAccessor() -flavour = pathlib._Flavour # type: ignore [attr-defined] - +if sys.version_info < (3, 12): + flavour = pathlib._Flavour # type: ignore[attr-defined] + + class _FakeFlavour(flavour): # type: ignore[valid-type, misc] + """Fake Flavour implementation used by PurePath and _Flavour""" + + filesystem = None + sep = "/" + altsep = None + has_drv = False + + ext_namespace_prefix = "\\\\?\\" + + drive_letters = {chr(x) for x in range(ord("a"), ord("z") + 1)} | { + chr(x) for x in range(ord("A"), ord("Z") + 1) + } + + def __init__(self, filesystem): + self.filesystem = filesystem + self.sep = filesystem.path_separator + self.altsep = filesystem.alternative_path_separator + self.has_drv = filesystem.is_windows_fs + super(_FakeFlavour, self).__init__() + + @staticmethod + def _split_extended_path(path, ext_prefix=ext_namespace_prefix): + prefix = "" + if path.startswith(ext_prefix): + prefix = path[:4] + path = path[4:] + if path.startswith("UNC\\"): + prefix += path[:3] + path = "\\" + path[3:] + return prefix, path + + def _splitroot_with_drive(self, path, sep): + first = path[0:1] + second = path[1:2] + if second == sep and first == sep: + # extended paths should also disable the collapsing of "." + # components (according to MSDN docs). + prefix, path = self._split_extended_path(path) + first = path[0:1] + second = path[1:2] + else: + prefix = "" + third = path[2:3] + if second == sep and first == sep and third != sep: + # is a UNC path: + # vvvvvvvvvvvvvvvvvvvvv root + # \\machine\mountpoint\directory\etc\... + # directory ^^^^^^^^^^^^^^ + index = path.find(sep, 2) + if index != -1: + index2 = path.find(sep, index + 1) + # a UNC path can't have two slashes in a row + # (after the initial two) + if index2 != index + 1: + if index2 == -1: + index2 = len(path) + if prefix: + return prefix + path[1:index2], sep, path[index2 + 1 :] + return path[:index2], sep, path[index2 + 1 :] + drv = root = "" + if second == ":" and first in self.drive_letters: + drv = path[:2] + path = path[2:] + first = third + if first == sep: + root = first + path = path.lstrip(sep) + return prefix + drv, root, path + + @staticmethod + def _splitroot_posix(path, sep): + if path and path[0] == sep: + stripped_part = path.lstrip(sep) + if len(path) - len(stripped_part) == 2: + return "", sep * 2, stripped_part + return "", sep, stripped_part + else: + return "", "", path + + def splitroot(self, path, sep=None): + """Split path into drive, root and rest.""" + if sep is None: + sep = self.filesystem.path_separator + if self.filesystem.is_windows_fs: + return self._splitroot_with_drive(path, sep) + return self._splitroot_posix(path, sep) + + def casefold(self, path): + """Return the lower-case version of s for a Windows filesystem.""" + if self.filesystem.is_windows_fs: + return path.lower() + return path -class _FakeFlavour(flavour): # type: ignore [valid-type, misc] - """Fake Flavour implementation used by PurePath and _Flavour""" + def casefold_parts(self, parts): + """Return the lower-case version of parts for a Windows filesystem.""" + if self.filesystem.is_windows_fs: + return [p.lower() for p in parts] + return parts - filesystem = None - sep = '/' - altsep = None - has_drv = False + def _resolve_posix(self, path, strict): + sep = self.sep + seen = {} - ext_namespace_prefix = '\\\\?\\' + def _resolve(path, rest): + if rest.startswith(sep): + path = "" - drive_letters = ( - set(chr(x) for x in range(ord('a'), ord('z') + 1)) | - set(chr(x) for x in range(ord('A'), ord('Z') + 1)) - ) - - def __init__(self, filesystem): - self.filesystem = filesystem - self.sep = filesystem.path_separator - self.altsep = filesystem.alternative_path_separator - self.has_drv = filesystem.is_windows_fs - super(_FakeFlavour, self).__init__() - - @staticmethod - def _split_extended_path(path, ext_prefix=ext_namespace_prefix): - prefix = '' - if path.startswith(ext_prefix): - prefix = path[:4] - path = path[4:] - if path.startswith('UNC\\'): - prefix += path[:3] - path = '\\' + path[3:] - return prefix, path - - def _splitroot_with_drive(self, path, sep): - first = path[0:1] - second = path[1:2] - if second == sep and first == sep: - # extended paths should also disable the collapsing of "." - # components (according to MSDN docs). - prefix, path = self._split_extended_path(path) - first = path[0:1] - second = path[1:2] - else: - prefix = '' - third = path[2:3] - if second == sep and first == sep and third != sep: - # is a UNC path: - # vvvvvvvvvvvvvvvvvvvvv root - # \\machine\mountpoint\directory\etc\... - # directory ^^^^^^^^^^^^^^ - index = path.find(sep, 2) - if index != -1: - index2 = path.find(sep, index + 1) - # a UNC path can't have two slashes in a row - # (after the initial two) - if index2 != index + 1: - if index2 == -1: - index2 = len(path) - if prefix: - return prefix + path[1:index2], sep, path[index2 + 1:] - return path[:index2], sep, path[index2 + 1:] - drv = root = '' - if second == ':' and first in self.drive_letters: - drv = path[:2] - path = path[2:] - first = third - if first == sep: - root = first - path = path.lstrip(sep) - return prefix + drv, root, path - - @staticmethod - def _splitroot_posix(path, sep): - if path and path[0] == sep: - stripped_part = path.lstrip(sep) - if len(path) - len(stripped_part) == 2: - return '', sep * 2, stripped_part - return '', sep, stripped_part - else: - return '', '', path - - def splitroot(self, path, sep=None): - """Split path into drive, root and rest.""" - if sep is None: - sep = self.filesystem.path_separator - if self.filesystem.is_windows_fs: - return self._splitroot_with_drive(path, sep) - return self._splitroot_posix(path, sep) - - def casefold(self, path): - """Return the lower-case version of s for a Windows filesystem.""" - if self.filesystem.is_windows_fs: - return path.lower() - return path - - def casefold_parts(self, parts): - """Return the lower-case version of parts for a Windows filesystem.""" - if self.filesystem.is_windows_fs: - return [p.lower() for p in parts] - return parts - - def _resolve_posix(self, path, strict): - sep = self.sep - seen = {} - - def _resolve(path, rest): - if rest.startswith(sep): - path = '' - - for name in rest.split(sep): - if not name or name == '.': - # current dir - continue - if name == '..': - # parent dir - path, _, _ = path.rpartition(sep) - continue - newpath = path + sep + name - if newpath in seen: - # Already seen this path - path = seen[newpath] - if path is not None: - # use cached value + for name in rest.split(sep): + if not name or name == ".": + # current dir + continue + if name == "..": + # parent dir + path, _, _ = path.rpartition(sep) continue - # The symlink is not resolved, so we must have - # a symlink loop. - raise RuntimeError("Symlink loop from %r" % newpath) - # Resolve the symbolic link + newpath = path + sep + name + if newpath in seen: + # Already seen this path + path = seen[newpath] + if path is not None: + # use cached value + continue + # The symlink is not resolved, so we must have + # a symlink loop. + raise RuntimeError("Symlink loop from %r" % newpath) + # Resolve the symbolic link + try: + target = self.filesystem.readlink(newpath) + except OSError as e: + if e.errno != errno.EINVAL and strict: + raise + # Not a symlink, or non-strict mode. We just leave the path + # untouched. + path = newpath + else: + seen[newpath] = None # not resolved symlink + path = _resolve(path, target) + seen[newpath] = path # resolved symlink + + return path + + # NOTE: according to POSIX, getcwd() cannot contain path components + # which are symlinks. + base = "" if path.is_absolute() else self.filesystem.cwd + return _resolve(base, str(path)) or sep + + def _resolve_windows(self, path, strict): + path = str(path) + if not path: + return os.getcwd() + previous_s = None + if strict: + if not self.filesystem.exists(path): + self.filesystem.raise_os_error(errno.ENOENT, path) + return self.filesystem.resolve_path(path) + else: + while True: + try: + path = self.filesystem.resolve_path(path) + except OSError: + previous_s = path + path = self.filesystem.splitpath(path)[0] + else: + if previous_s is None: + return path + return self.filesystem.joinpaths( + path, os.path.basename(previous_s) + ) + + def resolve(self, path, strict): + """Make the path absolute, resolving any symlinks.""" + if self.filesystem.is_windows_fs: + return self._resolve_windows(path, strict) + return self._resolve_posix(path, strict) + + def gethomedir(self, username): + """Return the home directory of the current user.""" + if not username: try: - target = self.filesystem.readlink(newpath) - except OSError as e: - if e.errno != errno.EINVAL and strict: - raise - # Not a symlink, or non-strict mode. We just leave the path - # untouched. - path = newpath - else: - seen[newpath] = None # not resolved symlink - path = _resolve(path, target) - seen[newpath] = path # resolved symlink + return os.environ["HOME"] + except KeyError: + import pwd - return path + return pwd.getpwuid(os.getuid()).pw_dir + else: + import pwd - # NOTE: according to POSIX, getcwd() cannot contain path components - # which are symlinks. - base = '' if path.is_absolute() else self.filesystem.cwd - return _resolve(base, str(path)) or sep + try: + return pwd.getpwnam(username).pw_dir + except KeyError: + raise RuntimeError( + "Can't determine home directory " "for %r" % username + ) + + class _FakeWindowsFlavour(_FakeFlavour): + """Flavour used by PureWindowsPath with some Windows specific + implementations independent of FakeFilesystem properties. + """ - def _resolve_windows(self, path, strict): - path = str(path) - if not path: - return os.getcwd() - previous_s = None - if strict: - if not self.filesystem.exists(path): - self.filesystem.raise_os_error(errno.ENOENT, path) - return self.filesystem.resolve_path(path) - else: - while True: + reserved_names = ( + {"CON", "PRN", "AUX", "NUL"} + | {"COM%d" % i for i in range(1, 10)} + | {"LPT%d" % i for i in range(1, 10)} + ) + pathmod = ntpath + + def is_reserved(self, parts): + """Return True if the path is considered reserved under Windows.""" + + # NOTE: the rules for reserved names seem somewhat complicated + # (e.g. r"..\NUL" is reserved but not r"foo\NUL"). + # We err on the side of caution and return True for paths which are + # not considered reserved by Windows. + if not parts: + return False + if self.filesystem.is_windows_fs and parts[0].startswith("\\\\"): + # UNC paths are never reserved + return False + return parts[-1].partition(".")[0].upper() in self.reserved_names + + def make_uri(self, path): + """Return a file URI for the given path""" + + # Under Windows, file URIs use the UTF-8 encoding. + # original version, not faked + drive = path.drive + if len(drive) == 2 and drive[1] == ":": + # It's a path on a local drive => 'file:///c:/a/b' + rest = path.as_posix()[2:].lstrip("/") + return "file:///%s/%s" % ( + drive, + urlquote_from_bytes(rest.encode("utf-8")), + ) + else: + # It's a path on a network drive => 'file://host/share/a/b' + return "file:" + urlquote_from_bytes(path.as_posix().encode("utf-8")) + + def gethomedir(self, username): + """Return the home directory of the current user.""" + + # original version, not faked + if "HOME" in os.environ: + userhome = os.environ["HOME"] + elif "USERPROFILE" in os.environ: + userhome = os.environ["USERPROFILE"] + elif "HOMEPATH" in os.environ: try: - path = self.filesystem.resolve_path(path) - except OSError: - previous_s = path - path = self.filesystem.splitpath(path)[0] - else: - if previous_s is None: - return path - return self.filesystem.joinpaths( - path, os.path.basename(previous_s)) - - def resolve(self, path, strict): - """Make the path absolute, resolving any symlinks.""" - if self.filesystem.is_windows_fs: - return self._resolve_windows(path, strict) - return self._resolve_posix(path, strict) - - def gethomedir(self, username): - """Return the home directory of the current user.""" - if not username: - try: - return os.environ['HOME'] - except KeyError: - import pwd - return pwd.getpwuid(os.getuid()).pw_dir - else: - import pwd - try: - return pwd.getpwnam(username).pw_dir - except KeyError: - raise RuntimeError("Can't determine home directory " - "for %r" % username) + drv = os.environ["HOMEDRIVE"] + except KeyError: + drv = "" + userhome = drv + os.environ["HOMEPATH"] + else: + raise RuntimeError("Can't determine home directory") + + if username: + # Try to guess user home directory. By default all users + # directories are located in the same place and are named by + # corresponding usernames. If current user home directory points + # to nonstandard place, this guess is likely wrong. + if os.environ["USERNAME"] != username: + drv, root, parts = self.parse_parts((userhome,)) + if parts[-1] != os.environ["USERNAME"]: + raise RuntimeError( + "Can't determine home directory " "for %r" % username + ) + parts[-1] = username + if drv or root: + userhome = drv + root + self.join(parts[1:]) + else: + userhome = self.join(parts) + return userhome + + def compile_pattern(self, pattern): + return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch + + class _FakePosixFlavour(_FakeFlavour): + """Flavour used by PurePosixPath with some Unix specific implementations + independent of FakeFilesystem properties. + """ + pathmod = posixpath -class _FakeWindowsFlavour(_FakeFlavour): - """Flavour used by PureWindowsPath with some Windows specific - implementations independent of FakeFilesystem properties. - """ - reserved_names = ( - {'CON', 'PRN', 'AUX', 'NUL'} | - {'COM%d' % i for i in range(1, 10)} | - {'LPT%d' % i for i in range(1, 10)} - ) + def is_reserved(self, parts): + return False - def is_reserved(self, parts): - """Return True if the path is considered reserved under Windows.""" + def make_uri(self, path): + # We represent the path using the local filesystem encoding, + # for portability to other applications. + bpath = bytes(path) + return "file://" + urlquote_from_bytes(bpath) - # NOTE: the rules for reserved names seem somewhat complicated - # (e.g. r"..\NUL" is reserved but not r"foo\NUL"). - # We err on the side of caution and return True for paths which are - # not considered reserved by Windows. - if not parts: - return False - if self.filesystem.is_windows_fs and parts[0].startswith('\\\\'): - # UNC paths are never reserved - return False - return parts[-1].partition('.')[0].upper() in self.reserved_names - - def make_uri(self, path): - """Return a file URI for the given path""" - - # Under Windows, file URIs use the UTF-8 encoding. - # original version, not faked - drive = path.drive - if len(drive) == 2 and drive[1] == ':': - # It's a path on a local drive => 'file:///c:/a/b' - rest = path.as_posix()[2:].lstrip('/') - return 'file:///%s/%s' % ( - drive, urlquote_from_bytes(rest.encode('utf-8'))) - else: - # It's a path on a network drive => 'file://host/share/a/b' - return ('file:' + - urlquote_from_bytes(path.as_posix().encode('utf-8'))) - - def gethomedir(self, username): - """Return the home directory of the current user.""" - - # original version, not faked - if 'HOME' in os.environ: - userhome = os.environ['HOME'] - elif 'USERPROFILE' in os.environ: - userhome = os.environ['USERPROFILE'] - elif 'HOMEPATH' in os.environ: - try: - drv = os.environ['HOMEDRIVE'] - except KeyError: - drv = '' - userhome = drv + os.environ['HOMEPATH'] - else: - raise RuntimeError("Can't determine home directory") - - if username: - # Try to guess user home directory. By default all users - # directories are located in the same place and are named by - # corresponding usernames. If current user home directory points - # to nonstandard place, this guess is likely wrong. - if os.environ['USERNAME'] != username: - drv, root, parts = self.parse_parts((userhome,)) - if parts[-1] != os.environ['USERNAME']: - raise RuntimeError("Can't determine home directory " - "for %r" % username) - parts[-1] = username - if drv or root: - userhome = drv + root + self.join(parts[1:]) - else: - userhome = self.join(parts) - return userhome - - def compile_pattern(self, pattern): - return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch - - -class _FakePosixFlavour(_FakeFlavour): - """Flavour used by PurePosixPath with some Unix specific implementations - independent of FakeFilesystem properties. - """ + def gethomedir(self, username): + # original version, not faked + if not username: + try: + return os.environ["HOME"] + except KeyError: + import pwd - def is_reserved(self, parts): - return False - - def make_uri(self, path): - # We represent the path using the local filesystem encoding, - # for portability to other applications. - bpath = bytes(path) - return 'file://' + urlquote_from_bytes(bpath) - - def gethomedir(self, username): - # original version, not faked - if not username: - try: - return os.environ['HOME'] - except KeyError: + return pwd.getpwuid(os.getuid()).pw_dir + else: import pwd - return pwd.getpwuid(os.getuid()).pw_dir - else: - import pwd - try: - return pwd.getpwnam(username).pw_dir - except KeyError: - raise RuntimeError("Can't determine home directory " - "for %r" % username) - def compile_pattern(self, pattern): - return re.compile(fnmatch.translate(pattern)).fullmatch + try: + return pwd.getpwnam(username).pw_dir + except KeyError: + raise RuntimeError( + "Can't determine home directory " "for %r" % username + ) + + def compile_pattern(self, pattern): + return re.compile(fnmatch.translate(pattern)).fullmatch class FakePath(pathlib.Path): @@ -479,42 +515,50 @@ class FakePath(pathlib.Path): def __new__(cls, *args, **kwargs): """Creates the correct subclass based on OS.""" if cls is FakePathlibModule.Path: - cls = (FakePathlibModule.WindowsPath - if cls.filesystem.is_windows_fs - else FakePathlibModule.PosixPath) - self = cls._from_parts(args) - return self - - @classmethod - def _from_parts(cls, args, init=False): # pylint: disable=unused-argument - # Overwritten to call _init to set the fake accessor, - # which is not done since Python 3.10 - self = object.__new__(cls) - self._init() - drv, root, parts = self._parse_args(args) - self._drv = drv - self._root = root - self._parts = parts - return self - - @classmethod - def _from_parsed_parts(cls, drv, root, parts): - # Overwritten to call _init to set the fake accessor, - # which is not done since Python 3.10 - self = object.__new__(cls) - self._init() - self._drv = drv - self._root = root - self._parts = parts - return self - - def _init(self, template=None): - """Initializer called from base class.""" - self._accessor = _fake_accessor - self._closed = False + cls = ( + FakePathlibModule.WindowsPath + if cls.filesystem.is_windows_fs + else FakePathlibModule.PosixPath + ) + if sys.version_info < (3, 12): + return cls._from_parts(args) # pytype: disable=attribute-error + else: + return object.__new__(cls) + + if sys.version_info[:2] == (3, 10): + # Overwritten class methods to call _init to set the fake accessor, + # which is not done in Python 3.10, and not needed from Python 3.11 on + @classmethod + def _from_parts(cls, args): + self = object.__new__(cls) + self._init() + drv, root, parts = self._parse_args(args) # pytype: disable=attribute-error + self._drv = drv + self._root = root + self._parts = parts + return self + + @classmethod + def _from_parsed_parts(cls, drv, root, parts): + self = object.__new__(cls) + self._drv = drv + self._root = root + self._parts = parts + self._init() + return self + + if sys.version_info < (3, 11): + + def _init(self, template=None): + """Initializer called from base class.""" + # only needed until Python 3.10 + self._accessor = _fake_accessor + # only needed until Python 3.8 + self._closed = False def _path(self): - """Returns the underlying path string as used by the fake filesystem. + """Returns the underlying path string as used by the fake + filesystem. """ return str(self) @@ -525,48 +569,51 @@ class FakePath(pathlib.Path): """ return cls(cls.filesystem.cwd) - def resolve(self, strict=None): - """Make the path absolute, resolving all symlinks on the way and also - normalizing it (for example turning slashes into backslashes - under Windows). + if sys.version_info < (3, 12): # in 3.12, we can use the pathlib implementation - Args: - strict: If False (default) no exception is raised if the path - does not exist. - New in Python 3.6. + def resolve(self, strict=None): + """Make the path absolute, resolving all symlinks on the way and also + normalizing it (for example turning slashes into backslashes + under Windows). - Raises: - OSError: if the path doesn't exist (strict=True or Python < 3.6) - """ - if sys.version_info >= (3, 6): - if strict is None: - strict = False - else: - if strict is not None: - raise TypeError( - "resolve() got an unexpected keyword argument 'strict'") - strict = True - if self._closed: - self._raise_closed() - path = self._flavour.resolve(self, strict=strict) - if path is None: - self.stat() - path = str(self.absolute()) - path = self.filesystem.absnormpath(path) - return FakePath(path) - - def open(self, mode='r', buffering=-1, encoding=None, - errors=None, newline=None): + Args: + strict: If False (default) no exception is raised if the path + does not exist. + New in Python 3.6. + + Raises: + OSError: if the path doesn't exist (strict=True or Python < 3.6) + """ + if sys.version_info >= (3, 6): + if strict is None: + strict = False + else: + if strict is not None: + raise TypeError( + "resolve() got an unexpected keyword argument 'strict'" + ) + strict = True + self._raise_on_closed() + path = self._flavour.resolve( + self, strict=strict + ) # pytype: disable=attribute-error + if path is None: + self.stat() + path = str(self.absolute()) + path = self.filesystem.absnormpath(path) + return FakePath(path) + + def open(self, mode="r", buffering=-1, encoding=None, errors=None, newline=None): """Open the file pointed by this path and return a fake file object. Raises: OSError: if the target object is a directory, the path is invalid or permission is denied. """ - if self._closed: - self._raise_closed() + self._raise_on_closed() return FakeFileOpen(self.filesystem)( - self._path(), mode, buffering, encoding, errors, newline) + self._path(), mode, buffering, encoding, errors, newline + ) def read_bytes(self): """Open the fake file in bytes mode, read it, and close the file. @@ -575,16 +622,18 @@ class FakePath(pathlib.Path): OSError: if the target object is a directory, the path is invalid or permission is denied. """ - with FakeFileOpen(self.filesystem)(self._path(), mode='rb') as f: + with FakeFileOpen(self.filesystem)( + self._path(), mode="rb" + ) as f: # pytype: disable=attribute-error return f.read() def read_text(self, encoding=None, errors=None): """ Open the fake file in text mode, read it, and close the file. """ - with FakeFileOpen(self.filesystem)(self._path(), mode='r', - encoding=encoding, - errors=errors) as f: + with FakeFileOpen(self.filesystem)( # pytype: disable=attribute-error + self._path(), mode="r", encoding=encoding, errors=errors + ) as f: return f.read() def write_bytes(self, data): @@ -597,7 +646,9 @@ class FakePath(pathlib.Path): """ # type-check for the buffer interface before truncating the file view = memoryview(data) - with FakeFileOpen(self.filesystem)(self._path(), mode='wb') as f: + with FakeFileOpen(self.filesystem)( + self._path(), mode="wb" + ) as f: # pytype: disable=attribute-error return f.write(view) def write_text(self, data, encoding=None, errors=None, newline=None): @@ -617,16 +668,18 @@ class FakePath(pathlib.Path): invalid or permission is denied. """ if not isinstance(data, str): - raise TypeError('data must be str, not %s' % - data.__class__.__name__) + raise TypeError("data must be str, not %s" % data.__class__.__name__) if newline is not None and sys.version_info < (3, 10): - raise TypeError("write_text() got an unexpected " - "keyword argument 'newline'") - with FakeFileOpen(self.filesystem)(self._path(), - mode='w', - encoding=encoding, - errors=errors, - newline=newline) as f: + raise TypeError( + "write_text() got an unexpected " "keyword argument 'newline'" + ) + with FakeFileOpen(self.filesystem)( # pytype: disable=attribute-error + self._path(), + mode="w", + encoding=encoding, + errors=errors, + newline=newline, + ) as f: return f.write(data) @classmethod @@ -635,13 +688,14 @@ class FakePath(pathlib.Path): returned by os.path.expanduser('~')). """ home = os.path.expanduser("~") - if cls.filesystem.is_windows_fs != (os.name == 'nt'): + if cls.filesystem.is_windows_fs != (os.name == "nt"): username = os.path.split(home)[1] if cls.filesystem.is_windows_fs: - home = os.path.join('C:', 'Users', username) + home = os.path.join("C:", "Users", username) else: - home = os.path.join('home', username) - cls.filesystem.create_dir(home) + home = os.path.join("home", username) + if not cls.filesystem.exists(home): + cls.filesystem.create_dir(home) return cls(home.replace(os.sep, cls.filesystem.path_separator)) def samefile(self, other_path): @@ -660,16 +714,21 @@ class FakePath(pathlib.Path): other_st = other_path.stat() except AttributeError: other_st = self.filesystem.stat(other_path) - return (st.st_ino == other_st.st_ino and - st.st_dev == other_st.st_dev) + return st.st_ino == other_st.st_ino and st.st_dev == other_st.st_dev def expanduser(self): - """ Return a new path with expanded ~ and ~user constructs + """Return a new path with expanded ~ and ~user constructs (as returned by os.path.expanduser) """ - return FakePath(os.path.expanduser(self._path()) - .replace(os.path.sep, - self.filesystem.path_separator)) + return FakePath( + os.path.expanduser(self._path()).replace( + os.path.sep, self.filesystem.path_separator + ) + ) + + def _raise_on_closed(self): + if sys.version_info < (3, 9) and self._closed: + self._raise_closed() def touch(self, mode=0o666, exist_ok=True): """Create a fake file for the path with the given access mode, @@ -683,18 +742,36 @@ class FakePath(pathlib.Path): Raises: FileExistsError: if the file exists and exits_ok is False. """ - if self._closed: - self._raise_closed() + self._raise_on_closed() if self.exists(): if exist_ok: self.filesystem.utime(self._path(), times=None) else: self.filesystem.raise_os_error(errno.EEXIST, self._path()) else: - fake_file = self.open('w') + fake_file = self.open("w") fake_file.close() self.chmod(mode) + if sys.version_info >= (3, 12): + """These are reimplemented for now because the original implementation + checks the flavour against ntpath/posixpath. + """ + + def is_absolute(self): + if self.filesystem.is_windows_fs: + return self.drive and self.root + return os.path.isabs(self._path()) + + def is_reserved(self): + if not self.filesystem.is_windows_fs or not self._tail: + return False + if self._tail[0].startswith("\\\\"): + # UNC paths are never reserved. + return False + name = self._tail[-1].partition(".")[0].partition(":")[0].rstrip(" ") + return name.upper() in pathlib._WIN_RESERVED_NAMES + class FakePathlibModule: """Uses FakeFilesystem to provide a fake pathlib module replacement. @@ -719,51 +796,54 @@ class FakePathlibModule: class PurePosixPath(PurePath): """A subclass of PurePath, that represents non-Windows filesystem paths""" + __slots__ = () class PureWindowsPath(PurePath): """A subclass of PurePath, that represents Windows filesystem paths""" + __slots__ = () class WindowsPath(FakePath, PureWindowsPath): """A subclass of Path and PureWindowsPath that represents concrete Windows filesystem paths. """ + __slots__ = () def owner(self): - raise NotImplementedError( - "Path.owner() is unsupported on this system") + raise NotImplementedError("Path.owner() is unsupported on this system") def group(self): - raise NotImplementedError( - "Path.group() is unsupported on this system") + raise NotImplementedError("Path.group() is unsupported on this system") def is_mount(self): - raise NotImplementedError( - "Path.is_mount() is unsupported on this system") + raise NotImplementedError("Path.is_mount() is unsupported on this system") class PosixPath(FakePath, PurePosixPath): """A subclass of Path and PurePosixPath that represents concrete non-Windows filesystem paths. """ + __slots__ = () def owner(self): - """Return the current user name. It is assumed that the fake - file system was created by the current user. + """Return the username of the file owner. + It is assumed that `st_uid` is related to a real user, + otherwise `KeyError` is raised. """ import pwd - return pwd.getpwuid(os.getuid()).pw_name + return pwd.getpwuid(self.stat().st_uid).pw_name def group(self): - """Return the current group name. It is assumed that the fake - file system was created by the current user. + """Return the group name of the file group. + It is assumed that `st_gid` is related to a real group, + otherwise `KeyError` is raised. """ import grp - return grp.getgrgid(os.getgid()).gr_name + return grp.getgrgid(self.stat().st_gid).gr_name Path = FakePath @@ -774,6 +854,7 @@ class FakePathlibModule: class FakePathlibPathModule: """Patches `pathlib.Path` by passing all calls to FakePathlibModule.""" + fake_pathlib = None def __init__(self, filesystem=None): @@ -786,6 +867,11 @@ class FakePathlibPathModule: def __getattr__(self, name): return getattr(self.fake_pathlib.Path, name) + @classmethod + def __instancecheck__(cls, instance): + # fake the inheritance to pass isinstance checks - see #666 + return isinstance(instance, PurePath) + class RealPath(pathlib.Path): """Replacement for `pathlib.Path` if it shall not be faked. @@ -793,13 +879,67 @@ class RealPath(pathlib.Path): itself is not. """ + if sys.version_info < (3, 12): + _flavour = ( + pathlib._WindowsFlavour() # type:ignore + if os.name == "nt" + else pathlib._PosixFlavour() # type:ignore + ) # type:ignore + else: + _flavour = ntpath if os.name == "nt" else posixpath + def __new__(cls, *args, **kwargs): """Creates the correct subclass based on OS.""" if cls is RealPathlibModule.Path: - cls = (RealPathlibModule.WindowsPath if os.name == 'nt' - else RealPathlibModule.PosixPath) - self = cls._from_parts(args) - return self + cls = ( + RealPathlibModule.WindowsPath # pytype: disable=attribute-error + if os.name == "nt" + else RealPathlibModule.PosixPath # pytype: disable=attribute-error + ) + if sys.version_info < (3, 12): + return cls._from_parts(args) # pytype: disable=attribute-error + else: + return object.__new__(cls) + + +if sys.version_info > (3, 10): + + def with_original_os(f: Callable) -> Callable: + """Decorator used for real pathlib Path methods to ensure that + real os functions instead of faked ones are used.""" + + @functools.wraps(f) + def wrapped(*args, **kwargs): + with use_original_os(): + return f(*args, **kwargs) + + return wrapped + + for name, fn in inspect.getmembers(RealPath, inspect.isfunction): + if not name.startswith("__"): + setattr(RealPath, name, with_original_os(fn)) + + +class RealPathlibPathModule: + """Patches `pathlib.Path` by passing all calls to RealPathlibModule.""" + + real_pathlib = None + + @classmethod + def __instancecheck__(cls, instance): + # as we cannot derive from pathlib.Path, we fake + # the inheritance to pass isinstance checks - see #666 + return isinstance(instance, PurePath) + + def __init__(self): + if self.real_pathlib is None: + self.__class__.real_pathlib = RealPathlibModule() + + def __call__(self, *args, **kwargs): + return RealPath(*args, **kwargs) + + def __getattr__(self, name): + return getattr(self.real_pathlib.Path, name) class RealPathlibModule: @@ -809,29 +949,34 @@ class RealPathlibModule: """ def __init__(self): - RealPathlibModule.PureWindowsPath._flavour = pathlib._WindowsFlavour() - RealPathlibModule.PurePosixPath._flavour = pathlib._PosixFlavour() self._pathlib_module = pathlib class PurePosixPath(PurePath): """A subclass of PurePath, that represents Posix filesystem paths""" + __slots__ = () class PureWindowsPath(PurePath): """A subclass of PurePath, that represents Windows filesystem paths""" + __slots__ = () - if sys.platform == 'win32': + if sys.platform == "win32": + class WindowsPath(RealPath, PureWindowsPath): """A subclass of Path and PureWindowsPath that represents concrete Windows filesystem paths. """ + __slots__ = () + else: + class PosixPath(RealPath, PurePosixPath): """A subclass of Path and PurePosixPath that represents concrete non-Windows filesystem paths. """ + __slots__ = () Path = RealPath @@ -839,24 +984,3 @@ class RealPathlibModule: def __getattr__(self, name): """Forwards any unfaked calls to the standard pathlib module.""" return getattr(self._pathlib_module, name) - - -class RealPathlibPathModule: - """Patches `pathlib.Path` by passing all calls to RealPathlibModule.""" - real_pathlib = None - - @classmethod - def __instancecheck__(cls, instance): - # as we cannot derive from pathlib.Path, we fake - # the inheritance to pass isinstance checks - see #666 - return isinstance(instance, PurePath) - - def __init__(self): - if self.real_pathlib is None: - self.__class__.real_pathlib = RealPathlibModule() - - def __call__(self, *args, **kwargs): - return self.real_pathlib.Path(*args, **kwargs) - - def __getattr__(self, name): - return getattr(self.real_pathlib.Path, name) |