diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index aad89bfd..53845d41 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 @@ -89,12 +92,16 @@ jobs: run: | pip install -r extra_requirements.txt pip install -r legacy_requirements.txt - pip install zstandard cffi # needed to test #910 + if [[ '${{ matrix.python-version }}' != '3.13-dev' ]]; then + pip install zstandard cffi # needed to test #910 + fi shell: bash - name: Run unit tests with extra packages as non-root user if: ${{ matrix.python-version != 'pypy-3.10' }} run: | - export PYTHON_ZSTANDARD_IMPORT_POLICY=cffi # needed to test #910 + if [[ '${{ matrix.python-version }}' != '3.13-dev' ]]; then + export PYTHON_ZSTANDARD_IMPORT_POLICY=cffi # needed to test #910 + fi python -m pyfakefs.tests.all_tests shell: bash - name: Run performance tests 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/legacy_requirements.txt b/legacy_requirements.txt index dd00d2a3..2f75c6e7 100644 --- a/legacy_requirements.txt +++ b/legacy_requirements.txt @@ -5,4 +5,4 @@ # Note that the usage of these modules is deprecated, and their support # will be removed in pyfakefs 6.0 pathlib2>=2.3.2 -scandir>=1.8 +scandir>=1.8; python_version < '3.13' # not (yet) available for Python 3.13 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..0f1cfa7a 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,32 @@ def listdir(self, target_directory: AnyStr) -> List[AnyStr]: def __str__(self) -> str: return str(self.root_dir) + if sys.version_info >= (3, 13): + # used for emulating Windows + _WIN_RESERVED_NAMES = frozenset( + {"CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$"} + | {f"COM{c}" for c in "123456789\xb9\xb2\xb3"} + | {f"LPT{c}" for c in "123456789\xb9\xb2\xb3"} + ) + + def isreserved(self, path): + def is_reserved_name(name): + if sys.platform == "win32": + return _isreservedname(name) + return name in self._WIN_RESERVED_NAMES + + path = os.fsdecode(self.splitroot(path)[2]) + if self.alternative_path_separator is not None: + path = path.replace( + self.alternative_path_separator, self.path_separator + ) + from os.path import _isreservedname # type: ignore[import-error] + + return any( + is_reserved_name(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..69d7dc07 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", @@ -83,20 +96,62 @@ def fake_open( """Redirect the call to FakeFileOpen. See FakeFileOpen.call() for description. """ + # since Python 3.13, we can run into a recursion in some instances here: + # traceback calls linecache.update_cache, which loads 'os' dynamically, + # which will be patched by the dynamic patcher and ends up here again; + # for these instances, we use a shortcut check here + if ( + isinstance(file, str) + and file.endswith(("fake_open.py", "fake_io.py")) + and os.path.split(os.path.dirname(file))[1] == "pyfakefs" + ): + return io.open( # pytype: disable=wrong-arg-count + file, + mode, + buffering, + encoding, + errors, + newline, + closefd, + opener, + ) + # workaround for built-in open called from skipped modules (see #552) # as open is not imported explicitly, we cannot patch it for # 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 +256,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..9c5d786c 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,14 @@ def splitroot(self, path: AnyStr): """ return self.filesystem.splitroot(path) + if sys.version_info >= (3, 13): + + def isreserved(self, path): + if not self.filesystem.is_windows_fs: + raise AttributeError("module 'os' has no attribute 'isreserved'") + + 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..8f79ae01 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,15 @@ 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 # type: ignore[module-attr] + return super()._glob_selector( # type: ignore[attribute-error] + parts, case_sensitive, recurse_symlinks + ) + def _path(self): """Returns the underlying path string as used by the fake filesystem. @@ -805,13 +818,22 @@ 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 sys.version_info >= (3, 13): + 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, + ) + 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 + return self.filesystem.isreserved(self._path()) class FakePathlibModule: @@ -837,11 +859,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 +959,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/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_glob_test.py b/pyfakefs/tests/fake_filesystem_glob_test.py index 1ca141f0..19dd142e 100644 --- a/pyfakefs/tests/fake_filesystem_glob_test.py +++ b/pyfakefs/tests/fake_filesystem_glob_test.py @@ -14,8 +14,10 @@ """Test for glob using fake_filesystem.""" +import contextlib import glob import os +import sys import unittest from pyfakefs import fake_filesystem_unittest @@ -71,7 +73,12 @@ def test_magic_dir(self): self.assertEqual(["/[Temp]"], glob.glob("/*emp*")) def test_glob1(self): - self.assertEqual(["[Temp]"], glob.glob1("/", "*Tem*")) + with ( + contextlib.nullcontext() + if sys.version_info < (3, 13) + else self.assertWarns(DeprecationWarning) + ): + self.assertEqual(["[Temp]"], glob.glob1("/", "*Tem*")) def test_has_magic(self): self.assertTrue(glob.has_magic("[")) diff --git a/pyfakefs/tests/fake_filesystem_test.py b/pyfakefs/tests/fake_filesystem_test.py index 1588208a..7c9e1a56 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" @@ -1248,6 +1253,22 @@ def test_splitroot_posix(self): ("", "!!", "foo!!bar"), self.filesystem.splitroot("!!foo!!bar") ) + @unittest.skipIf(sys.version_info < (3, 13), "Introduced in Python 3.13") + @unittest.skipIf(TestCase.is_windows, "Posix specific behavior") + def test_is_reserved_posix(self): + self.assertFalse(self.filesystem.isreserved("!dev")) + self.assertFalse(self.filesystem.isreserved("!")) + self.assertFalse(self.filesystem.isreserved("COM1")) + self.assertFalse(self.filesystem.isreserved("nul.txt")) + + @unittest.skipIf(sys.version_info < (3, 13), "Introduced in Python 3.13") + @unittest.skipIf(not TestCase.is_windows, "Windows specific behavior") + def test_is_reserved_windows(self): + self.assertFalse(self.filesystem.isreserved("!dev")) + self.assertFalse(self.filesystem.isreserved("!")) + self.assertTrue(self.filesystem.isreserved("COM1")) + self.assertTrue(self.filesystem.isreserved("nul.txt")) + class PathManipulationTestBase(TestCase): def setUp(self): @@ -2123,7 +2144,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 +2409,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 +2434,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..cce7faa5 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.""" @@ -5048,6 +5056,8 @@ def os_mknod(): os_mknod() self.add_supported_function(self.os.mknod) if self.os.mknod in self.os.supports_dir_fd: + if self.is_macos and sys.version_info >= (3, 13) and not is_root(): + self.skipTest("Needs root rights under macos") os_mknod() newdir_path = self.os.path.join(self.dir_fd_path, "newdir") self.assertTrue(self.os.path.exists(newdir_path)) @@ -5668,7 +5678,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..0581631b 100644 --- a/pyfakefs/tests/fake_pathlib_test.py +++ b/pyfakefs/tests/fake_pathlib_test.py @@ -19,6 +19,7 @@ python docs. """ +import contextlib import errno import os import pathlib @@ -214,18 +215,28 @@ class FakePathlibPurePathTest(RealPathlibTestCase): 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()) - self.assertFalse(self.path("nul.txt").is_reserved()) + with ( + contextlib.nullcontext() + if sys.version_info < (3, 13) + else self.assertWarns(DeprecationWarning) + ): + self.assertFalse(self.path("/dev").is_reserved()) + self.assertFalse(self.path("/").is_reserved()) + self.assertFalse(self.path("COM1").is_reserved()) + self.assertFalse(self.path("nul.txt").is_reserved()) @unittest.skipIf(not is_windows, "Windows specific behavior") def test_is_reserved_windows(self): self.check_windows_only() - self.assertFalse(self.path("/dev").is_reserved()) - self.assertFalse(self.path("/").is_reserved()) - self.assertTrue(self.path("COM1").is_reserved()) - self.assertTrue(self.path("nul.txt").is_reserved()) + with ( + contextlib.nullcontext() + if sys.version_info < (3, 13) + else self.assertWarns(DeprecationWarning) + ): + self.assertFalse(self.path("/dev").is_reserved()) + self.assertFalse(self.path("/").is_reserved()) + self.assertTrue(self.path("COM1").is_reserved()) + self.assertTrue(self.path("nul.txt").is_reserved()) def test_joinpath(self): self.assertEqual(self.path("/etc").joinpath("passwd"), self.path("/etc/passwd")) @@ -404,9 +415,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 +434,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 +471,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 +746,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 +1298,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/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):