Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix some problems with pathlib PosixPath/WindowsPath behavior #1055

Merged
merged 2 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ The released versions correspond to PyPI releases.

### Fixes
* 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 [#1055](../../issues/1055))

## [Version 5.6.0](https://pypi.python.org/pypi/pyfakefs/5.6.0) (2024-07-12)
Adds preliminary Python 3.13 support.
Expand Down
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
108 changes: 86 additions & 22 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 All @@ -101,7 +103,6 @@
)
from typing import (
List,
Optional,
Callable,
Union,
Any,
Expand All @@ -111,6 +112,7 @@
AnyStr,
overload,
NoReturn,
Optional,
)

from pyfakefs import fake_file, fake_path, fake_io, fake_os, helpers, fake_open
Expand All @@ -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,30 +271,71 @@ 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
mrbean-bremen marked this conversation as resolved.
Show resolved Hide resolved
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
def is_macos(self, value: bool) -> None:
if self._is_macos != value:
self._is_macos = value
if value:
self._is_windows_fs = False
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):
old_fs_type = self.fs_type
try:
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 @@ -1144,8 +1203,9 @@ def splitroot(self, path: AnyStr):
if isinstance(p, bytes):
sep = self.path_separator.encode()
altsep = None
if self.alternative_path_separator:
altsep = self.alternative_path_separator.encode()
alternative_path_separator = self.alternative_path_separator
if alternative_path_separator is not None:
altsep = alternative_path_separator.encode()
colon = b":"
unc_prefix = b"\\\\?\\UNC\\"
empty = b""
Expand Down Expand Up @@ -1292,7 +1352,11 @@ def _path_components(self, path: AnyStr) -> List[AnyStr]:
if not path or path == self.get_path_separator(path):
return []
drive, path = self.splitdrive(path)
path_components = path.split(self.get_path_separator(path))
sep = self.get_path_separator(path)
# handle special case of Windows emulated under POSIX
if self.is_windows_fs and sys.platform != "win32":
path = path.replace(matching_string(sep, "\\"), sep)
path_components = path.split(sep)
assert drive or path_components
if not path_components[0]:
if len(path_components) > 1 and not path_components[1]:
Expand Down Expand Up @@ -1438,7 +1502,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 +1579,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 +1725,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
Loading