Skip to content

Commit da7bd5d

Browse files
committed
Use OS-specific delimiters for fake Windows/PosixPath
- fixes the behavior for non-current OS - fixes #1055
1 parent a02d2b4 commit da7bd5d

File tree

8 files changed

+253
-156
lines changed

8 files changed

+253
-156
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ The released versions correspond to PyPI releases.
2121

2222
### Fixes
2323
* removing files while iterating over `scandir` results is now possible (see [#1051](../../issues/1051))
24+
* fake `pathlib.PosixPath` and `pathlib.WindowsPath` now behave more like in the real filesystem
25+
(see [#1055](../../issues/1055))
2426

2527
## [Version 5.6.0](https://pypi.python.org/pypi/pyfakefs/5.6.0) (2024-07-12)
2628
Adds preliminary Python 3.13 support.

pyfakefs/fake_file.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,8 +419,7 @@ def has_permission(self, permission_bits: int) -> bool:
419419

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

425424
@property
426425
def byte_contents(self) -> bytes:

pyfakefs/fake_filesystem.py

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@
8181
True
8282
"""
8383

84+
import contextlib
85+
import dataclasses
8486
import errno
8587
import heapq
8688
import os
@@ -101,7 +103,6 @@
101103
)
102104
from typing import (
103105
List,
104-
Optional,
105106
Callable,
106107
Union,
107108
Any,
@@ -111,6 +112,7 @@
111112
AnyStr,
112113
overload,
113114
NoReturn,
115+
Optional,
114116
)
115117

116118
from pyfakefs import fake_file, fake_path, fake_io, fake_os, helpers, fake_open
@@ -122,6 +124,9 @@
122124
matching_string,
123125
AnyPath,
124126
AnyString,
127+
WINDOWS_PROPERTIES,
128+
POSIX_PROPERTIES,
129+
FSType,
125130
)
126131

127132
if sys.platform.startswith("linux"):
@@ -179,10 +184,6 @@ class FakeFilesystem:
179184
"""Provides the appearance of a real directory tree for unit testing.
180185
181186
Attributes:
182-
path_separator: The path separator, corresponds to `os.path.sep`.
183-
alternative_path_separator: Corresponds to `os.path.altsep`.
184-
is_windows_fs: `True` in a real or faked Windows file system.
185-
is_macos: `True` under MacOS, or if we are faking it.
186187
is_case_sensitive: `True` if a case-sensitive file system is assumed.
187188
root: The root :py:class:`FakeDirectory<pyfakefs.fake_file.FakeDirectory>` entry
188189
of the file system.
@@ -217,12 +218,8 @@ def __init__(
217218
>>> filesystem = FakeFilesystem(path_separator='/')
218219
219220
"""
220-
self.path_separator: str = path_separator
221-
self.alternative_path_separator: Optional[str] = os.path.altsep
222221
self.patcher = patcher
223222
self.create_temp_dir = create_temp_dir
224-
if path_separator != os.sep:
225-
self.alternative_path_separator = None
226223

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

236233
# is_case_sensitive can be used to test pyfakefs for case-sensitive
237234
# file systems on non-case-sensitive systems and vice verse
238-
self.is_case_sensitive: bool = not (self.is_windows_fs or self._is_macos)
235+
self.is_case_sensitive: bool = not (self._is_windows_fs or self._is_macos)
236+
237+
# by default, we use the configured filesystem
238+
self.fs_type = FSType.DEFAULT
239+
base_properties = (
240+
WINDOWS_PROPERTIES if self._is_windows_fs else POSIX_PROPERTIES
241+
)
242+
self.fs_properties = [
243+
dataclasses.replace(base_properties),
244+
POSIX_PROPERTIES,
245+
WINDOWS_PROPERTIES,
246+
]
247+
self.path_separator = path_separator
239248

240249
self.root: FakeDirectory
241250
self._cwd = ""
@@ -262,30 +271,71 @@ def __init__(
262271

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

267277
@property
268278
def is_windows_fs(self) -> bool:
269-
return self._is_windows_fs
279+
"""Returns `True` in a real or faked Windows file system."""
280+
return self.fs_type == FSType.WINDOWS or (
281+
self.fs_type == FSType.DEFAULT and self._is_windows_fs
282+
)
270283

271284
@is_windows_fs.setter
272285
def is_windows_fs(self, value: bool) -> None:
273286
if self._is_windows_fs != value:
274287
self._is_windows_fs = value
288+
if value:
289+
self._is_macos = False
275290
self.reset()
276291
FakePathModule.reset(self)
277292

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

282298
@is_macos.setter
283299
def is_macos(self, value: bool) -> None:
284300
if self._is_macos != value:
285301
self._is_macos = value
302+
if value:
303+
self._is_windows_fs = False
286304
self.reset()
287305
FakePathModule.reset(self)
288306

307+
@property
308+
def path_separator(self) -> str:
309+
"""Returns the path separator, corresponds to `os.path.sep`."""
310+
return self.fs_properties[self.fs_type.value].sep
311+
312+
@path_separator.setter
313+
def path_separator(self, value: str) -> None:
314+
self.fs_properties[0].sep = value
315+
if value != os.sep:
316+
self.alternative_path_separator = None
317+
318+
@property
319+
def alternative_path_separator(self) -> Optional[str]:
320+
"""Returns the alternative path separator, corresponds to `os.path.altsep`."""
321+
return self.fs_properties[self.fs_type.value].altsep
322+
323+
@alternative_path_separator.setter
324+
def alternative_path_separator(self, value: Optional[str]) -> None:
325+
self.fs_properties[0].altsep = value
326+
327+
@property
328+
def devnull(self) -> str:
329+
return self.fs_properties[self.fs_type.value].devnull
330+
331+
@property
332+
def pathsep(self) -> str:
333+
return self.fs_properties[self.fs_type.value].pathsep
334+
335+
@property
336+
def line_separator(self) -> str:
337+
return self.fs_properties[self.fs_type.value].linesep
338+
289339
@property
290340
def cwd(self) -> str:
291341
"""Return the current working directory of the fake filesystem."""
@@ -334,8 +384,11 @@ def os(self, value: OSType) -> None:
334384
self._is_windows_fs = value == OSType.WINDOWS
335385
self._is_macos = value == OSType.MACOS
336386
self.is_case_sensitive = value == OSType.LINUX
337-
self.path_separator = "\\" if value == OSType.WINDOWS else "/"
338-
self.alternative_path_separator = "/" if value == OSType.WINDOWS else None
387+
self.fs_type = FSType.DEFAULT
388+
base_properties = (
389+
WINDOWS_PROPERTIES if self._is_windows_fs else POSIX_PROPERTIES
390+
)
391+
self.fs_properties[0] = base_properties
339392
self.reset()
340393
FakePathModule.reset(self)
341394

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

359412
fake_pathlib.init_module(self)
360413

414+
@contextlib.contextmanager
415+
def use_fs_type(self, fs_type: FSType):
416+
old_fs_type = self.fs_type
417+
try:
418+
self.fs_type = fs_type
419+
yield
420+
finally:
421+
self.fs_type = old_fs_type
422+
361423
def _add_root_mount_point(self, total_size):
362424
mount_point = "C:" if self.is_windows_fs else self.path_separator
363425
self._cwd = mount_point
@@ -403,9 +465,6 @@ def clear_cache(self) -> None:
403465
if self.patcher:
404466
self.patcher.clear_cache()
405467

406-
def line_separator(self) -> str:
407-
return "\r\n" if self.is_windows_fs else "\n"
408-
409468
def raise_os_error(
410469
self,
411470
err_no: int,
@@ -1144,8 +1203,9 @@ def splitroot(self, path: AnyStr):
11441203
if isinstance(p, bytes):
11451204
sep = self.path_separator.encode()
11461205
altsep = None
1147-
if self.alternative_path_separator:
1148-
altsep = self.alternative_path_separator.encode()
1206+
alternative_path_separator = self.alternative_path_separator
1207+
if alternative_path_separator is not None:
1208+
altsep = alternative_path_separator.encode()
11491209
colon = b":"
11501210
unc_prefix = b"\\\\?\\UNC\\"
11511211
empty = b""
@@ -1292,7 +1352,11 @@ def _path_components(self, path: AnyStr) -> List[AnyStr]:
12921352
if not path or path == self.get_path_separator(path):
12931353
return []
12941354
drive, path = self.splitdrive(path)
1295-
path_components = path.split(self.get_path_separator(path))
1355+
sep = self.get_path_separator(path)
1356+
# handle special case of Windows emulated under POSIX
1357+
if self.is_windows_fs and sys.platform != "win32":
1358+
path = path.replace(matching_string(sep, "\\"), sep)
1359+
path_components = path.split(sep)
12961360
assert drive or path_components
12971361
if not path_components[0]:
12981362
if len(path_components) > 1 and not path_components[1]:
@@ -1438,7 +1502,7 @@ def exists(self, file_path: AnyPath, check_link: bool = False) -> bool:
14381502
raise TypeError
14391503
if not path:
14401504
return False
1441-
if path == self.dev_null.name:
1505+
if path == self.devnull:
14421506
return not self.is_windows_fs or sys.version_info >= (3, 8)
14431507
try:
14441508
if self.is_filepath_ending_with_separator(path):
@@ -1515,7 +1579,7 @@ def resolve_path(self, file_path: AnyStr, allow_fd: bool = False) -> AnyStr:
15151579
path = self.replace_windows_root(path)
15161580
if self._is_root_path(path):
15171581
return path
1518-
if path == matching_string(path, self.dev_null.name):
1582+
if path == matching_string(path, self.devnull):
15191583
return path
15201584
path_components = self._path_components(path)
15211585
resolved_components = self._resolve_components(path_components)
@@ -1661,7 +1725,7 @@ def get_object_from_normpath(
16611725
path = make_string_path(file_path)
16621726
if path == matching_string(path, self.root.name):
16631727
return self.root
1664-
if path == matching_string(path, self.dev_null.name):
1728+
if path == matching_string(path, self.devnull):
16651729
return self.dev_null
16661730

16671731
path = self._original_path(path)

pyfakefs/fake_path.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ def __init__(self, filesystem: "FakeFilesystem", os_module: "FakeOsModule"):
123123
def reset(cls, filesystem: "FakeFilesystem") -> None:
124124
cls.sep = filesystem.path_separator
125125
cls.altsep = filesystem.alternative_path_separator
126-
cls.linesep = filesystem.line_separator()
127-
cls.devnull = "nul" if filesystem.is_windows_fs else "/dev/null"
128-
cls.pathsep = ";" if filesystem.is_windows_fs else ":"
126+
cls.linesep = filesystem.line_separator
127+
cls.devnull = filesystem.devnull
128+
cls.pathsep = filesystem.pathsep
129129

130130
def exists(self, path: AnyStr) -> bool:
131131
"""Determine whether the file object exists within the fake filesystem.

0 commit comments

Comments
 (0)