From d75b2ce2fcc85f4e67637c9595b26b9219554278 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Sun, 23 Jun 2024 20:31:44 +0200 Subject: [PATCH] Preliminary support for Python 3.13 - account for changes in pathlib implementation - improve handling of admin user under Windows - add os.isreserved - changed os.path.isabs under Windows --- .github/workflows/testsuite.yml | 7 ++- CHANGES.md | 6 ++- pyfakefs/fake_file.py | 3 +- pyfakefs/fake_filesystem.py | 16 +++++- pyfakefs/fake_filesystem_unittest.py | 15 ++++++ pyfakefs/fake_open.py | 50 ++++++++++++++++--- pyfakefs/fake_path.py | 18 ++++++- pyfakefs/fake_pathlib.py | 49 +++++++++++++----- pyfakefs/fake_scandir.py | 1 + pyfakefs/helpers.py | 11 ++-- .../pytest_tests/pytest_reload_pandas_test.py | 14 ++++-- pyfakefs/tests/fake_filesystem_test.py | 19 ++++--- pyfakefs/tests/fake_os_test.py | 18 +++++-- pyfakefs/tests/fake_pathlib_test.py | 23 ++++++--- pyfakefs/tests/import_as_example.py | 1 + pyfakefs/tests/test_utils.py | 24 ++------- 16 files changed, 204 insertions(+), 71 deletions(-) diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index aad89bfd..3a59bc94 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -3,6 +3,10 @@ name: Testsuite on: [push, pull_request] +defaults: + run: + shell: bash + jobs: pytype: runs-on: ubuntu-latest @@ -28,7 +32,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", "3.12"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12", "3.13-dev"] include: - python-version: "pypy-3.7" os: ubuntu-latest @@ -53,7 +57,6 @@ jobs: run: | python -m pip install --upgrade pip echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - echo "dir=$(pip cache dir)" >> $env:GITHUB_OUTPUT - name: Cache dependencies id: cache-dep diff --git a/CHANGES.md b/CHANGES.md index e7f76b86..f1637edf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,9 +7,11 @@ The released versions correspond to PyPI releases. ## Unreleased -### Fixes +### Enhancements +* added preliminary support for Python 3.13 (tested with beta2) (see [#1017](../../issues/1017)) -* Use real open calls for remaining `pathlib` functions so that it works nice with skippedmodules (see #1012) +### Fixes +* use real open calls for remaining `pathlib` functions so that it works nice with skippedmodules (see [#1012](../../issues/1012)) ## [Version 5.5.0](https://pypi.python.org/pypi/pyfakefs/5.5.0) (2024-05-12) Deprecates the usage of `pathlib2` and `scandir`. diff --git a/pyfakefs/fake_file.py b/pyfakefs/fake_file.py index 439e91f0..d045cd7e 100644 --- a/pyfakefs/fake_file.py +++ b/pyfakefs/fake_file.py @@ -54,6 +54,7 @@ AnyString, get_locale_encoding, _OpenModes, + is_root, ) if TYPE_CHECKING: @@ -587,7 +588,7 @@ def remove_entry(self, pathname_name: str, recursive: bool = True) -> None: pathname_name = self._normalized_entryname(pathname_name) entry = self.get_entry(pathname_name) if self.filesystem.is_windows_fs: - if entry.st_mode & helpers.PERM_WRITE == 0: + if not is_root() and entry.st_mode & helpers.PERM_WRITE == 0: self.filesystem.raise_os_error(errno.EACCES, pathname_name) if self.filesystem.has_open_file(entry): raise_error = True diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index 108c751b..b41b331d 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -2559,9 +2559,10 @@ def create_symlink( # resolve the link path only if it is not a link itself if not self.islink(link_path): link_path = self.resolve_path(link_path) + permission = helpers.PERM_DEF_FILE if self.is_windows_fs else helpers.PERM_DEF return self.create_file_internally( link_path, - st_mode=S_IFLNK | helpers.PERM_DEF, + st_mode=S_IFLNK | permission, contents=link_target_path, create_missing_dirs=create_missing_dirs, apply_umask=self.is_macos, @@ -3045,6 +3046,19 @@ def listdir(self, target_directory: AnyStr) -> List[AnyStr]: def __str__(self) -> str: return str(self.root_dir) + if sys.version_info >= (3, 13): + + def isreserved(self, path): + path = os.fsdecode(self.splitroot(path)[2]).replace( + self.alternative_path_separator, self.path_separator + ) + from os.path import _isreservedname + + return any( + _isreservedname(name) + for name in reversed(path.split(self.path_separator)) + ) + def _add_standard_streams(self) -> None: self.add_open_file(StandardStreamWrapper(sys.stdin)) self.add_open_file(StandardStreamWrapper(sys.stdout)) diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py index 45202a14..a9b76dd2 100644 --- a/pyfakefs/fake_filesystem_unittest.py +++ b/pyfakefs/fake_filesystem_unittest.py @@ -40,6 +40,7 @@ import doctest import functools import genericpath +import glob import inspect import io import linecache @@ -589,6 +590,9 @@ def __init__( self.modules_to_reload: List[ModuleType] = ( [] if sys.platform == "win32" else [tempfile] ) + if sys.version_info >= (3, 13): + # need to reload glob which holds references to os functions + self.modules_to_reload.append(glob) if modules_to_reload is not None: self.modules_to_reload.extend(modules_to_reload) self.patch_default_args = patch_default_args @@ -685,6 +689,11 @@ def _init_fake_module_classes(self) -> None: "io": fake_io.FakeIoModule, "pathlib": fake_pathlib.FakePathlibModule, } + if sys.version_info >= (3, 13): + # for Python 3.13, we need both pathlib (path with __init__.py) and + # pathlib._local (has the actual implementation); + # depending on how pathlib is imported, either may be used + self._fake_module_classes["pathlib._local"] = fake_pathlib.FakePathlibModule if IS_PYPY or sys.version_info >= (3, 12): # in PyPy and later cpython versions, the module is referenced as _io self._fake_module_classes["_io"] = fake_io.FakeIoModule2 @@ -697,7 +706,13 @@ def _init_fake_module_classes(self) -> None: # be contained in - this allows for alternative modules like # `pathlib` and `pathlib2` self._class_modules["Path"] = ["pathlib"] + if sys.version_info >= (3, 13): + self._class_modules["Path"].append("pathlib._local") self._unfaked_module_classes["pathlib"] = fake_pathlib.RealPathlibModule + if sys.version_info >= (3, 13): + self._unfaked_module_classes["pathlib._local"] = ( + fake_pathlib.RealPathlibModule + ) if pathlib2: self._fake_module_classes["pathlib2"] = ( fake_legacy_modules.FakePathlib2Module diff --git a/pyfakefs/fake_open.py b/pyfakefs/fake_open.py index 72a7ae04..aa52db05 100644 --- a/pyfakefs/fake_open.py +++ b/pyfakefs/fake_open.py @@ -67,6 +67,19 @@ "x+": (False, True, True, False, False, True), } +real_call_line_no = None + + +def _real_call_line_no(): + global real_call_line_no + if real_call_line_no is None: + fake_io_source = os.path.join(os.path.dirname(__file__), "fake_io.py") + for i, line in enumerate(io.open(fake_io_source)): + if "return self._io_module.open_code(path)" in line: + real_call_line_no = i + 1 + break + return real_call_line_no + def fake_open( filesystem: "FakeFilesystem", @@ -88,15 +101,37 @@ def fake_open( # specific modules; instead we check if the caller is a skipped # module (should work in most cases) stack = traceback.extract_stack(limit=3) + # handle the case that we try to call the original `open_code` # and get here instead (since Python 3.12) - from_open_code = ( - sys.version_info >= (3, 12) - and stack[0].name == "open_code" - and stack[0].line == "return self._io_module.open_code(path)" - ) + # TODO: use a more generic approach (see other PR #1025) + if sys.version_info >= (3, 13): + # TODO: check if stacktrace line is still not filled in final version + from_open_code = ( + stack[0].name == "open_code" and stack[0].lineno == _real_call_line_no() + ) + elif sys.version_info >= (3, 12): + from_open_code = ( + stack[0].name == "open_code" + and stack[0].line == "return self._io_module.open_code(path)" + ) + else: + from_open_code = False + module_name = os.path.splitext(stack[0].filename)[0] module_name = module_name.replace(os.sep, ".") + if sys.version_info >= (3, 13) and module_name.endswith( + ("pathlib._abc", "pathlib._local") + ): + stack = traceback.extract_stack(limit=6) + frame = 2 + # in Python 3.13, pathlib is implemented in 2 sub-modules that may call + # each other, so we have to look further in the stack + while frame >= 0 and module_name.endswith(("pathlib._abc", "pathlib._local")): + module_name = os.path.splitext(stack[frame].filename)[0] + module_name = module_name.replace(os.sep, ".") + frame -= 1 + if from_open_code or any( [module_name == sn or module_name.endswith("." + sn) for sn in skip_names] ): @@ -201,7 +236,10 @@ def call( # the pathlib opener is defined in a Path instance that may not be # patched under some circumstances; as it just calls standard open(), # we may ignore it, as it would not change the behavior - if opener is not None and opener.__module__ != "pathlib": + if opener is not None and opener.__module__ not in ( + "pathlib", + "pathlib._local", + ): # opener shall return a file descriptor, which will be handled # here as if directly passed file_ = opener(file_, self._open_flags_from_open_modes(open_modes)) diff --git a/pyfakefs/fake_path.py b/pyfakefs/fake_path.py index 5135f746..920022d2 100644 --- a/pyfakefs/fake_path.py +++ b/pyfakefs/fake_path.py @@ -165,10 +165,19 @@ def getsize(self, path: AnyStr): def isabs(self, path: AnyStr) -> bool: """Return True if path is an absolute pathname.""" + empty = matching_string(path, "") if self.filesystem.is_windows_fs: - path = self.splitdrive(path)[1] + drive, path = self.splitdrive(path) + else: + drive = empty path = make_string_path(path) - return self.filesystem.starts_with_sep(path) + if not self.filesystem.starts_with_sep(path): + return False + if self.filesystem.is_windows_fs and sys.version_info >= (3, 13): + # from Python 3.13 on, a path under Windows starting with a single separator + # (e.g. not a drive and not an UNC path) is no more considered absolute + return drive != empty + return True def isdir(self, path: AnyStr) -> bool: """Determine if path identifies a directory.""" @@ -204,6 +213,11 @@ def splitroot(self, path: AnyStr): """ return self.filesystem.splitroot(path) + if sys.version_info >= (3, 13): + + def isreserved(self, path): + return self.filesystem.isreserved(path) + def getmtime(self, path: AnyStr) -> float: """Returns the modification time of the fake file. diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index 0fdaa928..2e03255c 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -32,6 +32,7 @@ import errno import fnmatch import functools +import glob import inspect import ntpath import os @@ -39,6 +40,7 @@ import posixpath import re import sys +import warnings from pathlib import PurePath from typing import Callable, List from urllib.parse import quote_from_bytes as urlquote_from_bytes @@ -70,23 +72,25 @@ def init_module(filesystem): fake_pure_nt_flavour.altsep = "/" FakePathlibModule.PureWindowsPath._flavour = fake_pure_nt_flavour else: - # in Python 3.12, the flavour is no longer an own class, + # in Python > 3.11, the flavour is no longer a separate class, # but points to the os-specific path module (posixpath/ntpath) fake_os = FakeOsModule(filesystem) - FakePathlibModule.PosixPath._flavour = fake_os.path - FakePathlibModule.WindowsPath._flavour = fake_os.path + parser_name = "_flavour" if sys.version_info < (3, 13) else "parser" + + setattr(FakePathlibModule.PosixPath, parser_name, fake_os.path) + setattr(FakePathlibModule.WindowsPath, parser_name, fake_os.path) # Pure POSIX path separators must be filesystem independent. fake_pure_posix_os = FakeOsModule(filesystem) fake_pure_posix_os.path.sep = "/" fake_pure_posix_os.path.altsep = None - FakePathlibModule.PurePosixPath._flavour = fake_pure_posix_os.path + setattr(FakePathlibModule.PurePosixPath, parser_name, fake_pure_posix_os.path) # Pure Windows path separators must be filesystem independent. fake_pure_nt_os = FakeOsModule(filesystem) fake_pure_nt_os.path.sep = "\\" fake_pure_nt_os.path.altsep = "/" - FakePathlibModule.PureWindowsPath._flavour = fake_pure_nt_os.path + setattr(FakePathlibModule.PureWindowsPath, parser_name, fake_pure_nt_os.path) def _wrap_strfunc(strfunc): @@ -578,6 +582,13 @@ def _init(self, template=None): # only needed until Python 3.8 self._closed = False + if sys.version_info >= (3, 13): + + def _glob_selector(self, parts, case_sensitive, recurse_symlinks): + # make sure we get the patched version of the globber + self._globber = glob._StringGlobber + return super()._glob_selector(parts, case_sensitive, recurse_symlinks) + def _path(self): """Returns the underlying path string as used by the fake filesystem. @@ -805,13 +816,21 @@ def is_absolute(self): 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. + if not self.filesystem.is_windows_fs: return False - name = self._tail[-1].partition(".")[0].partition(":")[0].rstrip(" ") - return name.upper() in pathlib._WIN_RESERVED_NAMES + if sys.version_info < (3, 13): + if not self._tail or 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 + warnings.warn( + "pathlib.PurePath.is_reserved() is deprecated and scheduled " + "for removal in Python 3.15. Use os.path.isreserved() to detect " + "reserved paths on Windows.", + DeprecationWarning, + ) + return self.filesystem.isreserved(self._path()) class FakePathlibModule: @@ -837,11 +856,15 @@ class PurePosixPath(PurePath): paths""" __slots__ = () + if sys.version_info >= (3, 13): + parser = posixpath class PureWindowsPath(PurePath): """A subclass of PurePath, that represents Windows filesystem paths""" __slots__ = () + if sys.version_info >= (3, 13): + parser = ntpath class WindowsPath(FakePath, PureWindowsPath): """A subclass of Path and PureWindowsPath that represents @@ -933,8 +956,10 @@ class RealPath(pathlib.Path): if os.name == "nt" else pathlib._PosixFlavour() # type:ignore ) # type:ignore - else: + elif sys.version_info < (3, 13): _flavour = ntpath if os.name == "nt" else posixpath + else: + parser = ntpath if os.name == "nt" else posixpath def __new__(cls, *args, **kwargs): """Creates the correct subclass based on OS.""" diff --git a/pyfakefs/fake_scandir.py b/pyfakefs/fake_scandir.py index b9f7690f..3b9da314 100644 --- a/pyfakefs/fake_scandir.py +++ b/pyfakefs/fake_scandir.py @@ -182,6 +182,7 @@ def scandir(filesystem, path=""): Raises: OSError: if the target is not a directory. """ + print(f"fake scandir({path})") return ScanDirIter(filesystem, path) diff --git a/pyfakefs/helpers.py b/pyfakefs/helpers.py index 4852e370..cf84936d 100644 --- a/pyfakefs/helpers.py +++ b/pyfakefs/helpers.py @@ -12,6 +12,7 @@ """Helper classes use for fake file system implementation.""" +import ctypes import io import locale import os @@ -44,8 +45,9 @@ ) if sys.platform == "win32": - USER_ID = 1 - GROUP_ID = 1 + fake_id = 0 if ctypes.windll.shell32.IsUserAnAdmin() else 1 + USER_ID = fake_id + GROUP_ID = fake_id else: USER_ID = os.getuid() GROUP_ID = os.getgid() @@ -87,8 +89,9 @@ def set_gid(gid: int) -> None: def reset_ids() -> None: """Set the global user ID and group ID back to default values.""" if sys.platform == "win32": - set_uid(1) - set_gid(1) + reset_id = 0 if ctypes.windll.shell32.IsUserAnAdmin() else 1 + set_uid(reset_id) + set_gid(reset_id) else: set_uid(os.getuid()) set_gid(os.getgid()) diff --git a/pyfakefs/pytest_tests/pytest_reload_pandas_test.py b/pyfakefs/pytest_tests/pytest_reload_pandas_test.py index 4f85ab75..bb2c49fb 100644 --- a/pyfakefs/pytest_tests/pytest_reload_pandas_test.py +++ b/pyfakefs/pytest_tests/pytest_reload_pandas_test.py @@ -4,23 +4,31 @@ from pathlib import Path -import pandas as pd import pytest +try: + import pandas as pd +except ImportError: + pd = None + try: import parquet except ImportError: parquet = None -@pytest.mark.skipif(parquet is None, reason="parquet not installed") +@pytest.mark.skipif( + pd is None or parquet is None, reason="pandas or parquet not installed" +) def test_1(fs): dir_ = Path(__file__).parent / "data" fs.add_real_directory(dir_) pd.read_parquet(dir_ / "test.parquet") -@pytest.mark.skipif(parquet is None, reason="parquet not installed") +@pytest.mark.skipif( + pd is None or parquet is None, reason="pandas or parquet not installed" +) def test_2(): dir_ = Path(__file__).parent / "data" pd.read_parquet(dir_ / "test.parquet") diff --git a/pyfakefs/tests/fake_filesystem_test.py b/pyfakefs/tests/fake_filesystem_test.py index 1588208a..4af657c4 100644 --- a/pyfakefs/tests/fake_filesystem_test.py +++ b/pyfakefs/tests/fake_filesystem_test.py @@ -583,8 +583,9 @@ def test_create_file(self): new_file = self.filesystem.get_object(path) self.assertEqual(os.path.basename(path), new_file.name) if IS_WIN: - self.assertEqual(1, new_file.st_uid) - self.assertEqual(1, new_file.st_gid) + fake_id = 0 if is_root() else 1 + self.assertEqual(fake_id, new_file.st_uid) + self.assertEqual(fake_id, new_file.st_gid) else: self.assertEqual(os.getuid(), new_file.st_uid) self.assertEqual(os.getgid(), new_file.st_gid) @@ -955,8 +956,12 @@ def test_isabs_with_drive_component(self): self.filesystem.is_windows_fs = True self.assertTrue(self.path.isabs("C:!foo")) self.assertTrue(self.path.isabs(b"C:!foo")) - self.assertTrue(self.path.isabs("!")) - self.assertTrue(self.path.isabs(b"!")) + if sys.version_info < (3, 13): + self.assertTrue(self.path.isabs("!")) + self.assertTrue(self.path.isabs(b"!")) + else: + self.assertFalse(self.path.isabs("!")) + self.assertFalse(self.path.isabs(b"!")) def test_relpath(self): path_foo = "!path!to!foo" @@ -2123,7 +2128,7 @@ def test_cannot_overwrite_symlink_with_dir(self): self.filesystem.add_real_directory(root_dir, target_path="/root/") def test_symlink_is_merged(self): - self.skip_if_symlink_not_supported(force_real_fs=True) + self.skip_if_symlink_not_supported() self.filesystem.create_dir(os.path.join("/", "root", "foo")) with self.create_real_paths() as root_dir: link_path = os.path.join(root_dir, "link.txt") @@ -2388,7 +2393,7 @@ def test_add_existing_real_directory_symlink(self): ) def test_add_existing_real_directory_symlink_target_path(self): - self.skip_if_symlink_not_supported(force_real_fs=True) + self.skip_if_symlink_not_supported() real_directory = self._setup_temp_directory() symlinks = [ ( @@ -2413,7 +2418,7 @@ def test_add_existing_real_directory_symlink_target_path(self): self.assertTrue(self.filesystem.exists("/path/fixtures/symlink_file_relative")) def test_add_existing_real_directory_symlink_lazy_read(self): - self.skip_if_symlink_not_supported(force_real_fs=True) + self.skip_if_symlink_not_supported() real_directory = self._setup_temp_directory() symlinks = [ ( diff --git a/pyfakefs/tests/fake_os_test.py b/pyfakefs/tests/fake_os_test.py index b17ddb86..736cfb18 100644 --- a/pyfakefs/tests/fake_os_test.py +++ b/pyfakefs/tests/fake_os_test.py @@ -898,6 +898,7 @@ def test_remove_file_no_directory(self): def test_remove_file_with_read_permission_raises_in_windows(self): self.check_windows_only() + self.skip_root() path = self.make_path("foo", "bar") self.create_file(path) self.os.chmod(path, 0o444) @@ -2024,8 +2025,11 @@ def test_access_symlink(self): self.assertTrue(self.os.access(link_path, self.os.F_OK, follow_symlinks=False)) self.assertTrue(self.os.access(link_path, self.os.R_OK, follow_symlinks=False)) self.assertTrue(self.os.access(link_path, self.os.W_OK, follow_symlinks=False)) - self.assertTrue(self.os.access(link_path, self.os.X_OK, follow_symlinks=False)) - self.assertTrue(self.os.access(link_path, self.rwx, follow_symlinks=False)) + if not self.is_windows_fs: + self.assertTrue( + self.os.access(link_path, self.os.X_OK, follow_symlinks=False) + ) + self.assertTrue(self.os.access(link_path, self.rwx, follow_symlinks=False)) self.assertTrue(self.os.access(link_path, self.rw, follow_symlinks=False)) def test_access_non_existent_file(self): @@ -2086,7 +2090,10 @@ def test_chmod_follow_symlink(self): self.assertEqual(stat.S_IMODE(0o700), stat.S_IMODE(st.st_mode) & 0o700) def test_chmod_no_follow_symlink(self): - self.check_posix_only() + if sys.version_info < (3, 13): + self.check_posix_only() + else: + self.skip_if_symlink_not_supported() path = self.make_path("some_file") self.createTestFile(path) link_path = self.make_path("link_to_some_file") @@ -2100,7 +2107,8 @@ def test_chmod_no_follow_symlink(self): mode = 0o644 if self.is_macos else 0o666 self.assert_mode_equal(mode, st.st_mode) st = self.os.stat(link_path, follow_symlinks=False) - self.assert_mode_equal(0o6543, st.st_mode) + mode = 0o444 if self.is_windows_fs else 0o6543 + self.assert_mode_equal(mode, st.st_mode) def test_lchmod(self): """lchmod shall behave like chmod with follow_symlinks=True.""" @@ -5668,7 +5676,7 @@ def test_listdir_user_readable_dir_from_other_user(self): with self.assertRaises(PermissionError): self.os.listdir(dir_path) else: - self.assertEqual(["some_file"], self.os.listdir(self.dir_path)) + self.assertEqual([], self.os.listdir(dir_path)) def test_listdir_group_readable_dir_from_other_user(self): self.skip_real_fs() # won't change user in real fs diff --git a/pyfakefs/tests/fake_pathlib_test.py b/pyfakefs/tests/fake_pathlib_test.py index f4cc2104..e119bfff 100644 --- a/pyfakefs/tests/fake_pathlib_test.py +++ b/pyfakefs/tests/fake_pathlib_test.py @@ -404,9 +404,11 @@ def test_lchmod(self): self.path(self.file_link_path).lchmod(0o444) else: self.path(self.file_link_path).lchmod(0o444) - self.assertEqual(file_stat.st_mode, stat.S_IFREG | 0o644) + mode = 0o666 if is_windows else 0o644 + self.assertEqual(file_stat.st_mode, stat.S_IFREG | mode) # the exact mode depends on OS and Python version - self.assertEqual(link_stat.st_mode & 0o777700, stat.S_IFLNK | 0o700) + mode_mask = 0o600 if self.is_windows_fs else 0o700 + self.assertEqual(link_stat.st_mode & 0o777700, stat.S_IFLNK | mode_mask) @unittest.skipIf( sys.version_info < (3, 10), @@ -421,9 +423,11 @@ def test_chmod_no_followsymlinks(self): self.path(self.file_link_path).chmod(0o444, follow_symlinks=False) else: self.path(self.file_link_path).chmod(0o444, follow_symlinks=False) - self.assertEqual(file_stat.st_mode, stat.S_IFREG | 0o644) + mode = 0o666 if is_windows else 0o644 + self.assertEqual(file_stat.st_mode, stat.S_IFREG | mode) # the exact mode depends on OS and Python version - self.assertEqual(link_stat.st_mode & 0o777700, stat.S_IFLNK | 0o700) + mode_mask = 0o600 if self.is_windows_fs else 0o700 + self.assertEqual(link_stat.st_mode & 0o777700, stat.S_IFLNK | mode_mask) def test_resolve(self): self.create_dir(self.make_path("antoine", "docs")) @@ -456,10 +460,14 @@ def test_iterdir_in_unreadable_dir(self): file_path = self.os.path.join(dir_path, "some_file") self.create_file(file_path) self.os.chmod(dir_path, 0o000) - it = self.path(dir_path).iterdir() if not is_root(): - self.assert_raises_os_error(errno.EACCES, list, it) + if sys.version_info >= (3, 13): + self.assert_raises_os_error(errno.EACCES, self.path(dir_path).iterdir) + else: + it = self.path(dir_path).iterdir() + self.assert_raises_os_error(errno.EACCES, list, it) else: + it = self.path(dir_path).iterdir() path = str(list(it)[0]) self.assertTrue(path.endswith("some_file")) @@ -727,7 +735,6 @@ def test_link_to(self): @unittest.skipIf(sys.version_info < (3, 10), "hardlink_to new in Python 3.10") def test_hardlink_to(self): - self.skip_if_symlink_not_supported() file_name = self.make_path("foo", "bar.txt") self.create_file(file_name) self.assertEqual(1, self.os.stat(file_name).st_nlink) @@ -1280,6 +1287,8 @@ def setUp(self) -> None: @unittest.skipIf(sys.platform != "win32", "Windows specific test") def test_is_file_for_unreadable_dir_windows(self): self.fs.os = OSType.WINDOWS + if is_root(): + self.skipTest("Test only valid for non-root user") path = pathlib.Path("/foo/bar") self.fs.create_file(path) # normal chmod does not really set the mode to 0 diff --git a/pyfakefs/tests/import_as_example.py b/pyfakefs/tests/import_as_example.py index 835a2c4d..131c3e87 100644 --- a/pyfakefs/tests/import_as_example.py +++ b/pyfakefs/tests/import_as_example.py @@ -59,6 +59,7 @@ def check_if_exists6(filepath): def check_if_exists7(filepath): # tests patching pathlib + print(f"{pathlib.Path=}") return pathlib.Path(filepath).exists() diff --git a/pyfakefs/tests/test_utils.py b/pyfakefs/tests/test_utils.py index e28fe5a3..a5b35c60 100644 --- a/pyfakefs/tests/test_utils.py +++ b/pyfakefs/tests/test_utils.py @@ -269,32 +269,18 @@ def skip_real_fs_failure( "Skipping because FakeFS does not match real FS" ) - def symlink_can_be_tested(self, force_real_fs=False): + def symlink_can_be_tested(self): """Used to check if symlinks and hard links can be tested under Windows. All tests are skipped under Windows for Python versions not supporting links, and real tests are skipped if running without administrator rights. """ - if not TestCase.is_windows or (not force_real_fs and not self.use_real_fs()): - return True - if TestCase.symlinks_can_be_tested is None: - if force_real_fs: - self.base_path = tempfile.mkdtemp() - link_path = self.make_path("link") - try: - self.os.symlink(self.base_path, link_path) - TestCase.symlinks_can_be_tested = True - self.os.remove(link_path) - except (OSError, NotImplementedError): - TestCase.symlinks_can_be_tested = False - if force_real_fs: - self.base_path = None - return TestCase.symlinks_can_be_tested - - def skip_if_symlink_not_supported(self, force_real_fs=False): + return not TestCase.is_windows or is_root() + + def skip_if_symlink_not_supported(self): """If called at test start, tests are skipped if symlinks are not supported.""" - if not self.symlink_can_be_tested(force_real_fs): + if not self.symlink_can_be_tested(): raise unittest.SkipTest("Symlinks under Windows need admin privileges") def make_path(self, *args):