Skip to content

Commit

Permalink
Make fake fs, fake os and fake os.path respect additional_skip_names (p…
Browse files Browse the repository at this point in the history
…ytest-dev#1025)

- make fake filesystem, fake `os` and fake `os.path` respect `additional_skip_names`
- use is_called_from_skipped_module in fake_io and fake_open too
- disable one failing test for PyPy 3.7
- fixes pytest-dev#1023
---------

Co-authored-by: mrbean-bremen <[email protected]>
  • Loading branch information
sassanh and mrbean-bremen authored Aug 15, 2024
1 parent b9dbf7a commit c954966
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 88 deletions.
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ The released versions correspond to PyPI releases.
* the default for `FakeFilesystem.shuffle_listdir_results` will change to `True` to reflect
the real filesystem behavior

## Unreleased

### Enhancements

- refactor the implementation of `additional_skip_names` parameter to make it work with more modules (see [#1023](../../issues/1023))

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

Expand Down
4 changes: 2 additions & 2 deletions pyfakefs/fake_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,8 +603,8 @@ def remove_entry(self, pathname_name: str, recursive: bool = True) -> None:
st = traceback.extract_stack(limit=6)
if sys.version_info < (3, 10):
if (
st[1].name == "TemporaryFile"
and st[1].line == "_os.unlink(name)"
st[0].name == "TemporaryFile"
and st[0].line == "_os.unlink(name)"
):
raise_error = False
else:
Expand Down
12 changes: 6 additions & 6 deletions pyfakefs/fake_filesystem_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ def __init__(
set_uid(1)
set_gid(1)

self._skip_names = self.SKIPNAMES.copy()
self.skip_names = self.SKIPNAMES.copy()
# save the original open function for use in pytest plugin
self.original_open = open
self.patch_open_code = patch_open_code
Expand All @@ -582,7 +582,7 @@ def __init__(
cast(ModuleType, m).__name__ if inspect.ismodule(m) else cast(str, m)
for m in additional_skip_names
]
self._skip_names.update(skip_names)
self.skip_names.update(skip_names)

self._fake_module_classes: Dict[str, Any] = {}
self._unfaked_module_classes: Dict[str, Any] = {}
Expand Down Expand Up @@ -628,8 +628,8 @@ def __init__(
if patched_module_names != self.PATCHED_MODULE_NAMES:
self.__class__.PATCHED_MODULE_NAMES = patched_module_names
clear_cache = True
if self._skip_names != self.ADDITIONAL_SKIP_NAMES:
self.__class__.ADDITIONAL_SKIP_NAMES = self._skip_names
if self.skip_names != self.ADDITIONAL_SKIP_NAMES:
self.__class__.ADDITIONAL_SKIP_NAMES = self.skip_names
clear_cache = True
if patch_default_args != self.PATCH_DEFAULT_ARGS:
self.__class__.PATCH_DEFAULT_ARGS = patch_default_args
Expand Down Expand Up @@ -875,7 +875,7 @@ def _find_modules(self) -> None:
pass
continue
skipped = module in self.SKIPMODULES or any(
[sn.startswith(module.__name__) for sn in self._skip_names]
[sn.startswith(module.__name__) for sn in self.skip_names]
)
module_items = module.__dict__.copy().items()

Expand Down Expand Up @@ -922,7 +922,7 @@ def _refresh(self) -> None:
for name in self._fake_module_classes:
self.fake_modules[name] = self._fake_module_classes[name](self.fs)
if hasattr(self.fake_modules[name], "skip_names"):
self.fake_modules[name].skip_names = self._skip_names
self.fake_modules[name].skip_names = self.skip_names
self.fake_modules[PATH_MODULE] = self.fake_modules["os"].path
for name in self._unfaked_module_classes:
self.unfaked_modules[name] = self._unfaked_module_classes[name]()
Expand Down
16 changes: 13 additions & 3 deletions pyfakefs/fake_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

from pyfakefs.fake_file import AnyFileWrapper
from pyfakefs.fake_open import fake_open
from pyfakefs.helpers import IS_PYPY
from pyfakefs.helpers import IS_PYPY, is_called_from_skipped_module

if TYPE_CHECKING:
from pyfakefs.fake_filesystem import FakeFilesystem
Expand Down Expand Up @@ -180,6 +180,16 @@ def lockf(
) -> Any:
pass

def __getattr__(self, name):
def __getattribute__(self, name):
"""Forwards any unfaked calls to the standard fcntl module."""
return getattr(self._fcntl_module, name)
fs: FakeFilesystem = object.__getattribute__(self, "filesystem")
fnctl_module = object.__getattribute__(self, "_fcntl_module")
if fs.patcher:
if is_called_from_skipped_module(
skip_names=fs.patcher.skip_names,
case_sensitive=fs.is_case_sensitive,
):
# remove the `self` argument for FakeOsModule methods
return getattr(fnctl_module, name)

return object.__getattribute__(self, name)
39 changes: 4 additions & 35 deletions pyfakefs/fake_open.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import io
import os
import sys
import traceback
from stat import (
S_ISDIR,
)
Expand All @@ -44,6 +43,7 @@
)
from pyfakefs.helpers import (
AnyString,
is_called_from_skipped_module,
is_root,
PERM_READ,
PERM_WRITE,
Expand Down Expand Up @@ -86,40 +86,9 @@ def fake_open(
"""Redirect the call to FakeFileOpen.
See FakeFileOpen.call() for description.
"""

# workaround for built-in open called from skipped modules (see #552)
# as open is not imported explicitly, we cannot patch it for
# specific modules; instead we check if the caller is a skipped
# module (should work in most cases)
stack = traceback.extract_stack(limit=3)

# handle the case that we try to call the original `open_code`
# and get here instead (since Python 3.12)
# TODO: use a more generic approach (see PR #1025)
if sys.version_info >= (3, 12):
from_open_code = (
stack[0].name == "open_code"
and stack[0].line == "return self._io_module.open_code(path)"
)
else:
from_open_code = False

module_name = os.path.splitext(stack[0].filename)[0]
module_name = module_name.replace(os.sep, ".")
if sys.version_info >= (3, 13) and module_name.endswith(
("pathlib._abc", "pathlib._local")
):
stack = traceback.extract_stack(limit=6)
frame = 2
# in Python 3.13, pathlib is implemented in 2 sub-modules that may call
# each other, so we have to look further in the stack
while frame >= 0 and module_name.endswith(("pathlib._abc", "pathlib._local")):
module_name = os.path.splitext(stack[frame].filename)[0]
module_name = module_name.replace(os.sep, ".")
frame -= 1

if from_open_code or any(
[module_name == sn or module_name.endswith("." + sn) for sn in skip_names]
if is_called_from_skipped_module(
skip_names=skip_names,
case_sensitive=filesystem.is_case_sensitive,
):
return io_open( # pytype: disable=wrong-arg-count
file,
Expand Down
48 changes: 31 additions & 17 deletions pyfakefs/fake_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from pyfakefs.fake_scandir import scandir, walk, ScanDirIter
from pyfakefs.helpers import (
FakeStatResult,
is_called_from_skipped_module,
is_int_type,
is_byte_string,
make_string_path,
Expand Down Expand Up @@ -1409,27 +1410,40 @@ def __getattr__(self, name: str) -> Any:
return getattr(self.os_module, name)


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

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

@functools.wraps(f)
def wrapped(*args, **kwargs):
if FakeOsModule.use_original:
# remove the `self` argument for FakeOsModule methods
if args and isinstance(args[0], FakeOsModule):
args = args[1:]
return getattr(os, f.__name__)(*args, **kwargs)
return f(*args, **kwargs)
if not should_use_original and args:
self = args[0]
fs: FakeFilesystem = self.filesystem
if self.filesystem.patcher:
skip_names = fs.patcher.skip_names
if is_called_from_skipped_module(
skip_names=skip_names,
case_sensitive=fs.is_case_sensitive,
):
should_use_original = True

return wrapped
if should_use_original:
# remove the `self` argument for FakeOsModule methods
if args and isinstance(args[0], FakeOsModule):
args = args[1:]
return getattr(os, f.__name__)(*args, **kwargs)

for name, fn in inspect.getmembers(FakeOsModule, inspect.isfunction):
if not fn.__name__.startswith("_"):
setattr(FakeOsModule, name, handle_original_call(fn))
return f(*args, **kwargs)

return wrapped


for name, fn in inspect.getmembers(FakeOsModule, inspect.isfunction):
if not fn.__name__.startswith("_"):
setattr(FakeOsModule, name, handle_original_call(fn))


@contextmanager
Expand Down
38 changes: 38 additions & 0 deletions pyfakefs/fake_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"""Faked ``os.path`` module replacement. See ``fake_filesystem`` for usage."""

import errno
import functools
import inspect
import os
import sys
from stat import (
Expand All @@ -23,6 +25,7 @@
)
from types import ModuleType
from typing import (
Callable,
List,
Optional,
Union,
Expand All @@ -36,6 +39,7 @@
)

from pyfakefs.helpers import (
is_called_from_skipped_module,
make_string_path,
to_string,
matching_string,
Expand Down Expand Up @@ -553,3 +557,37 @@ def _isdir(self, path: AnyStr) -> bool:
def __getattr__(self, name: str) -> Any:
"""Forwards any non-faked calls to the real nt module."""
return getattr(self.nt_module, name)


def handle_original_call(f: Callable) -> Callable:
"""Decorator used for real pathlib Path methods to ensure that
real os functions instead of faked ones are used.
Applied to all non-private methods of `FakePathModule`."""

@functools.wraps(f)
def wrapped(*args, **kwargs):
if args:
self = args[0]
should_use_original = self.os.use_original
if not should_use_original and self.filesystem.patcher:
skip_names = self.filesystem.patcher.skip_names
if is_called_from_skipped_module(
skip_names=skip_names,
case_sensitive=self.filesystem.is_case_sensitive,
):
should_use_original = True

if should_use_original:
# remove the `self` argument for FakePathModule methods
if args and isinstance(args[0], FakePathModule):
args = args[1:]
return getattr(os.path, f.__name__)(*args, **kwargs)

return f(*args, **kwargs)

return wrapped


for name, fn in inspect.getmembers(FakePathModule, inspect.isfunction):
if not fn.__name__.startswith("_"):
setattr(FakePathModule, name, handle_original_call(fn))
Loading

0 comments on commit c954966

Please sign in to comment.