From 199561f04084f0e92dd452c3916570b8bc8d65bf Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Tue, 28 Mar 2023 20:19:21 +0200 Subject: [PATCH] Add support for current Python 3.12 - adapt for changed pathlib implementation (removed flavour implementation) - Windows: add patching for some os.path functions now implemented in nt instead of ntpath - fix handling of devnull for changed OS - add fake implementation for os.path.splitroot - fixes #770 --- .github/workflows/testsuite.yml | 3 +- CHANGES.md | 2 + pyfakefs/fake_filesystem.py | 57 ++ pyfakefs/fake_filesystem_unittest.py | 4 +- pyfakefs/fake_path.py | 57 +- pyfakefs/fake_pathlib.py | 683 +++++++++--------- pyfakefs/tests/fake_filesystem_test.py | 46 ++ .../tests/fake_filesystem_unittest_test.py | 19 +- pyfakefs/tests/fake_os_test.py | 4 + pyfakefs/tests/fake_pathlib_test.py | 2 +- pyfakefs/tests/import_as_example.py | 17 +- pyfakefs/tests/test_utils.py | 7 +- 12 files changed, 569 insertions(+), 332 deletions(-) diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index f55c3811..4e46f428 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -10,8 +10,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] -# python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12-dev"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12-dev"] include: - python-version: "pypy-3.7" os: ubuntu-latest diff --git a/CHANGES.md b/CHANGES.md index afa02a94..79124e76 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ The released versions correspond to PyPI releases. ### Features * added possibility to set a path inaccessible under Windows by using `chown()` with the `force_unix_mode` flag (see [#720](../../issues/720)) +* added support for changes in Python 3.12 (currently in last beta version) +* added support for `os.path.splitroot` (new in Python 3.12) ## [Version 5.1.0](https://pypi.python.org/pypi/pyfakefs/5.1.0) (2023-01-12) New version before Debian freeze diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index d9350001..7a5c6b58 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -325,6 +325,7 @@ def reset(self, total_size: Optional[int] = None): """Remove all file system contents and reset the root.""" self.root = FakeDirectory(self.path_separator, filesystem=self) + self.dev_null = FakeNullFile(self) self.open_files = [] self._free_fd_heap = [] self.last_ino = 0 @@ -1071,6 +1072,62 @@ def splitdrive(self, path: AnyStr) -> Tuple[AnyStr, AnyStr]: return path_str[:2], path_str[2:] return path_str[:0], path_str + def splitroot(self, path: AnyStr): + """Split a pathname into drive, root and tail. + Implementation taken from ntpath and posixpath. + """ + p = os.fspath(path) + if isinstance(p, bytes): + sep = self.path_separator.encode() + altsep = None + if self.alternative_path_separator: + altsep = self.alternative_path_separator.encode() + colon = b":" + unc_prefix = b"\\\\?\\UNC\\" + empty = b"" + else: + sep = self.path_separator + altsep = self.alternative_path_separator + colon = ":" + unc_prefix = "\\\\?\\UNC\\" + empty = "" + if self.is_windows_fs: + normp = p.replace(altsep, sep) if altsep else p + if normp[:1] == sep: + if normp[1:2] == sep: + # UNC drives, e.g. \\server\share or \\?\UNC\server\share + # Device drives, e.g. \\.\device or \\?\device + start = 8 if normp[:8].upper() == unc_prefix else 2 + index = normp.find(sep, start) + if index == -1: + return p, empty, empty + index2 = normp.find(sep, index + 1) + if index2 == -1: + return p, empty, empty + return p[:index2], p[index2 : index2 + 1], p[index2 + 1 :] + else: + # Relative path with root, e.g. \Windows + return empty, p[:1], p[1:] + elif normp[1:2] == colon: + if normp[2:3] == sep: + # Absolute drive-letter path, e.g. X:\Windows + return p[:2], p[2:3], p[3:] + else: + # Relative path with drive, e.g. X:Windows + return p[:2], empty, p[2:] + else: + # Relative path, e.g. Windows + return empty, empty, p + else: + if p[:1] != sep: + # Relative path, e.g.: 'foo' + return empty, empty, p + elif p[1:2] != sep or p[2:3] == sep: + # Absolute path, e.g.: '/foo', '///foo', '////foo', etc. + return empty, sep, p[1:] + else: + return empty, p[:2], p[2:] + def _join_paths_with_drive_support(self, *all_paths: AnyStr) -> AnyStr: """Taken from Python 3.5 os.path.join() code in ntpath.py and slightly adapted""" diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py index 0a591d3a..8036ab2e 100644 --- a/pyfakefs/fake_filesystem_unittest.py +++ b/pyfakefs/fake_filesystem_unittest.py @@ -653,7 +653,9 @@ def _init_fake_module_classes(self) -> None: if IS_PYPY: # in PyPy io.open, the module is referenced as _io self._fake_module_classes["_io"] = fake_io.FakeIoModule - if sys.platform != "win32": + if sys.platform == "win32": + self._fake_module_classes["nt"] = fake_path.FakeNtModule + else: self._fake_module_classes["fcntl"] = fake_filesystem.FakeFcntlModule # class modules maps class names against a list of modules they can diff --git a/pyfakefs/fake_path.py b/pyfakefs/fake_path.py index 0fb87ffc..514fb386 100644 --- a/pyfakefs/fake_path.py +++ b/pyfakefs/fake_path.py @@ -101,7 +101,7 @@ def dir() -> List[str]: "samefile", ] if sys.version_info >= (3, 12): - dir_list.append("isjunction") + dir_list += ["isjunction", "splitroot"] return dir_list def __init__(self, filesystem: "FakeFilesystem", os_module: "FakeOsModule"): @@ -198,6 +198,12 @@ def isjunction(self, path: AnyStr) -> bool: """Returns False. Junctions are never faked.""" return self.filesystem.isjunction(path) + def splitroot(self, path: AnyStr): + """Split a pathname into drive, root and tail. + Implementation taken from ntpath and posixpath. + """ + return self.filesystem.splitroot(path) + def getmtime(self, path: AnyStr) -> float: """Returns the modification time of the fake file. @@ -473,3 +479,52 @@ def ismount(self, path: AnyStr) -> bool: def __getattr__(self, name: str) -> Any: """Forwards any non-faked calls to the real os.path.""" return getattr(self._os_path, name) + + +if sys.platform == "win32": + + class FakeNtModule: + """Under windows, a few function of `os.path` are taken from the `nt` module + for performance reasons. These are patched here. + """ + + @staticmethod + def dir(): + if sys.version_info >= (3, 12): + return ["_path_exists", "_path_isfile", "_path_isdir", "_path_islink"] + else: + return ["_isdir"] + + def __init__(self, filesystem: "FakeFilesystem"): + """Init. + + Args: + filesystem: FakeFilesystem used to provide file system information + """ + import nt + + self.filesystem = filesystem + self.nt_module: Any = nt + + if sys.version_info >= (3, 12): + + def _path_isdir(self, path: AnyStr) -> bool: + return self.filesystem.isdir(path) + + def _path_isfile(self, path: AnyStr) -> bool: + return self.filesystem.isfile(path) + + def _path_islink(self, path: AnyStr) -> bool: + return self.filesystem.islink(path) + + def _path_exists(self, path: AnyStr) -> bool: + return self.filesystem.exists(path) + + else: + + def _isdir(self, path: AnyStr) -> bool: + return self.filesystem.isdir(path) + + def __getattr__(self, name: str) -> Any: + """Forwards any non-faked calls to the real nt module.""" + return getattr(self.nt_module, name) diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index 8d9fa1fb..4f8ec6fd 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -46,15 +46,23 @@ from pyfakefs.extra_packages import use_scandir from pyfakefs.fake_filesystem import FakeFilesystem from pyfakefs.fake_open import FakeFileOpen -from pyfakefs.fake_os import use_original_os +from pyfakefs.fake_os import FakeOsModule, use_original_os 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): @@ -176,322 +184,322 @@ def getcwd(self): _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""" -class _FakeFlavour(flavour): # type: ignore [valid-type, misc] - """Fake Flavour implementation used by PurePath and _Flavour""" + filesystem = None + sep = "/" + altsep = None + has_drv = False - filesystem = None - sep = "/" - altsep = None - has_drv = False + ext_namespace_prefix = "\\\\?\\" - 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) + } - 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__() - 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) + @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] - 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 - 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 - + 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 - # 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: - return os.environ["HOME"] - except KeyError: - import pwd - - return pwd.getpwuid(os.getuid()).pw_dir - else: - import pwd + 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 - try: - return pwd.getpwnam(username).pw_dir - except KeyError: - raise RuntimeError( - "Can't determine home directory " "for %r" % username - ) + def _resolve_posix(self, path, strict): + sep = self.sep + seen = {} + def _resolve(path, rest): + if rest.startswith(sep): + path = "" -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)} - ) - pathmod = ntpath + 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 + 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: + return os.environ["HOME"] + except KeyError: + import pwd - def is_reserved(self, parts): - """Return True if the path is considered reserved under Windows.""" + return pwd.getpwuid(os.getuid()).pw_dir + else: + import pwd - # 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"]: + try: + return pwd.getpwnam(username).pw_dir + except KeyError: 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 _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)} + ) + 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: + 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. + """ -class _FakePosixFlavour(_FakeFlavour): - """Flavour used by PurePosixPath with some Unix specific implementations - independent of FakeFilesystem properties. - """ + pathmod = posixpath - pathmod = posixpath + def is_reserved(self, parts): + return False - 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 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: + import pwd - 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 - ) + 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 + def compile_pattern(self, pattern): + return re.compile(fnmatch.translate(pattern)).fullmatch class FakePath(pathlib.Path): @@ -521,7 +529,10 @@ def _from_parts(cls, args, init=False): # pylint: disable=unused-argument # which is not done since Python 3.10 self = object.__new__(cls) self._init() - drv, root, parts = self._parse_args(args) + parse_fct = ( + self._parse_parts if sys.version_info >= (3, 12) else self._parse_args + ) + drv, root, parts = parse_fct(args) self._drv = drv self._root = root self._parts = parts @@ -556,34 +567,38 @@ def cwd(cls): """ 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) + 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 + 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): """Open the file pointed by this path and return a fake file object. @@ -729,6 +744,25 @@ def touch(self, mode=0o666, exist_ok=True): 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._drv and self._root + return os.path.isabs(self._path()) + + def is_reserved(self): + if not self.filesystem.is_windows_fs or not self._parts: + return False + if self._parts[0].startswith("\\\\"): + # UNC paths are never reserved. + return False + name = self._parts[-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. @@ -836,11 +870,14 @@ class RealPath(pathlib.Path): itself is not. """ - _flavour = ( - pathlib._WindowsFlavour() # type:ignore - if os.name == "nt" - else pathlib._PosixFlavour() # type:ignore - ) # type:ignore + 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.""" diff --git a/pyfakefs/tests/fake_filesystem_test.py b/pyfakefs/tests/fake_filesystem_test.py index dc929b22..dda46383 100644 --- a/pyfakefs/tests/fake_filesystem_test.py +++ b/pyfakefs/tests/fake_filesystem_test.py @@ -1230,6 +1230,14 @@ def test_getattr_forward_to_real_os_path(self): ) self.assertFalse(hasattr(self.path, "nonexistent")) + def test_splitroot_posix(self): + self.filesystem.is_windows_fs = False + self.assertEqual(("", "", "foo!bar"), self.filesystem.splitroot("foo!bar")) + self.assertEqual(("", "!", "foo!bar"), self.filesystem.splitroot("!foo!bar")) + self.assertEqual( + ("", "!!", "foo!!bar"), self.filesystem.splitroot("!!foo!!bar") + ) + class PathManipulationTestBase(TestCase): def setUp(self): @@ -1581,6 +1589,44 @@ def test_split_path_with_unc_path_alt_sep(self): self.assertEqual(("^^foo^bar", ""), self.filesystem.splitpath("^^foo^bar")) self.assertEqual(("^^foo^bar^^", ""), self.filesystem.splitpath("^^foo^bar^^")) + def test_splitroot_with_drive(self): + self.assertEqual( + ("E:", "!", "foo!bar"), self.filesystem.splitroot("E:!foo!bar") + ) + self.assertEqual( + ("E:", "!", "!foo!!!bar"), self.filesystem.splitroot("E:!!foo!!!bar") + ) + self.assertEqual( + (b"E:", b"!", b"!foo!!!bar"), self.filesystem.splitroot(b"E:!!foo!!!bar") + ) + self.assertEqual( + ("C:", "^", "foo^bar"), self.filesystem.splitroot("C:^foo^bar") + ) + + def test_splitroot_with_unc_path(self): + self.assertEqual( + ("!!foo!bar", "!", "baz"), self.filesystem.splitroot("!!foo!bar!baz") + ) + self.assertEqual( + ("!!?!UNC", "!", "foo!bar"), self.filesystem.splitroot("!!?!UNC!foo!bar") + ) + self.assertEqual( + ("^^foo^bar", "^", "baz"), self.filesystem.splitroot("^^foo^bar^baz") + ) + self.assertEqual( + (b"!!foo!bar", b"!", b"baz"), self.filesystem.splitroot(b"!!foo!bar!baz") + ) + + def test_splitroot_with_empty_parts(self): + self.assertEqual(("", "", ""), self.filesystem.splitroot("")) + self.assertEqual(("", "!", "foo"), self.filesystem.splitroot("!foo")) + self.assertEqual(("!!foo!bar", "", ""), self.filesystem.splitroot("!!foo!bar")) + self.assertEqual(("!!foo", "", ""), self.filesystem.splitroot("!!foo")) + self.assertEqual( + ("!!foo!bar", "!", ""), self.filesystem.splitroot("!!foo!bar!") + ) + self.assertEqual(("C:", "", "foo!bar"), self.filesystem.splitroot("C:foo!bar")) + class DiskSpaceTest(TestCase): def setUp(self): diff --git a/pyfakefs/tests/fake_filesystem_unittest_test.py b/pyfakefs/tests/fake_filesystem_unittest_test.py index bdb260bd..52342388 100644 --- a/pyfakefs/tests/fake_filesystem_unittest_test.py +++ b/pyfakefs/tests/fake_filesystem_unittest_test.py @@ -181,11 +181,28 @@ def test_import_path_from_pathlib(self): self.fs.create_dir(file_path) self.assertTrue(pyfakefs.tests.import_as_example.check_if_exists3(file_path)) - def test_import_function_from_os_path(self): + def test_import_exists_from_os_path(self): file_path = "/foo/bar" self.fs.create_dir(file_path) self.assertTrue(pyfakefs.tests.import_as_example.check_if_exists5(file_path)) + def test_import_isfile_from_os_path(self): + file_path = "/foo/bar" + self.fs.create_file(file_path) + self.assertTrue(pyfakefs.tests.import_as_example.check_if_isfile(file_path)) + + def test_import_isdir_from_os_path(self): + file_path = "/foo/bar" + self.fs.create_dir(file_path) + self.assertTrue(pyfakefs.tests.import_as_example.check_if_isdir(file_path)) + + def test_import_islink_from_os_path(self): + file_path = "/foo/bar" + link_path = "/foo/link" + self.fs.create_file(file_path) + self.fs.create_symlink(link_path, file_path) + self.assertTrue(pyfakefs.tests.import_as_example.check_if_islink(link_path)) + def test_import_function_from_os_path_as_other_name(self): file_path = "/foo/bar" self.fs.create_dir(file_path) diff --git a/pyfakefs/tests/fake_os_test.py b/pyfakefs/tests/fake_os_test.py index b0b80874..d3727a12 100644 --- a/pyfakefs/tests/fake_os_test.py +++ b/pyfakefs/tests/fake_os_test.py @@ -4338,10 +4338,14 @@ def test_writing_behind_end_of_file(self): def test_devnull_posix(self): self.check_posix_only() + # make sure os.devnull is correctly set after changing the filesystem + self.setup_fake_fs() self.assertTrue(self.os.path.exists(self.os.devnull)) def test_devnull_windows(self): self.check_windows_only() + # make sure os.devnull is correctly set after changing the filesystem + self.setup_fake_fs() if sys.version_info < (3, 8): self.assertFalse(self.os.path.exists(self.os.devnull)) else: diff --git a/pyfakefs/tests/fake_pathlib_test.py b/pyfakefs/tests/fake_pathlib_test.py index 6b8d1e3f..6cc7bd98 100644 --- a/pyfakefs/tests/fake_pathlib_test.py +++ b/pyfakefs/tests/fake_pathlib_test.py @@ -203,8 +203,8 @@ def use_real_fs(self): class FakePathlibPurePathTest(RealPathlibTestCase): """Tests functionality present in PurePath class.""" - @unittest.skipIf(is_windows, "POSIX specific behavior") def test_is_reserved_posix(self): + self.check_posix_only() self.assertFalse(self.path("/dev").is_reserved()) self.assertFalse(self.path("/").is_reserved()) self.assertFalse(self.path("COM1").is_reserved()) diff --git a/pyfakefs/tests/import_as_example.py b/pyfakefs/tests/import_as_example.py index 956f0989..74f95059 100644 --- a/pyfakefs/tests/import_as_example.py +++ b/pyfakefs/tests/import_as_example.py @@ -22,7 +22,7 @@ from os import path from os import stat from os import stat as my_stat -from os.path import exists +from os.path import exists, isfile, isdir, islink from os.path import exists as my_exists from pathlib import Path @@ -61,6 +61,21 @@ def check_if_exists7(filepath): return pathlib.Path(filepath).exists() +def check_if_isfile(filepath): + # tests patching `isfile` imported from os.path + return isfile(filepath) + + +def check_if_isdir(filepath): + # tests patching `isdir` imported from os.path + return isdir(filepath) + + +def check_if_islink(filepath): + # tests patching `islink` imported from os.path + return islink(filepath) + + def file_stat1(filepath): # tests patching `stat` imported from os return stat(filepath) diff --git a/pyfakefs/tests/test_utils.py b/pyfakefs/tests/test_utils.py index 780e806f..75a28efe 100644 --- a/pyfakefs/tests/test_utils.py +++ b/pyfakefs/tests/test_utils.py @@ -431,12 +431,15 @@ def setUp(self): self.filesystem = fake_filesystem.FakeFilesystem( path_separator=self.path_separator() ) + self.setup_fake_fs() + self.setUpFileSystem() + + def setup_fake_fs(self): + if not self.use_real_fs(): self.open = fake_open.FakeFileOpen(self.filesystem) self.os = fake_os.FakeOsModule(self.filesystem) self.create_basepath() - self.setUpFileSystem() - def tearDown(self): RealFsTestMixin.tearDown(self)