Skip to content

Commit 4b6aecd

Browse files
committed
Fix handling for other Python versions
- fix wrapped functions in pathlib (accessor not available in most Python versions) - fix open_code handling - make module path comparisons case-insensitive under Windows and macOS
1 parent 39fdac8 commit 4b6aecd

File tree

7 files changed

+101
-85
lines changed

7 files changed

+101
-85
lines changed

pyfakefs/fake_filesystem_unittest.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ def __init__(
570570
set_uid(1)
571571
set_gid(1)
572572

573-
self._skip_names = self.SKIPNAMES.copy()
573+
self.skip_names = self.SKIPNAMES.copy()
574574
# save the original open function for use in pytest plugin
575575
self.original_open = open
576576
self.patch_open_code = patch_open_code
@@ -582,7 +582,7 @@ def __init__(
582582
cast(ModuleType, m).__name__ if inspect.ismodule(m) else cast(str, m)
583583
for m in additional_skip_names
584584
]
585-
self._skip_names.update(skip_names)
585+
self.skip_names.update(skip_names)
586586

587587
self._fake_module_classes: Dict[str, Any] = {}
588588
self._unfaked_module_classes: Dict[str, Any] = {}
@@ -628,8 +628,8 @@ def __init__(
628628
if patched_module_names != self.PATCHED_MODULE_NAMES:
629629
self.__class__.PATCHED_MODULE_NAMES = patched_module_names
630630
clear_cache = True
631-
if self._skip_names != self.ADDITIONAL_SKIP_NAMES:
632-
self.__class__.ADDITIONAL_SKIP_NAMES = self._skip_names
631+
if self.skip_names != self.ADDITIONAL_SKIP_NAMES:
632+
self.__class__.ADDITIONAL_SKIP_NAMES = self.skip_names
633633
clear_cache = True
634634
if patch_default_args != self.PATCH_DEFAULT_ARGS:
635635
self.__class__.PATCH_DEFAULT_ARGS = patch_default_args
@@ -875,7 +875,7 @@ def _find_modules(self) -> None:
875875
pass
876876
continue
877877
skipped = module in self.SKIPMODULES or any(
878-
[sn.startswith(module.__name__) for sn in self._skip_names]
878+
[sn.startswith(module.__name__) for sn in self.skip_names]
879879
)
880880
module_items = module.__dict__.copy().items()
881881

@@ -922,7 +922,7 @@ def _refresh(self) -> None:
922922
for name in self._fake_module_classes:
923923
self.fake_modules[name] = self._fake_module_classes[name](self.fs)
924924
if hasattr(self.fake_modules[name], "skip_names"):
925-
self.fake_modules[name].skip_names = self._skip_names
925+
self.fake_modules[name].skip_names = self.skip_names
926926
self.fake_modules[PATH_MODULE] = self.fake_modules["os"].path
927927
for name in self._unfaked_module_classes:
928928
self.unfaked_modules[name] = self._unfaked_module_classes[name]()

pyfakefs/fake_io.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,14 @@ def lockf(
182182

183183
def __getattribute__(self, name):
184184
"""Forwards any unfaked calls to the standard fcntl module."""
185-
filesystem = object.__getattribute__(self, "filesystem")
185+
fs: FakeFilesystem = object.__getattribute__(self, "filesystem")
186186
fnctl_module = object.__getattribute__(self, "_fcntl_module")
187-
if filesystem.patcher:
188-
skip_names = filesystem.patcher._skip_names
189-
if is_called_from_skipped_module(skip_names=skip_names):
187+
if fs.patcher:
188+
skip_names = fs.patcher.skip_names
189+
if is_called_from_skipped_module(
190+
skip_names=skip_names,
191+
case_sensitive=fs.is_case_sensitive,
192+
):
190193
# remove the `self` argument for FakeOsModule methods
191194
return getattr(fnctl_module, name)
192195

pyfakefs/fake_open.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ def fake_open(
8686
"""Redirect the call to FakeFileOpen.
8787
See FakeFileOpen.call() for description.
8888
"""
89-
if is_called_from_skipped_module(skip_names=skip_names):
89+
if is_called_from_skipped_module(
90+
skip_names=skip_names,
91+
case_sensitive=filesystem.is_case_sensitive,
92+
):
9093
return io_open( # pytype: disable=wrong-arg-count
9194
file,
9295
mode,

pyfakefs/fake_os.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,9 +1421,13 @@ def wrapped(*args, **kwargs):
14211421

14221422
if not should_use_original and args:
14231423
self = args[0]
1424+
fs: FakeFilesystem = self.filesystem
14241425
if self.filesystem.patcher:
1425-
skip_names = self.filesystem.patcher._skip_names
1426-
if is_called_from_skipped_module(skip_names=skip_names):
1426+
skip_names = fs.patcher.skip_names
1427+
if is_called_from_skipped_module(
1428+
skip_names=skip_names,
1429+
case_sensitive=fs.is_case_sensitive,
1430+
):
14271431
should_use_original = True
14281432

14291433
if should_use_original:

pyfakefs/fake_path.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,8 +570,11 @@ def wrapped(*args, **kwargs):
570570
self = args[0]
571571
should_use_original = self.os.use_original
572572
if not should_use_original and self.filesystem.patcher:
573-
skip_names = self.filesystem.patcher._skip_names
574-
if is_called_from_skipped_module(skip_names=skip_names):
573+
skip_names = self.filesystem.patcher.skip_names
574+
if is_called_from_skipped_module(
575+
skip_names=skip_names,
576+
case_sensitive=self.filesystem.is_case_sensitive,
577+
):
575578
should_use_original = True
576579

577580
if should_use_original:

pyfakefs/fake_pathlib.py

Lines changed: 50 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -93,62 +93,47 @@ def init_module(filesystem):
9393
setattr(FakePathlibModule.PureWindowsPath, parser_name, fake_pure_nt_os.path)
9494

9595

96-
def _wrap_strfunc(strfunc):
97-
@functools.wraps(strfunc)
96+
def _wrap_strfunc(fake_fct, original_fct):
97+
@functools.wraps(fake_fct)
9898
def _wrapped(pathobj, *args, **kwargs):
99-
should_use_original = False
100-
if pathobj.filesystem.patcher:
101-
skip_names = pathobj.filesystem.patcher._skip_names
102-
if is_called_from_skipped_module(skip_names=skip_names):
103-
should_use_original = True
104-
105-
if should_use_original:
106-
return getattr(pathobj._original_accessor, strfunc.__name__)(
107-
str(pathobj),
108-
*args,
109-
**kwargs,
110-
)
111-
return strfunc(pathobj.filesystem, str(pathobj), *args, **kwargs)
99+
fs: FakeFilesystem = pathobj.filesystem
100+
if fs.patcher:
101+
if is_called_from_skipped_module(
102+
skip_names=fs.patcher.skip_names,
103+
case_sensitive=fs.is_case_sensitive,
104+
):
105+
return original_fct(str(pathobj), *args, **kwargs)
106+
return fake_fct(fs, str(pathobj), *args, **kwargs)
112107

113108
return staticmethod(_wrapped)
114109

115110

116-
def _wrap_binary_strfunc(strfunc):
117-
@functools.wraps(strfunc)
111+
def _wrap_binary_strfunc(fake_fct, original_fct):
112+
@functools.wraps(fake_fct)
118113
def _wrapped(pathobj1, pathobj2, *args):
119-
should_use_original = False
120-
if pathobj1.filesystem.patcher:
121-
skip_names = pathobj1.filesystem.patcher._skip_names
122-
if is_called_from_skipped_module(skip_names=skip_names):
123-
should_use_original = True
124-
125-
if should_use_original:
126-
return getattr(pathobj1._original_accessor, strfunc.__name__)(
127-
str(pathobj1),
128-
str(pathobj2),
129-
*args,
130-
)
131-
return strfunc(pathobj1.filesystem, str(pathobj1), str(pathobj2), *args)
114+
fs: FakeFilesystem = pathobj1.filesystem
115+
if fs.patcher:
116+
if is_called_from_skipped_module(
117+
skip_names=fs.patcher.skip_names,
118+
case_sensitive=fs.is_case_sensitive,
119+
):
120+
return original_fct(str(pathobj1), str(pathobj2), *args)
121+
return fake_fct(fs, str(pathobj1), str(pathobj2), *args)
132122

133123
return staticmethod(_wrapped)
134124

135125

136-
def _wrap_binary_strfunc_reverse(strfunc):
137-
@functools.wraps(strfunc)
126+
def _wrap_binary_strfunc_reverse(fake_fct, original_fct):
127+
@functools.wraps(fake_fct)
138128
def _wrapped(pathobj1, pathobj2, *args):
139-
should_use_original = False
140-
if pathobj2.filesystem.patcher:
141-
skip_names = pathobj2.filesystem.patcher._skip_names
142-
if is_called_from_skipped_module(skip_names=skip_names):
143-
should_use_original = True
144-
145-
if should_use_original:
146-
return getattr(pathobj2._original_accessor, strfunc.__name__)(
147-
str(pathobj2),
148-
str(pathobj1),
149-
*args,
150-
)
151-
return strfunc(pathobj2.filesystem, str(pathobj2), str(pathobj1), *args)
129+
fs: FakeFilesystem = pathobj2.filesystem
130+
if fs.patcher:
131+
if is_called_from_skipped_module(
132+
skip_names=fs.patcher.skip_names,
133+
case_sensitive=fs.is_case_sensitive,
134+
):
135+
return original_fct(str(pathobj2), str(pathobj1), *args)
136+
return fake_fct(fs, str(pathobj2), str(pathobj1), *args)
152137

153138
return staticmethod(_wrapped)
154139

@@ -164,20 +149,21 @@ class _FakeAccessor(accessor): # type: ignore[valid-type, misc]
164149
methods.
165150
"""
166151

167-
stat = _wrap_strfunc(FakeFilesystem.stat)
152+
stat = _wrap_strfunc(FakeFilesystem.stat, os.stat)
168153

169154
lstat = _wrap_strfunc(
170-
lambda fs, path: FakeFilesystem.stat(fs, path, follow_symlinks=False)
155+
lambda fs, path: FakeFilesystem.stat(fs, path, follow_symlinks=False), os.lstat
171156
)
172157

173-
listdir = _wrap_strfunc(FakeFilesystem.listdir)
174-
scandir = _wrap_strfunc(fake_scandir.scandir)
158+
listdir = _wrap_strfunc(FakeFilesystem.listdir, os.listdir)
159+
scandir = _wrap_strfunc(fake_scandir.scandir, os.scandir)
175160

176161
if hasattr(os, "lchmod"):
177162
lchmod = _wrap_strfunc(
178163
lambda fs, path, mode: FakeFilesystem.chmod(
179164
fs, path, mode, follow_symlinks=False
180-
)
165+
),
166+
os.lchmod,
181167
)
182168
else:
183169

@@ -200,47 +186,51 @@ def chmod(self, pathobj, *args, **kwargs):
200186
)
201187
return pathobj.filesystem.chmod(str(pathobj), *args, **kwargs)
202188

203-
mkdir = _wrap_strfunc(FakeFilesystem.makedir)
189+
mkdir = _wrap_strfunc(FakeFilesystem.makedir, os.mkdir)
204190

205-
unlink = _wrap_strfunc(FakeFilesystem.remove)
191+
unlink = _wrap_strfunc(FakeFilesystem.remove, os.unlink)
206192

207-
rmdir = _wrap_strfunc(FakeFilesystem.rmdir)
193+
rmdir = _wrap_strfunc(FakeFilesystem.rmdir, os.rmdir)
208194

209-
rename = _wrap_binary_strfunc(FakeFilesystem.rename)
195+
rename = _wrap_binary_strfunc(FakeFilesystem.rename, os.rename)
210196

211197
replace = _wrap_binary_strfunc(
212198
lambda fs, old_path, new_path: FakeFilesystem.rename(
213199
fs, old_path, new_path, force_replace=True
214-
)
200+
),
201+
os.replace,
215202
)
216203

217204
symlink = _wrap_binary_strfunc_reverse(
218205
lambda fs, fpath, target, target_is_dir: FakeFilesystem.create_symlink(
219206
fs, fpath, target, create_missing_dirs=False
220-
)
207+
),
208+
os.symlink,
221209
)
222210

223211
if (3, 8) <= sys.version_info:
224212
link_to = _wrap_binary_strfunc(
225213
lambda fs, file_path, link_target: FakeFilesystem.link(
226214
fs, file_path, link_target
227-
)
215+
),
216+
os.link,
228217
)
229218

230219
if sys.version_info >= (3, 10):
231220
link = _wrap_binary_strfunc(
232221
lambda fs, file_path, link_target: FakeFilesystem.link(
233222
fs, file_path, link_target
234-
)
223+
),
224+
os.link,
235225
)
236226

237227
# this will use the fake filesystem because os is patched
238228
def getcwd(self):
239229
return os.getcwd()
240230

241-
readlink = _wrap_strfunc(FakeFilesystem.readlink)
231+
readlink = _wrap_strfunc(FakeFilesystem.readlink, os.readlink)
242232

243-
utime = _wrap_strfunc(FakeFilesystem.utime)
233+
utime = _wrap_strfunc(FakeFilesystem.utime, os.utime)
244234

245235

246236
_fake_accessor = _FakeAccessor()
@@ -613,7 +603,6 @@ def _from_parsed_parts(cls, drv, root, parts):
613603

614604
def _init(self, template=None):
615605
"""Initializer called from base class."""
616-
self._original_accessor = self._accessor
617606
# only needed until Python 3.10
618607
self._accessor = _fake_accessor
619608
# only needed until Python 3.8

pyfakefs/helpers.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -440,24 +440,38 @@ def putvalue(self, value: bytes) -> None:
440440
self._bytestream.write(value)
441441

442442

443-
def is_called_from_skipped_module(skip_names: list) -> bool:
443+
def is_called_from_skipped_module(skip_names: list, case_sensitive: bool) -> bool:
444+
def starts_with(path, string):
445+
if case_sensitive:
446+
return path.startswith(string)
447+
return path.lower().startswith(string.lower())
448+
444449
stack = traceback.extract_stack()
445-
from_open_code = (
450+
451+
# handle the case that we try to call the original `open_code`
452+
# (since Python 3.12)
453+
# The stack in this case is:
454+
# -1: helpers.is_called_from_skipped_module: 'stack = traceback.extract_stack()'
455+
# -2: fake_open.fake_open: 'if is_called_from_skipped_module('
456+
# -3: fake_io.open: 'return fake_open('
457+
# -4: fake_io.open_code : 'return self._io_module.open_code(path)'
458+
if (
446459
sys.version_info >= (3, 12)
447-
and stack[0].name == "open_code"
448-
and stack[0].line == "return self._io_module.open_code(path)"
449-
)
460+
and stack[-4].name == "open_code"
461+
and stack[-4].line == "return self._io_module.open_code(path)"
462+
):
463+
return True
450464

451465
caller_filename = next(
452466
(
453467
frame.filename
454468
for frame in stack[::-1]
455469
if not frame.filename.startswith("<frozen ")
456-
and not frame.filename.startswith(STDLIB_PATH)
470+
and not starts_with(frame.filename, STDLIB_PATH)
457471
and (
458-
not frame.filename.startswith(PYFAKEFS_PATH)
472+
not starts_with(frame.filename, PYFAKEFS_PATH)
459473
or any(
460-
frame.filename.startswith(test_path)
474+
starts_with(frame.filename, test_path)
461475
for test_path in PYFAKEFS_TEST_PATHS
462476
)
463477
)
@@ -469,7 +483,7 @@ def is_called_from_skipped_module(skip_names: list) -> bool:
469483
caller_module_name = os.path.splitext(caller_filename)[0]
470484
caller_module_name = caller_module_name.replace(os.sep, ".")
471485

472-
if from_open_code or any(
486+
if any(
473487
[
474488
caller_module_name == sn or caller_module_name.endswith("." + sn)
475489
for sn in skip_names

0 commit comments

Comments
 (0)