Skip to content

Commit

Permalink
Patch is_reserved for Windows/PosixPurePath
Browse files Browse the repository at this point in the history
- had been only patched for pathlib.Path
- avoid possible recursion in PurePath.joinpath
- fixes #1067
  • Loading branch information
mrbean-bremen committed Oct 8, 2024
1 parent 24808e4 commit ed752e0
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 36 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ The released versions correspond to PyPI releases.
* removing files while iterating over `scandir` results is now possible (see [#1051](../../issues/1051))
* fake `pathlib.PosixPath` and `pathlib.WindowsPath` now behave more like in the real filesystem
(see [#1053](../../issues/1053))
* `PurePosixPath` reported Windows reserved names as reserved in Python >= 3.12
(see [#1067](../../issues/1067))

## [Version 5.6.0](https://pypi.python.org/pypi/pyfakefs/5.6.0) (2024-07-12)
Adds preliminary Python 3.13 support.
Expand Down
17 changes: 17 additions & 0 deletions pyfakefs/fake_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,14 @@ def starts_with_drive_letter(self, file_path: AnyStr) -> bool:
# check if the path exists because it has been mapped in
# this is not foolproof, but handles most cases
try:
if len(file_path) == 2:
# avoid recursion, check directly in the entries
return any(
[
entry.upper() == file_path.upper()
for entry in self.root_dir.entries
]
)
self.get_object_from_normpath(file_path)
return True
except OSError:
Expand Down Expand Up @@ -3127,6 +3135,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:
Expand All @@ -3137,6 +3148,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])
Expand Down
83 changes: 51 additions & 32 deletions pyfakefs/fake_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"""
Expand Down Expand Up @@ -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:
Expand All @@ -900,12 +884,47 @@ 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): # type: ignore[module-attr]
return os.path.isabs(self)

def joinpath(self, *pathsegments):
with os.path.filesystem.use_fs_type(FSType.POSIX): # type: ignore[module-attr]
return super().joinpath(*pathsegments)

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): # type: ignore[module-attr]
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.
Expand Down
11 changes: 7 additions & 4 deletions pyfakefs/tests/fake_pathlib_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,13 @@ def test_joinpath(self):
self.assertEqual(
self.path("/foo").joinpath("bar", "baz"), self.path("/foo/bar/baz")
)
self.assertEqual(
self.path("c:").joinpath("/Program Files"),
self.path("/Program Files"),
)
if os.name != "nt":
# under Windows, this does not work correctly at the moment
# we get "C:/Program Files" instead
self.assertEqual(
self.path("c:").joinpath("/Program Files"),
self.path("/Program Files"),
)

def test_match(self):
self.assertTrue(self.path("a/b.py").match("*.py"))
Expand Down

0 comments on commit ed752e0

Please sign in to comment.