Skip to content

Commit

Permalink
Use OS-specific delimiters for fake Windows/PosixPath
Browse files Browse the repository at this point in the history
- fixes the behavior of the Path for non-current OS
  • Loading branch information
mrbean-bremen committed Sep 7, 2024
1 parent e0d2ed8 commit bdb63cc
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 151 deletions.
3 changes: 1 addition & 2 deletions pyfakefs/fake_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,8 +419,7 @@ def has_permission(self, permission_bits: int) -> bool:

class FakeNullFile(FakeFile):
def __init__(self, filesystem: "FakeFilesystem") -> None:
devnull = "nul" if filesystem.is_windows_fs else "/dev/null"
super().__init__(devnull, filesystem=filesystem, contents="")
super().__init__(filesystem.devnull, filesystem=filesystem, contents="")

@property
def byte_contents(self) -> bytes:
Expand Down
95 changes: 77 additions & 18 deletions pyfakefs/fake_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
True
"""

import contextlib
import dataclasses
import errno
import heapq
import os
Expand Down Expand Up @@ -122,6 +124,9 @@
matching_string,
AnyPath,
AnyString,
WINDOWS_PROPERTIES,
POSIX_PROPERTIES,
FSType,
)

if sys.platform.startswith("linux"):
Expand Down Expand Up @@ -179,10 +184,6 @@ class FakeFilesystem:
"""Provides the appearance of a real directory tree for unit testing.
Attributes:
path_separator: The path separator, corresponds to `os.path.sep`.
alternative_path_separator: Corresponds to `os.path.altsep`.
is_windows_fs: `True` in a real or faked Windows file system.
is_macos: `True` under MacOS, or if we are faking it.
is_case_sensitive: `True` if a case-sensitive file system is assumed.
root: The root :py:class:`FakeDirectory<pyfakefs.fake_file.FakeDirectory>` entry
of the file system.
Expand Down Expand Up @@ -217,12 +218,8 @@ def __init__(
>>> filesystem = FakeFilesystem(path_separator='/')
"""
self.path_separator: str = path_separator
self.alternative_path_separator: Optional[str] = os.path.altsep
self.patcher = patcher
self.create_temp_dir = create_temp_dir
if path_separator != os.sep:
self.alternative_path_separator = None

# is_windows_fs can be used to test the behavior of pyfakefs under
# Windows fs on non-Windows systems and vice verse;
Expand All @@ -235,7 +232,19 @@ def __init__(

# is_case_sensitive can be used to test pyfakefs for case-sensitive
# file systems on non-case-sensitive systems and vice verse
self.is_case_sensitive: bool = not (self.is_windows_fs or self._is_macos)
self.is_case_sensitive: bool = not (self._is_windows_fs or self._is_macos)

# by default, we use the configured filesystem
self.fs_type = FSType.DEFAULT
base_properties = (
WINDOWS_PROPERTIES if self._is_windows_fs else POSIX_PROPERTIES
)
self.fs_properties = [
dataclasses.replace(base_properties),
POSIX_PROPERTIES,
WINDOWS_PROPERTIES,
]
self.path_separator = path_separator

self.root: FakeDirectory
self._cwd = ""
Expand All @@ -262,21 +271,30 @@ def __init__(

@property
def is_linux(self) -> bool:
"""Returns `True` in a real or faked Linux file system."""
return not self.is_windows_fs and not self.is_macos

@property
def is_windows_fs(self) -> bool:
return self._is_windows_fs
"""Returns `True` in a real or faked Windows file system."""
return (
self.fs_type == FSType.WINDOWS
or self.fs_type == FSType.DEFAULT
and self._is_windows_fs
)

@is_windows_fs.setter
def is_windows_fs(self, value: bool) -> None:
if self._is_windows_fs != value:
self._is_windows_fs = value
if value:
self._is_macos = False
self.reset()
FakePathModule.reset(self)

@property
def is_macos(self) -> bool:
"""Returns `True` in a real or faked macOS file system."""
return self._is_macos

@is_macos.setter
Expand All @@ -286,6 +304,38 @@ def is_macos(self, value: bool) -> None:
self.reset()
FakePathModule.reset(self)

@property
def path_separator(self) -> str:
"""Returns the path separator, corresponds to `os.path.sep`."""
return self.fs_properties[self.fs_type.value].sep

@path_separator.setter
def path_separator(self, value: str) -> None:
self.fs_properties[0].sep = value
if value != os.sep:
self.alternative_path_separator = None

@property
def alternative_path_separator(self) -> Optional[str]:
"""Returns the alternative path separator, corresponds to `os.path.altsep`."""
return self.fs_properties[self.fs_type.value].altsep

@alternative_path_separator.setter
def alternative_path_separator(self, value: Optional[str]) -> None:
self.fs_properties[0].altsep = value

@property
def devnull(self) -> str:
return self.fs_properties[self.fs_type.value].devnull

@property
def pathsep(self) -> str:
return self.fs_properties[self.fs_type.value].pathsep

@property
def line_separator(self) -> str:
return self.fs_properties[self.fs_type.value].linesep

@property
def cwd(self) -> str:
"""Return the current working directory of the fake filesystem."""
Expand Down Expand Up @@ -334,8 +384,11 @@ def os(self, value: OSType) -> None:
self._is_windows_fs = value == OSType.WINDOWS
self._is_macos = value == OSType.MACOS
self.is_case_sensitive = value == OSType.LINUX
self.path_separator = "\\" if value == OSType.WINDOWS else "/"
self.alternative_path_separator = "/" if value == OSType.WINDOWS else None
self.fs_type = FSType.DEFAULT
base_properties = (
WINDOWS_PROPERTIES if self._is_windows_fs else POSIX_PROPERTIES
)
self.fs_properties[0] = base_properties
self.reset()
FakePathModule.reset(self)

Expand All @@ -358,6 +411,15 @@ def reset(self, total_size: Optional[int] = None, init_pathlib: bool = True):

fake_pathlib.init_module(self)

@contextlib.contextmanager
def use_fs_type(self, fs_type: FSType):
try:
old_fs_type = self.fs_type
self.fs_type = fs_type
yield
finally:
self.fs_type = old_fs_type

def _add_root_mount_point(self, total_size):
mount_point = "C:" if self.is_windows_fs else self.path_separator
self._cwd = mount_point
Expand Down Expand Up @@ -403,9 +465,6 @@ def clear_cache(self) -> None:
if self.patcher:
self.patcher.clear_cache()

def line_separator(self) -> str:
return "\r\n" if self.is_windows_fs else "\n"

def raise_os_error(
self,
err_no: int,
Expand Down Expand Up @@ -1438,7 +1497,7 @@ def exists(self, file_path: AnyPath, check_link: bool = False) -> bool:
raise TypeError
if not path:
return False
if path == self.dev_null.name:
if path == self.devnull:
return not self.is_windows_fs or sys.version_info >= (3, 8)
try:
if self.is_filepath_ending_with_separator(path):
Expand Down Expand Up @@ -1515,7 +1574,7 @@ def resolve_path(self, file_path: AnyStr, allow_fd: bool = False) -> AnyStr:
path = self.replace_windows_root(path)
if self._is_root_path(path):
return path
if path == matching_string(path, self.dev_null.name):
if path == matching_string(path, self.devnull):
return path
path_components = self._path_components(path)
resolved_components = self._resolve_components(path_components)
Expand Down Expand Up @@ -1661,7 +1720,7 @@ def get_object_from_normpath(
path = make_string_path(file_path)
if path == matching_string(path, self.root.name):
return self.root
if path == matching_string(path, self.dev_null.name):
if path == matching_string(path, self.devnull):
return self.dev_null

path = self._original_path(path)
Expand Down
6 changes: 3 additions & 3 deletions pyfakefs/fake_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ def __init__(self, filesystem: "FakeFilesystem", os_module: "FakeOsModule"):
def reset(cls, filesystem: "FakeFilesystem") -> None:
cls.sep = filesystem.path_separator
cls.altsep = filesystem.alternative_path_separator
cls.linesep = filesystem.line_separator()
cls.devnull = "nul" if filesystem.is_windows_fs else "/dev/null"
cls.pathsep = ";" if filesystem.is_windows_fs else ":"
cls.linesep = filesystem.line_separator
cls.devnull = filesystem.devnull
cls.pathsep = filesystem.pathsep

def exists(self, path: AnyStr) -> bool:
"""Determine whether the file object exists within the fake filesystem.
Expand Down
90 changes: 59 additions & 31 deletions pyfakefs/fake_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
from pyfakefs.fake_filesystem import FakeFilesystem
from pyfakefs.fake_open import fake_open
from pyfakefs.fake_os import FakeOsModule, use_original_os
from pyfakefs.helpers import IS_PYPY, is_called_from_skipped_module
from pyfakefs.fake_path import FakePathModule
from pyfakefs.helpers import IS_PYPY, is_called_from_skipped_module, FSType


def init_module(filesystem):
Expand All @@ -67,30 +68,27 @@ def init_module(filesystem):
FakePathlibModule.PurePosixPath._flavour = fake_pure_posix_flavour

# Pure Windows path separators must be filesystem-independent.
fake_pure_nt_flavour = _FakePosixFlavour(filesystem)
fake_pure_nt_flavour = _FakeWindowsFlavour(filesystem)
fake_pure_nt_flavour.sep = "\\"
fake_pure_nt_flavour.altsep = "/"
FakePathlibModule.PureWindowsPath._flavour = fake_pure_nt_flavour
else:
# 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)
parser_name = "_flavour" if sys.version_info < (3, 13) else "parser"
fake_os_posix = FakeOsModule(filesystem)
if filesystem.is_windows_fs:
fake_os_posix.path = FakePosixPathModule(filesystem, fake_os_posix)
fake_os_windows = FakeOsModule(filesystem)
if not filesystem.is_windows_fs:
fake_os_windows.path = FakeWindowsPathModule(filesystem, fake_os_windows)

setattr(FakePathlibModule.PosixPath, parser_name, fake_os.path)
setattr(FakePathlibModule.WindowsPath, parser_name, fake_os.path)
parser_name = "_flavour" if sys.version_info < (3, 13) else "parser"

# 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
setattr(FakePathlibModule.PurePosixPath, parser_name, fake_pure_posix_os.path)
# Pure POSIX path properties must be filesystem independent.
setattr(FakePathlibModule.PurePosixPath, parser_name, fake_os_posix.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 = "/"
setattr(FakePathlibModule.PureWindowsPath, parser_name, fake_pure_nt_os.path)
# Pure Windows path properties must be filesystem independent.
setattr(FakePathlibModule.PureWindowsPath, parser_name, fake_os_windows.path)


def _wrap_strfunc(fake_fct, original_fct):
Expand Down Expand Up @@ -242,9 +240,6 @@ class _FakeFlavour(flavour): # type: ignore[valid-type, misc]
"""Fake Flavour implementation used by PurePath and _Flavour"""

filesystem = None
sep = "/"
altsep = None
has_drv = False

ext_namespace_prefix = "\\\\?\\"

Expand All @@ -254,9 +249,6 @@ class _FakeFlavour(flavour): # type: ignore[valid-type, misc]

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().__init__()

@staticmethod
Expand Down Expand Up @@ -320,9 +312,13 @@ def _splitroot_posix(path, sep):

def splitroot(self, path, sep=None):
"""Split path into drive, root and rest."""
is_windows = isinstance(self, _FakeWindowsFlavour)
if sep is None:
sep = self.filesystem.path_separator
if self.filesystem.is_windows_fs:
if is_windows == self.filesystem.is_windows_fs:
sep = self.filesystem.path_separator
else:
sep = self.sep
if is_windows:
return self._splitroot_with_drive(path, sep)
return self._splitroot_posix(path, sep)

Expand Down Expand Up @@ -443,6 +439,9 @@ class _FakeWindowsFlavour(_FakeFlavour):
| {"COM%d" % i for i in range(1, 10)}
| {"LPT%d" % i for i in range(1, 10)}
)
sep = "\\"
altsep = "/"
has_drv = True
pathmod = ntpath

def is_reserved(self, parts):
Expand Down Expand Up @@ -519,6 +518,9 @@ class _FakePosixFlavour(_FakeFlavour):
independent of FakeFilesystem properties.
"""

sep = "/"
altsep = ""
has_drv = False
pathmod = posixpath

def is_reserved(self, parts):
Expand Down Expand Up @@ -551,6 +553,36 @@ def gethomedir(self, username):

def compile_pattern(self, pattern):
return re.compile(fnmatch.translate(pattern)).fullmatch
else: # Python >= 3.12

class FakePosixPathModule(FakePathModule):
def __init__(self, filesystem: "FakeFilesystem", os_module: "FakeOsModule"):
super().__init__(filesystem, os_module)
with self.filesystem.use_fs_type(FSType.POSIX):
self.reset(self.filesystem)

class FakeWindowsPathModule(FakePathModule):
def __init__(self, filesystem: "FakeFilesystem", os_module: "FakeOsModule"):
super().__init__(filesystem, os_module)
with self.filesystem.use_fs_type(FSType.WINDOWS):
self.reset(self.filesystem)

def with_fs_type(f: Callable, fs_type: FSType) -> Callable:
"""Decorator used for fake_path methods to ensure that
the correct filesystem type is used."""

@functools.wraps(f)
def wrapped(self, *args, **kwargs):
with self.filesystem.use_fs_type(fs_type):
return f(self, *args, **kwargs)

return wrapped

# decorate all public functions to use the correct fs type
for fct_name in FakePathModule.dir():
fn = getattr(FakePathModule, fct_name)
setattr(FakeWindowsPathModule, fct_name, with_fs_type(fn, FSType.WINDOWS))
setattr(FakePosixPathModule, fct_name, with_fs_type(fn, FSType.POSIX))


class FakePath(pathlib.Path):
Expand Down Expand Up @@ -878,15 +910,11 @@ 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
Expand Down Expand Up @@ -1010,9 +1038,9 @@ def wrapped(*args, **kwargs):

return wrapped

for name, fn in inspect.getmembers(RealPath, inspect.isfunction):
if not name.startswith("__"):
setattr(RealPath, name, with_original_os(fn))
for fct_name, fn in inspect.getmembers(RealPath, inspect.isfunction):
if not fct_name.startswith("__"):
setattr(RealPath, fct_name, with_original_os(fn))


class RealPathlibPathModule:
Expand Down
Loading

0 comments on commit bdb63cc

Please sign in to comment.