Skip to content

Commit 35c8eec

Browse files
committed
Make fake filesystem, fake os and fake os.path respect additional_skip_names
1 parent 9689317 commit 35c8eec

File tree

6 files changed

+156
-18
lines changed

6 files changed

+156
-18
lines changed

pyfakefs/fake_os.py

+27-17
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from pyfakefs.fake_scandir import scandir, walk, ScanDirIter
5555
from pyfakefs.helpers import (
5656
FakeStatResult,
57+
is_called_from_skipped_module,
5758
is_int_type,
5859
is_byte_string,
5960
make_string_path,
@@ -1409,27 +1410,36 @@ def __getattr__(self, name: str) -> Any:
14091410
return getattr(self.os_module, name)
14101411

14111412

1412-
if sys.version_info > (3, 10):
1413+
def handle_original_call(f: Callable) -> Callable:
1414+
"""Decorator used for real pathlib Path methods to ensure that
1415+
real os functions instead of faked ones are used.
1416+
Applied to all non-private methods of `FakeOsModule`."""
14131417

1414-
def handle_original_call(f: Callable) -> Callable:
1415-
"""Decorator used for real pathlib Path methods to ensure that
1416-
real os functions instead of faked ones are used.
1417-
Applied to all non-private methods of `FakeOsModule`."""
1418+
@functools.wraps(f)
1419+
def wrapped(*args, **kwargs):
1420+
should_use_original = FakeOsModule.use_original
14181421

1419-
@functools.wraps(f)
1420-
def wrapped(*args, **kwargs):
1421-
if FakeOsModule.use_original:
1422-
# remove the `self` argument for FakeOsModule methods
1423-
if args and isinstance(args[0], FakeOsModule):
1424-
args = args[1:]
1425-
return getattr(os, f.__name__)(*args, **kwargs)
1426-
return f(*args, **kwargs)
1422+
if not should_use_original and args:
1423+
self = args[0]
1424+
if self.filesystem.patcher:
1425+
skip_names = self.filesystem.patcher._skip_names
1426+
if is_called_from_skipped_module(skip_names=skip_names):
1427+
should_use_original = True
14271428

1428-
return wrapped
1429+
if should_use_original:
1430+
# remove the `self` argument for FakeOsModule methods
1431+
if args and isinstance(args[0], FakeOsModule):
1432+
args = args[1:]
1433+
return getattr(os, f.__name__)(*args, **kwargs)
14291434

1430-
for name, fn in inspect.getmembers(FakeOsModule, inspect.isfunction):
1431-
if not fn.__name__.startswith("_"):
1432-
setattr(FakeOsModule, name, handle_original_call(fn))
1435+
return f(*args, **kwargs)
1436+
1437+
return wrapped
1438+
1439+
1440+
for name, fn in inspect.getmembers(FakeOsModule, inspect.isfunction):
1441+
if not fn.__name__.startswith("_"):
1442+
setattr(FakeOsModule, name, handle_original_call(fn))
14331443

14341444

14351445
@contextmanager

pyfakefs/fake_path.py

+35
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"""Faked ``os.path`` module replacement. See ``fake_filesystem`` for usage."""
1616

1717
import errno
18+
import functools
19+
import inspect
1820
import os
1921
import sys
2022
from stat import (
@@ -23,6 +25,7 @@
2325
)
2426
from types import ModuleType
2527
from typing import (
28+
Callable,
2629
List,
2730
Optional,
2831
Union,
@@ -36,6 +39,7 @@
3639
)
3740

3841
from pyfakefs.helpers import (
42+
is_called_from_skipped_module,
3943
make_string_path,
4044
to_string,
4145
matching_string,
@@ -536,3 +540,34 @@ def _isdir(self, path: AnyStr) -> bool:
536540
def __getattr__(self, name: str) -> Any:
537541
"""Forwards any non-faked calls to the real nt module."""
538542
return getattr(self.nt_module, name)
543+
544+
545+
def handle_original_call(f: Callable) -> Callable:
546+
"""Decorator used for real pathlib Path methods to ensure that
547+
real os functions instead of faked ones are used.
548+
Applied to all non-private methods of `FakePathModule`."""
549+
550+
@functools.wraps(f)
551+
def wrapped(*args, **kwargs):
552+
if args:
553+
self = args[0]
554+
should_use_original = self.os.use_original
555+
if not should_use_original and self.filesystem.patcher:
556+
skip_names = self.filesystem.patcher._skip_names
557+
if is_called_from_skipped_module(skip_names=skip_names):
558+
should_use_original = True
559+
560+
if should_use_original:
561+
# remove the `self` argument for FakePathModule methods
562+
if args and isinstance(args[0], FakePathModule):
563+
args = args[1:]
564+
return getattr(os.path, f.__name__)(*args, **kwargs)
565+
566+
return f(*args, **kwargs)
567+
568+
return wrapped
569+
570+
571+
for name, fn in inspect.getmembers(FakePathModule, inspect.isfunction):
572+
if not fn.__name__.startswith("_"):
573+
setattr(FakePathModule, name, handle_original_call(fn))

pyfakefs/fake_pathlib.py

+38-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from pyfakefs.fake_filesystem import FakeFilesystem
4848
from pyfakefs.fake_open import fake_open
4949
from pyfakefs.fake_os import FakeOsModule, use_original_os
50-
from pyfakefs.helpers import IS_PYPY
50+
from pyfakefs.helpers import IS_PYPY, is_called_from_skipped_module
5151

5252

5353
def init_module(filesystem):
@@ -92,6 +92,18 @@ def init_module(filesystem):
9292
def _wrap_strfunc(strfunc):
9393
@functools.wraps(strfunc)
9494
def _wrapped(pathobj, *args, **kwargs):
95+
should_use_original = False
96+
if pathobj.filesystem.patcher:
97+
skip_names = pathobj.filesystem.patcher._skip_names
98+
if is_called_from_skipped_module(skip_names=skip_names):
99+
should_use_original = True
100+
101+
if should_use_original:
102+
return getattr(pathobj._original_accessor, strfunc.__name__)(
103+
str(pathobj),
104+
*args,
105+
**kwargs,
106+
)
95107
return strfunc(pathobj.filesystem, str(pathobj), *args, **kwargs)
96108

97109
return staticmethod(_wrapped)
@@ -100,6 +112,18 @@ def _wrapped(pathobj, *args, **kwargs):
100112
def _wrap_binary_strfunc(strfunc):
101113
@functools.wraps(strfunc)
102114
def _wrapped(pathobj1, pathobj2, *args):
115+
should_use_original = False
116+
if pathobj1.filesystem.patcher:
117+
skip_names = pathobj1.filesystem.patcher._skip_names
118+
if is_called_from_skipped_module(skip_names=skip_names):
119+
should_use_original = True
120+
121+
if should_use_original:
122+
return getattr(pathobj1._original_accessor, strfunc.__name__)(
123+
str(pathobj1),
124+
str(pathobj2),
125+
*args,
126+
)
103127
return strfunc(pathobj1.filesystem, str(pathobj1), str(pathobj2), *args)
104128

105129
return staticmethod(_wrapped)
@@ -108,6 +132,18 @@ def _wrapped(pathobj1, pathobj2, *args):
108132
def _wrap_binary_strfunc_reverse(strfunc):
109133
@functools.wraps(strfunc)
110134
def _wrapped(pathobj1, pathobj2, *args):
135+
should_use_original = False
136+
if pathobj2.filesystem.patcher:
137+
skip_names = pathobj2.filesystem.patcher._skip_names
138+
if is_called_from_skipped_module(skip_names=skip_names):
139+
should_use_original = True
140+
141+
if should_use_original:
142+
return getattr(pathobj2._original_accessor, strfunc.__name__)(
143+
str(pathobj2),
144+
str(pathobj1),
145+
*args,
146+
)
111147
return strfunc(pathobj2.filesystem, str(pathobj2), str(pathobj1), *args)
112148

113149
return staticmethod(_wrapped)
@@ -573,6 +609,7 @@ def _from_parsed_parts(cls, drv, root, parts):
573609

574610
def _init(self, template=None):
575611
"""Initializer called from base class."""
612+
self._original_accessor = self._accessor
576613
# only needed until Python 3.10
577614
self._accessor = _fake_accessor
578615
# only needed until Python 3.8

pyfakefs/helpers.py

+48
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import platform
1919
import stat
2020
import sys
21+
import sysconfig
2122
import time
23+
import traceback
2224
from collections import namedtuple
2325
from copy import copy
2426
from stat import S_IFLNK
@@ -38,6 +40,13 @@
3840
PERM_DEF_FILE = 0o666 # Default permission bits (regular file)
3941
PERM_ALL = 0o7777 # All permission bits.
4042

43+
STDLIB_PATH = os.path.realpath(sysconfig.get_path("stdlib"))
44+
PYFAKEFS_PATH = os.path.dirname(__file__)
45+
PYFAKEFS_TEST_PATHS = [
46+
os.path.join(PYFAKEFS_PATH, "tests"),
47+
os.path.join(PYFAKEFS_PATH, "pytest_tests"),
48+
]
49+
4150
_OpenModes = namedtuple(
4251
"_OpenModes",
4352
"must_exist can_read can_write truncate append must_not_exist",
@@ -426,3 +435,42 @@ def getvalue(self) -> bytes:
426435

427436
def putvalue(self, value: bytes) -> None:
428437
self._bytestream.write(value)
438+
439+
440+
def is_called_from_skipped_module(skip_names: list) -> bool:
441+
stack = traceback.extract_stack()
442+
from_open_code = (
443+
sys.version_info >= (3, 12)
444+
and stack[0].name == "open_code"
445+
and stack[0].line == "return self._io_module.open_code(path)"
446+
)
447+
448+
caller_filename = next(
449+
(
450+
frame.filename
451+
for frame in stack[::-1]
452+
if not frame.filename.startswith("<frozen importlib")
453+
and not frame.filename.startswith(STDLIB_PATH)
454+
and (
455+
not frame.filename.startswith(PYFAKEFS_PATH)
456+
or any(
457+
frame.filename.startswith(test_path)
458+
for test_path in PYFAKEFS_TEST_PATHS
459+
)
460+
)
461+
),
462+
None,
463+
)
464+
465+
if caller_filename:
466+
caller_module_name = os.path.splitext(caller_filename)[0]
467+
caller_module_name = caller_module_name.replace(os.sep, ".")
468+
469+
if from_open_code or any(
470+
[
471+
caller_module_name == sn or caller_module_name.endswith("." + sn)
472+
for sn in skip_names
473+
]
474+
):
475+
return True
476+
return False

pyfakefs/tests/fake_pathlib_test.py

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from pyfakefs.fake_filesystem import OSType
3434
from pyfakefs.helpers import IS_PYPY, is_root
3535
from pyfakefs.tests.skipped_pathlib import (
36+
check_exists_pathlib,
3637
read_bytes_pathlib,
3738
read_pathlib,
3839
read_text_pathlib,
@@ -1336,6 +1337,9 @@ def test_read_bytes_in_skipped_module(self):
13361337
contents = read_bytes_pathlib("skipped_pathlib.py")
13371338
self.assertTrue(contents.startswith(b"# Licensed under the Apache License"))
13381339

1340+
def test_exists(self):
1341+
self.assertTrue(check_exists_pathlib())
1342+
13391343

13401344
if __name__ == "__main__":
13411345
unittest.main(verbosity=2)

pyfakefs/tests/skipped_pathlib.py

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ def read_bytes_pathlib(file_name):
2929
return (Path(__file__).parent / file_name).read_bytes()
3030

3131

32+
def check_exists_pathlib():
33+
return os.path.exists(__file__) and Path(__file__).exists()
34+
35+
3236
def read_open(file_name):
3337
with open(os.path.join(os.path.dirname(__file__), file_name)) as f:
3438
return f.read()

0 commit comments

Comments
 (0)