diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index 1aa5e484..d1918faf 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -3127,6 +3127,9 @@ def __str__(self) -> str: | {f"COM{c}" for c in "123456789\xb9\xb2\xb3"} | {f"LPT{c}" for c in "123456789\xb9\xb2\xb3"} ) + _WIN_RESERVED_CHARS = frozenset( + {chr(i) for i in range(32)} | {'"', "*", ":", "<", ">", "?", "|", "/", "\\"} + ) def isreserved(self, path): if not self.is_windows_fs: @@ -3137,6 +3140,12 @@ def is_reserved_name(name): from os.path import _isreservedname # type: ignore[import-error] return _isreservedname(name) + + if name[-1:] in (".", " "): + return name not in (".", "..") + if self._WIN_RESERVED_CHARS.intersection(name): + return True + name = name.partition(".")[0].rstrip(" ").upper() return name in self._WIN_RESERVED_NAMES path = os.fsdecode(self.splitroot(path)[2]) diff --git a/pyfakefs/fake_open.py b/pyfakefs/fake_open.py index 46cb1e99..04c521d6 100644 --- a/pyfakefs/fake_open.py +++ b/pyfakefs/fake_open.py @@ -89,6 +89,7 @@ def fake_open( if is_called_from_skipped_module( skip_names=skip_names, case_sensitive=filesystem.is_case_sensitive, + check_open_code=sys.version_info >= (3, 12), ): return io_open( # pytype: disable=wrong-arg-count file, diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index b9a1e773..3cbe0313 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -52,6 +52,13 @@ from pyfakefs.helpers import IS_PYPY, is_called_from_skipped_module, FSType +_WIN_RESERVED_NAMES = ( + {"CON", "PRN", "AUX", "NUL"} + | {"COM%d" % i for i in range(1, 10)} + | {"LPT%d" % i for i in range(1, 10)} +) + + def init_module(filesystem): """Initializes the fake module with the fake file system.""" # pylint: disable=protected-access @@ -433,11 +440,6 @@ class _FakeWindowsFlavour(_FakeFlavour): 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)} - ) sep = "\\" altsep = "/" has_drv = True @@ -455,7 +457,7 @@ def is_reserved(self, parts): 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 + return parts[-1].partition(".")[0].upper() in _WIN_RESERVED_NAMES def make_uri(self, path): """Return a file URI for the given path""" @@ -848,33 +850,15 @@ 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.drive and self.root - return os.path.isabs(self._path()) - - def is_reserved(self): - 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 - 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()) +def _warn_is_reserved_deprecated(): + 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, + ) class FakePathlibModule: @@ -900,12 +884,43 @@ class PurePosixPath(PurePath): paths""" __slots__ = () + if sys.version_info >= (3, 12): + + def is_reserved(self): + _warn_is_reserved_deprecated() + return False + + def is_absolute(self): + with os.path.filesystem.use_fs_type(FSType.POSIX): + return os.path.isabs(self) class PureWindowsPath(PurePath): """A subclass of PurePath, that represents Windows filesystem paths""" __slots__ = () + if sys.version_info >= (3, 12): + """These are reimplemented because the PurePath implementation + checks the flavour against ntpath/posixpath. + """ + + def is_reserved(self): + _warn_is_reserved_deprecated() + 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 _WIN_RESERVED_NAMES + with os.path.filesystem.use_fs_type(FSType.WINDOWS): + return os.path.isreserved(self) + + def is_absolute(self): + with os.path.filesystem.use_fs_type(FSType.WINDOWS): + return bool(self.drive and self.root) + class WindowsPath(FakePath, PureWindowsPath): """A subclass of Path and PureWindowsPath that represents concrete Windows filesystem paths. diff --git a/pyfakefs/helpers.py b/pyfakefs/helpers.py index 652969dd..6db8d96b 100644 --- a/pyfakefs/helpers.py +++ b/pyfakefs/helpers.py @@ -478,12 +478,19 @@ def putvalue(self, value: bytes) -> None: self._bytestream.write(value) -def is_called_from_skipped_module(skip_names: list, case_sensitive: bool) -> bool: +def is_called_from_skipped_module( + skip_names: list, case_sensitive: bool, check_open_code: bool = False +) -> bool: def starts_with(path, string): if case_sensitive: return path.startswith(string) return path.lower().startswith(string.lower()) + # in most cases we don't have skip names and won't need the overhead + # of analyzing the traceback, except when checking for open_code + if not skip_names and not check_open_code: + return False + stack = traceback.extract_stack() # handle the case that we try to call the original `open_code` @@ -494,12 +501,15 @@ def starts_with(path, string): # -3: fake_io.open: 'return fake_open(' # -4: fake_io.open_code : 'return self._io_module.open_code(path)' if ( - sys.version_info >= (3, 12) + check_open_code and stack[-4].name == "open_code" and stack[-4].line == "return self._io_module.open_code(path)" ): return True + if not skip_names: + return False + caller_filename = next( ( frame.filename