diff options
Diffstat (limited to 'pyfakefs/fake_filesystem_unittest.py')
-rw-r--r-- | pyfakefs/fake_filesystem_unittest.py | 698 |
1 files changed, 421 insertions, 277 deletions
diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py index 6633cb5..4604a65 100644 --- a/pyfakefs/fake_filesystem_unittest.py +++ b/pyfakefs/fake_filesystem_unittest.py @@ -35,10 +35,15 @@ Existing unit tests that use the real file system can be retrofitted to use pyfakefs by simply changing their base class from `:py:class`unittest.TestCase` to `:py:class`pyfakefs.fake_filesystem_unittest.TestCase`. """ +import _io # type:ignore[import] +import builtins import doctest import functools +import genericpath import inspect +import io import linecache +import os import shutil import sys import tempfile @@ -46,28 +51,38 @@ import tokenize from importlib.abc import Loader, MetaPathFinder from types import ModuleType, TracebackType, FunctionType from typing import ( - Any, Callable, Dict, List, Set, Tuple, Optional, Union, - AnyStr, Type, Iterator, cast, ItemsView, Sequence + Any, + Callable, + Dict, + List, + Set, + Tuple, + Optional, + Union, + Type, + Iterator, + cast, + ItemsView, + Sequence, ) import unittest import warnings from unittest import TestSuite -from pyfakefs.deprecator import Deprecator from pyfakefs.fake_filesystem import ( - set_uid, set_gid, reset_ids, PatchMode, FakeFile, FakeFilesystem + set_uid, + set_gid, + reset_ids, + PatchMode, + FakeFilesystem, ) from pyfakefs.helpers import IS_PYPY from pyfakefs.mox3_stubout import StubOutForTesting -try: - from importlib.machinery import ModuleSpec -except ImportError: - ModuleSpec = object # type: ignore[assignment, misc] - +from importlib.machinery import ModuleSpec from importlib import reload -from pyfakefs import fake_filesystem +from pyfakefs import fake_filesystem, fake_io, fake_os, fake_open, fake_path, fake_file from pyfakefs import fake_filesystem_shutil from pyfakefs import fake_pathlib from pyfakefs import mox3_stubout @@ -76,20 +91,22 @@ from pyfakefs.extra_packages import pathlib2, use_scandir if use_scandir: from pyfakefs import fake_scandir -OS_MODULE = 'nt' if sys.platform == 'win32' else 'posix' -PATH_MODULE = 'ntpath' if sys.platform == 'win32' else 'posixpath' - - -def patchfs(_func: Callable = None, *, - additional_skip_names: Optional[ - List[Union[str, ModuleType]]] = None, - modules_to_reload: Optional[List[ModuleType]] = None, - modules_to_patch: Optional[Dict[str, ModuleType]] = None, - allow_root_user: bool = True, - use_known_patches: bool = True, - patch_open_code: PatchMode = PatchMode.OFF, - patch_default_args: bool = False, - use_cache: bool = True) -> Callable: +OS_MODULE = "nt" if sys.platform == "win32" else "posix" +PATH_MODULE = "ntpath" if sys.platform == "win32" else "posixpath" + + +def patchfs( + _func: Optional[Callable] = None, + *, + additional_skip_names: Optional[List[Union[str, ModuleType]]] = None, + modules_to_reload: Optional[List[ModuleType]] = None, + modules_to_patch: Optional[Dict[str, ModuleType]] = None, + allow_root_user: bool = True, + use_known_patches: bool = True, + patch_open_code: PatchMode = PatchMode.OFF, + patch_default_args: bool = False, + use_cache: bool = True +) -> Callable: """Convenience decorator to use patcher with additional parameters in a test function. @@ -108,14 +125,15 @@ def patchfs(_func: Callable = None, *, @functools.wraps(f) def wrapped(*args, **kwargs): with Patcher( - additional_skip_names=additional_skip_names, - modules_to_reload=modules_to_reload, - modules_to_patch=modules_to_patch, - allow_root_user=allow_root_user, - use_known_patches=use_known_patches, - patch_open_code=patch_open_code, - patch_default_args=patch_default_args, - use_cache=use_cache) as p: + additional_skip_names=additional_skip_names, + modules_to_reload=modules_to_reload, + modules_to_patch=modules_to_patch, + allow_root_user=allow_root_user, + use_known_patches=use_known_patches, + patch_open_code=patch_open_code, + patch_default_args=patch_default_args, + use_cache=use_cache, + ) as p: args = list(args) args.append(p.fs) return f(*args, **kwargs) @@ -128,23 +146,28 @@ def patchfs(_func: Callable = None, *, "Decorator argument is not a function.\n" "Did you mean `@patchfs(additional_skip_names=...)`?" ) - if hasattr(_func, 'patchings'): + if hasattr(_func, "patchings"): _func.nr_patches = len(_func.patchings) # type: ignore return wrap_patchfs(_func) return wrap_patchfs +DOCTEST_PATCHER = None + + def load_doctests( - loader: Any, tests: TestSuite, ignore: Any, module: ModuleType, - additional_skip_names: Optional[ - List[Union[str, ModuleType]]] = None, - modules_to_reload: Optional[List[ModuleType]] = None, - modules_to_patch: Optional[Dict[str, ModuleType]] = None, - allow_root_user: bool = True, - use_known_patches: bool = True, - patch_open_code: PatchMode = PatchMode.OFF, - patch_default_args: bool = False + loader: Any, + tests: TestSuite, + ignore: Any, + module: ModuleType, + additional_skip_names: Optional[List[Union[str, ModuleType]]] = None, + modules_to_reload: Optional[List[ModuleType]] = None, + modules_to_patch: Optional[Dict[str, ModuleType]] = None, + allow_root_user: bool = True, + use_known_patches: bool = True, + patch_open_code: PatchMode = PatchMode.OFF, + patch_default_args: bool = False, ) -> TestSuite: # pylint:disable=unused-argument """Load the doctest tests for the specified module into unittest. Args: @@ -154,18 +177,28 @@ def load_doctests( File `example_test.py` in the pyfakefs release provides a usage example. """ - _patcher = Patcher(additional_skip_names=additional_skip_names, - modules_to_reload=modules_to_reload, - modules_to_patch=modules_to_patch, - allow_root_user=allow_root_user, - use_known_patches=use_known_patches, - patch_open_code=patch_open_code, - patch_default_args=patch_default_args) - globs = _patcher.replace_globs(vars(module)) - tests.addTests(doctest.DocTestSuite(module, - globs=globs, - setUp=_patcher.setUp, - tearDown=_patcher.tearDown)) + has_patcher = Patcher.DOC_PATCHER is not None + if not has_patcher: + Patcher.DOC_PATCHER = Patcher( + additional_skip_names=additional_skip_names, + modules_to_reload=modules_to_reload, + modules_to_patch=modules_to_patch, + allow_root_user=allow_root_user, + use_known_patches=use_known_patches, + patch_open_code=patch_open_code, + patch_default_args=patch_default_args, + is_doc_test=True, + ) + assert Patcher.DOC_PATCHER is not None + globs = Patcher.DOC_PATCHER.replace_globs(vars(module)) + tests.addTests( + doctest.DocTestSuite( + module, + globs=globs, + setUp=Patcher.DOC_PATCHER.setUp, + tearDown=Patcher.DOC_PATCHER.tearDown, + ) + ) return tests @@ -214,19 +247,26 @@ class TestCaseMixin: modules_to_patch: Optional[Dict[str, ModuleType]] = None @property + def patcher(self): + if hasattr(self, "_patcher"): + return self._patcher or Patcher.PATCHER + return Patcher.PATCHER + + @property def fs(self) -> FakeFilesystem: - return cast(FakeFilesystem, self._stubber.fs) - - def setUpPyfakefs(self, - additional_skip_names: Optional[ - List[Union[str, ModuleType]]] = None, - modules_to_reload: Optional[List[ModuleType]] = None, - modules_to_patch: Optional[Dict[str, ModuleType]] = None, - allow_root_user: bool = True, - use_known_patches: bool = True, - patch_open_code: PatchMode = PatchMode.OFF, - patch_default_args: bool = False, - use_cache: bool = True) -> None: + return cast(FakeFilesystem, self.patcher.fs) + + def setUpPyfakefs( + self, + additional_skip_names: Optional[List[Union[str, ModuleType]]] = None, + modules_to_reload: Optional[List[ModuleType]] = None, + modules_to_patch: Optional[Dict[str, ModuleType]] = None, + allow_root_user: bool = True, + use_known_patches: bool = True, + patch_open_code: PatchMode = PatchMode.OFF, + patch_default_args: bool = False, + use_cache: bool = True, + ) -> None: """Bind the file-related modules to the :py:class:`pyfakefs` fake file system instead of the real file system. Also bind the fake `open()` function. @@ -238,13 +278,68 @@ class TestCaseMixin: the current test case. Settings the arguments here may be a more convenient way to adapt the setting than overwriting `__init__()`. """ + # if the class has already a patcher setup, we use this one + if Patcher.PATCHER is not None: + return + if additional_skip_names is None: additional_skip_names = self.additional_skip_names if modules_to_reload is None: modules_to_reload = self.modules_to_reload if modules_to_patch is None: modules_to_patch = self.modules_to_patch - self._stubber = Patcher( + self._patcher = Patcher( + additional_skip_names=additional_skip_names, + modules_to_reload=modules_to_reload, + modules_to_patch=modules_to_patch, + allow_root_user=allow_root_user, + use_known_patches=use_known_patches, + patch_open_code=patch_open_code, + patch_default_args=patch_default_args, + use_cache=use_cache, + ) + + self._patcher.setUp() + cast(TestCase, self).addCleanup(self._patcher.tearDown) + + @classmethod + def setUpClassPyfakefs( + cls, + additional_skip_names: Optional[List[Union[str, ModuleType]]] = None, + modules_to_reload: Optional[List[ModuleType]] = None, + modules_to_patch: Optional[Dict[str, ModuleType]] = None, + allow_root_user: bool = True, + use_known_patches: bool = True, + patch_open_code: PatchMode = PatchMode.OFF, + patch_default_args: bool = False, + use_cache: bool = True, + ) -> None: + """Similar to :py:func:`setUpPyfakefs`, but as a class method that + can be used in `setUpClass` instead of in `setUp`. + The fake filesystem will live in all test methods in the test class + and can be used in the usual way. + Note that using both :py:func:`setUpClassPyfakefs` and + :py:func:`setUpPyfakefs` in the same class will not work correctly. + + .. note:: This method is only available from Python 3.8 onwards. + """ + if sys.version_info < (3, 8): + raise NotImplementedError( + "setUpClassPyfakefs is only available in " + "Python versions starting from 3.8" + ) + + # if the class has already a patcher setup, we use this one + if Patcher.PATCHER is not None: + return + + if additional_skip_names is None: + additional_skip_names = cls.additional_skip_names + if modules_to_reload is None: + modules_to_reload = cls.modules_to_reload + if modules_to_patch is None: + modules_to_patch = cls.modules_to_patch + Patcher.PATCHER = Patcher( additional_skip_names=additional_skip_names, modules_to_reload=modules_to_reload, modules_to_patch=modules_to_patch, @@ -252,11 +347,21 @@ class TestCaseMixin: use_known_patches=use_known_patches, patch_open_code=patch_open_code, patch_default_args=patch_default_args, - use_cache=use_cache + use_cache=use_cache, ) - self._stubber.setUp() - cast(TestCase, self).addCleanup(self._stubber.tearDown) + Patcher.PATCHER.setUp() + cast(TestCase, cls).addClassCleanup(Patcher.PATCHER.tearDown) + + @classmethod + def fake_fs(cls): + """Convenience class method for accessing the fake filesystem. + For use inside `setUpClass`, after :py:func:`setUpClassPyfakefs` + has been called. + """ + if Patcher.PATCHER: + return Patcher.PATCHER.fs + return None def pause(self) -> None: """Pause the patching of the file system modules until `resume` is @@ -265,7 +370,7 @@ class TestCaseMixin: Calling pause() twice is silently ignored. """ - self._stubber.pause() + self.patcher.pause() def resume(self) -> None: """Resume the patching of the file system modules if `pause` has @@ -273,7 +378,7 @@ class TestCaseMixin: executed in the fake file system. Does nothing if patching is not paused. """ - self._stubber.resume() + self.patcher.resume() class TestCase(unittest.TestCase, TestCaseMixin): @@ -283,11 +388,13 @@ class TestCase(unittest.TestCase, TestCaseMixin): The arguments are explained in :py:class:`TestCaseMixin`. """ - def __init__(self, methodName: str = 'runTest', - additional_skip_names: Optional[ - List[Union[str, ModuleType]]] = None, - modules_to_reload: Optional[List[ModuleType]] = None, - modules_to_patch: Optional[Dict[str, ModuleType]] = None): + def __init__( + self, + methodName: str = "runTest", + additional_skip_names: Optional[List[Union[str, ModuleType]]] = None, + modules_to_reload: Optional[List[ModuleType]] = None, + modules_to_patch: Optional[Dict[str, ModuleType]] = None, + ): """Creates the test class instance and the patcher used to stub out file system related modules. @@ -301,53 +408,10 @@ class TestCase(unittest.TestCase, TestCaseMixin): self.modules_to_reload = modules_to_reload self.modules_to_patch = modules_to_patch - @Deprecator('add_real_file') - def copyRealFile(self, real_file_path: AnyStr, - fake_file_path: Optional[AnyStr] = None, - create_missing_dirs: bool = True) -> FakeFile: - """Add the file `real_file_path` in the real file system to the same - path in the fake file system. - - **This method is deprecated** in favor of - :py:meth:`FakeFilesystem..add_real_file`. - `copyRealFile()` is retained with limited functionality for backward - compatibility only. - - Args: - real_file_path: Path to the file in both the real and fake - file systems - fake_file_path: Deprecated. Use the default, which is - `real_file_path`. - If a value other than `real_file_path` is specified, a `ValueError` - exception will be raised. - create_missing_dirs: Deprecated. Use the default, which creates - missing directories in the fake file system. If `False` is - specified, a `ValueError` exception is raised. - - Returns: - The newly created FakeFile object. - - Raises: - OSError: If the file already exists in the fake file system. - ValueError: If deprecated argument values are specified. - - See: - :py:meth:`FakeFileSystem.add_real_file` - """ - if fake_file_path is not None and real_file_path != fake_file_path: - raise ValueError("CopyRealFile() is deprecated and no longer " - "supports different real and fake file paths") - if not create_missing_dirs: - raise ValueError("CopyRealFile() is deprecated and no longer " - "supports NOT creating missing directories") - assert self._stubber.fs is not None - return self._stubber.fs.add_real_file(real_file_path, read_only=False) - def tearDownPyfakefs(self) -> None: """This method is deprecated and exists only for backward compatibility. It does nothing. """ - pass class Patcher: @@ -363,21 +427,49 @@ class Patcher: with Patcher(): doStuff() """ - '''Stub nothing that is imported within these modules. + + """Stub nothing that is imported within these modules. `sys` is included to prevent `sys.path` from being stubbed with the fake `os.path`. - The `pytest` and `py` modules are used by pytest and have to access the - real file system. The `linecache` module is used to read the test file in case of test failure to get traceback information before test tear down. In order to make sure that reading the test file is not faked, we skip faking the module. We also have to set back the cached open function in tokenize. - ''' + """ SKIPMODULES = { - None, fake_filesystem, fake_filesystem_shutil, - sys, linecache, tokenize + None, + fake_filesystem, + fake_filesystem_shutil, + fake_os, + fake_io, + fake_open, + fake_path, + fake_file, + sys, + linecache, + tokenize, + os, + io, + _io, + genericpath, + os.path, } + if sys.platform == "win32": + import nt # type:ignore[import] + import ntpath + + SKIPMODULES.add(nt) + SKIPMODULES.add(ntpath) + else: + import posix + import posixpath + import fcntl + + SKIPMODULES.add(posix) + SKIPMODULES.add(posixpath) + SKIPMODULES.add(fcntl) + # caches all modules that do not have file system modules or function # to speed up _find_modules CACHED_MODULES: Set[ModuleType] = set() @@ -386,28 +478,42 @@ class Patcher: FS_DEFARGS: List[Tuple[FunctionType, int, Callable[..., Any]]] = [] SKIPPED_FS_MODULES: Dict[str, Set[Tuple[ModuleType, str]]] = {} - assert None in SKIPMODULES, ("sys.modules contains 'None' values;" - " must skip them.") + assert None in SKIPMODULES, "sys.modules contains 'None' values;" " must skip them." - IS_WINDOWS = sys.platform in ('win32', 'cygwin') + IS_WINDOWS = sys.platform in ("win32", "cygwin") - SKIPNAMES = {'os', 'path', 'io', 'genericpath', 'fcntl', - OS_MODULE, PATH_MODULE} + SKIPNAMES: Set[str] = set() # hold values from last call - if changed, the cache has to be invalidated PATCHED_MODULE_NAMES: Set[str] = set() ADDITIONAL_SKIP_NAMES: Set[str] = set() PATCH_DEFAULT_ARGS = False - - def __init__(self, additional_skip_names: Optional[ - List[Union[str, ModuleType]]] = None, - modules_to_reload: Optional[List[ModuleType]] = None, - modules_to_patch: Optional[Dict[str, ModuleType]] = None, - allow_root_user: bool = True, - use_known_patches: bool = True, - patch_open_code: PatchMode = PatchMode.OFF, - patch_default_args: bool = False, - use_cache: bool = True) -> None: + PATCHER: Optional["Patcher"] = None + DOC_PATCHER: Optional["Patcher"] = None + REF_COUNT = 0 + DOC_REF_COUNT = 0 + + def __new__(cls, *args, **kwargs): + if kwargs.get("is_doc_test", False): + if cls.DOC_PATCHER is None: + cls.DOC_PATCHER = super().__new__(cls) + return cls.DOC_PATCHER + if cls.PATCHER is None: + cls.PATCHER = super().__new__(cls) + return cls.PATCHER + + def __init__( + self, + additional_skip_names: Optional[List[Union[str, ModuleType]]] = None, + modules_to_reload: Optional[List[ModuleType]] = None, + modules_to_patch: Optional[Dict[str, ModuleType]] = None, + allow_root_user: bool = True, + use_known_patches: bool = True, + patch_open_code: PatchMode = PatchMode.OFF, + patch_default_args: bool = False, + use_cache: bool = True, + is_doc_test: bool = False, + ) -> None: """ Args: additional_skip_names: names of modules inside of which no module @@ -439,7 +545,12 @@ class Patcher: feature, this argument allows to turn it off in case it causes any problems. """ - + self.is_doc_test = is_doc_test + if is_doc_test: + if self.DOC_REF_COUNT > 0: + return + elif self.REF_COUNT > 0: + return if not allow_root_user: # set non-root IDs even if the real user is root set_uid(1) @@ -449,11 +560,12 @@ class Patcher: # save the original open function for use in pytest plugin self.original_open = open self.patch_open_code = patch_open_code + self.fake_open: fake_open.FakeFileOpen if additional_skip_names is not None: skip_names = [ - cast(ModuleType, m).__name__ if inspect.ismodule(m) - else cast(str, m) for m in additional_skip_names + cast(ModuleType, m).__name__ if inspect.ismodule(m) else cast(str, m) + for m in additional_skip_names ] self._skip_names.update(skip_names) @@ -464,7 +576,7 @@ class Patcher: # reload tempfile under posix to patch default argument self.modules_to_reload: List[ModuleType] = ( - [] if sys.platform == 'win32' else [tempfile] + [] if sys.platform == "win32" else [tempfile] ) if modules_to_reload is not None: self.modules_to_reload.extend(modules_to_reload) @@ -473,8 +585,9 @@ class Patcher: if use_known_patches: from pyfakefs.patched_packages import ( - get_modules_to_patch, get_classes_to_patch, - get_fake_module_classes + get_modules_to_patch, + get_classes_to_patch, + get_fake_module_classes, ) modules_to_patch = modules_to_patch or {} @@ -516,13 +629,18 @@ class Patcher: self._dyn_patcher: Optional[DynamicPatcher] = None self._patching = False - def clear_cache(self) -> None: + @classmethod + def clear_fs_cache(cls) -> None: """Clear the module cache.""" - self.__class__.CACHED_MODULES = set() - self.__class__.FS_MODULES = {} - self.__class__.FS_FUNCTIONS = {} - self.__class__.FS_DEFARGS = [] - self.__class__.SKIPPED_FS_MODULES = {} + cls.CACHED_MODULES = set() + cls.FS_MODULES = {} + cls.FS_FUNCTIONS = {} + cls.FS_DEFARGS = [] + cls.SKIPPED_FS_MODULES = {} + + def clear_cache(self) -> None: + """Clear the module cache (convenience instance method).""" + self.__class__.clear_fs_cache() def _init_fake_module_classes(self) -> None: # IMPORTANT TESTING NOTE: Whenever you add a new module below, test @@ -530,63 +648,63 @@ class Patcher: # and a test in fake_filesystem_unittest_test.py, class # TestAttributesWithFakeModuleNames. self._fake_module_classes = { - 'os': fake_filesystem.FakeOsModule, - 'shutil': fake_filesystem_shutil.FakeShutilModule, - 'io': fake_filesystem.FakeIoModule, - 'pathlib': fake_pathlib.FakePathlibModule + "os": fake_os.FakeOsModule, + "shutil": fake_filesystem_shutil.FakeShutilModule, + "io": fake_io.FakeIoModule, + "pathlib": fake_pathlib.FakePathlibModule, } if IS_PYPY: # in PyPy io.open, the module is referenced as _io - self._fake_module_classes['_io'] = fake_filesystem.FakeIoModule - if sys.platform != 'win32': - self._fake_module_classes[ - 'fcntl'] = fake_filesystem.FakeFcntlModule + self._fake_module_classes["_io"] = fake_io.FakeIoModule + if sys.platform == "win32": + self._fake_module_classes["nt"] = fake_path.FakeNtModule + else: + self._fake_module_classes["fcntl"] = fake_filesystem.FakeFcntlModule # class modules maps class names against a list of modules they can # be contained in - this allows for alternative modules like # `pathlib` and `pathlib2` - self._class_modules['Path'] = ['pathlib'] - self._unfaked_module_classes[ - 'pathlib'] = fake_pathlib.RealPathlibModule + self._class_modules["Path"] = ["pathlib"] + self._unfaked_module_classes["pathlib"] = fake_pathlib.RealPathlibModule if pathlib2: - self._fake_module_classes[ - 'pathlib2'] = fake_pathlib.FakePathlibModule - self._class_modules['Path'].append('pathlib2') - self._unfaked_module_classes[ - 'pathlib2'] = fake_pathlib.RealPathlibModule - self._fake_module_classes[ - 'Path'] = fake_pathlib.FakePathlibPathModule - self._unfaked_module_classes[ - 'Path'] = fake_pathlib.RealPathlibPathModule + self._fake_module_classes["pathlib2"] = fake_pathlib.FakePathlibModule + self._class_modules["Path"].append("pathlib2") + self._unfaked_module_classes["pathlib2"] = fake_pathlib.RealPathlibModule + self._fake_module_classes["Path"] = fake_pathlib.FakePathlibPathModule + self._unfaked_module_classes["Path"] = fake_pathlib.RealPathlibPathModule if use_scandir: - self._fake_module_classes[ - 'scandir'] = fake_scandir.FakeScanDirModule + self._fake_module_classes["scandir"] = fake_scandir.FakeScanDirModule def _init_fake_module_functions(self) -> None: # handle patching function imported separately like # `from os import stat` # each patched function name has to be looked up separately for mod_name, fake_module in self._fake_module_classes.items(): - if (hasattr(fake_module, 'dir') and - inspect.isfunction(fake_module.dir)): - for fct_name in fake_module.dir(): - module_attr = (getattr(fake_module, fct_name), mod_name) - self._fake_module_functions.setdefault( - fct_name, {})[mod_name] = module_attr - if mod_name == 'os': - self._fake_module_functions.setdefault( - fct_name, {})[OS_MODULE] = module_attr + if hasattr(fake_module, "dir"): + module_dir = fake_module.dir + if inspect.isfunction(module_dir): + for fct_name in fake_module.dir(): + module_attr = (getattr(fake_module, fct_name), mod_name) + self._fake_module_functions.setdefault(fct_name, {})[ + mod_name + ] = module_attr + if mod_name == "os": + self._fake_module_functions.setdefault(fct_name, {})[ + OS_MODULE + ] = module_attr # special handling for functions in os.path fake_module = fake_filesystem.FakePathModule for fct_name in fake_module.dir(): module_attr = (getattr(fake_module, fct_name), PATH_MODULE) - self._fake_module_functions.setdefault( - fct_name, {})['genericpath'] = module_attr - self._fake_module_functions.setdefault( - fct_name, {})[PATH_MODULE] = module_attr - - def __enter__(self) -> 'Patcher': + self._fake_module_functions.setdefault(fct_name, {})[ + "genericpath" + ] = module_attr + self._fake_module_functions.setdefault(fct_name, {})[ + PATH_MODULE + ] = module_attr + + def __enter__(self) -> "Patcher": """Context manager for usage outside of fake_filesystem_unittest.TestCase. Ensure that all patched modules are removed in case of an @@ -595,49 +713,45 @@ class Patcher: self.setUp() return self - def __exit__(self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType]) -> None: + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: self.tearDown() - def _is_fs_module(self, mod: ModuleType, - name: str, - module_names: List[str]) -> bool: - try: - # check for __name__ first and ignore the AttributeException - # if it does not exist - avoids calling expansive ismodule - if mod.__name__ in module_names and inspect.ismodule(mod): - return True - except Exception: - pass + def _is_fs_module( + self, mod: ModuleType, name: str, module_names: List[str] + ) -> bool: try: - if (name in self._class_modules and - mod.__module__ in self._class_modules[name]): - return inspect.isclass(mod) + return ( + inspect.ismodule(mod) + and mod.__name__ in module_names + or inspect.isclass(mod) + and mod.__module__ in self._class_modules.get(name, []) + ) except Exception: - # handle AttributeError and any other exception possibly triggered - # by side effects of inspect methods - pass - return False + # handle cases where the module has no __name__ or __module__ + # attribute - see #460, and any other exception triggered + # by inspect functions + return False def _is_fs_function(self, fct: FunctionType) -> bool: try: - # check for __name__ first and ignore the AttributeException - # if it does not exist - avoids calling expansive inspect - # methods in most cases - return (fct.__name__ in self._fake_module_functions and - fct.__module__ in self._fake_module_functions[ - fct.__name__] and - (inspect.isfunction(fct) or inspect.isbuiltin(fct))) + return ( + (inspect.isfunction(fct) or inspect.isbuiltin(fct)) + and fct.__name__ in self._fake_module_functions + and fct.__module__ in self._fake_module_functions[fct.__name__] + ) except Exception: - # handle AttributeError and any other exception possibly triggered - # by side effects of inspect methods + # handle cases where the function has no __name__ or __module__ + # attribute, or any other exception in inspect functions return False def _def_values( - self, - item: FunctionType) -> Iterator[Tuple[FunctionType, int, Any]]: + self, item: FunctionType + ) -> Iterator[Tuple[FunctionType, int, Any]]: """Find default arguments that are file-system functions to be patched in top-level functions and members of top-level classes.""" # check for module-level functions @@ -653,8 +767,7 @@ class Patcher: # check for methods in class # (nested classes are ignored for now) # inspect.getmembers is very expansive! - for m in inspect.getmembers(item, - predicate=inspect.isfunction): + for m in inspect.getmembers(item, predicate=inspect.isfunction): f = cast(FunctionType, m[1]) if f.__defaults__: for i, d in enumerate(f.__defaults__): @@ -666,8 +779,7 @@ class Patcher: # _DontDoThat() (see #523) pass - def _find_def_values( - self, module_items: ItemsView[str, FunctionType]) -> None: + def _find_def_values(self, module_items: ItemsView[str, FunctionType]) -> None: for _, fct in module_items: for f, i, d in self._def_values(fct): self.__class__.FS_DEFARGS.append((f, i, d)) @@ -680,8 +792,11 @@ class Patcher: module_names = list(self._fake_module_classes.keys()) + [PATH_MODULE] for name, module in list(sys.modules.items()): try: - if (self.use_cache and module in self.CACHED_MODULES or - not inspect.ismodule(module)): + if ( + self.use_cache + and module in self.CACHED_MODULES + or not inspect.ismodule(module) + ): continue except Exception: # workaround for some py (part of pytest) versions @@ -691,30 +806,35 @@ class Patcher: if self.use_cache: self.__class__.CACHED_MODULES.add(module) continue - skipped = (module in self.SKIPMODULES or - any([sn.startswith(module.__name__) - for sn in self._skip_names])) + skipped = module in self.SKIPMODULES or any( + [sn.startswith(module.__name__) for sn in self._skip_names] + ) module_items = module.__dict__.copy().items() - modules = {name: mod for name, mod in module_items - if self._is_fs_module(mod, name, module_names)} + modules = { + name: mod + for name, mod in module_items + if self._is_fs_module(mod, name, module_names) + } if skipped: for name, mod in modules.items(): - self.__class__.SKIPPED_FS_MODULES.setdefault( - name, set()).add((module, mod.__name__)) + self.__class__.SKIPPED_FS_MODULES.setdefault(name, set()).add( + (module, mod.__name__) + ) else: for name, mod in modules.items(): self.__class__.FS_MODULES.setdefault(name, set()).add( - (module, mod.__name__)) - functions = {name: fct for name, fct in - module_items - if self._is_fs_function(fct)} + (module, mod.__name__) + ) + functions = { + name: fct for name, fct in module_items if self._is_fs_function(fct) + } for name, fct in functions.items(): self.__class__.FS_FUNCTIONS.setdefault( - (name, fct.__name__, fct.__module__), - set()).add(module) + (name, fct.__name__, fct.__module__), set() + ).add(module) # find default arguments that are file system functions if self.patch_default_args: @@ -729,13 +849,14 @@ class Patcher: self._stubs.smart_unset_all() self._stubs = mox3_stubout.StubOutForTesting() - self.fs = fake_filesystem.FakeFilesystem(patcher=self) + self.fs = fake_filesystem.FakeFilesystem(patcher=self, create_temp_dir=True) self.fs.patch_open_code = self.patch_open_code + self.fake_open = fake_open.FakeFileOpen(self.fs) 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'): + if hasattr(self.fake_modules[name], "skip_names"): self.fake_modules[name].skip_names = self._skip_names - self.fake_modules[PATH_MODULE] = self.fake_modules['os'].path + 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]() @@ -745,18 +866,25 @@ class Patcher: """Bind the file-related modules to the :py:mod:`pyfakefs` fake modules real ones. Also bind the fake `file()` and `open()` functions. """ - self.has_fcopy_file = (sys.platform == 'darwin' and - hasattr(shutil, '_HAS_FCOPYFILE') and - shutil._HAS_FCOPYFILE) + if self.is_doc_test: + self.__class__.DOC_REF_COUNT += 1 + if self.__class__.DOC_REF_COUNT > 1: + return + else: + self.__class__.REF_COUNT += 1 + if self.__class__.REF_COUNT > 1: + return + self.has_fcopy_file = ( + sys.platform == "darwin" + and hasattr(shutil, "_HAS_FCOPYFILE") + and shutil._HAS_FCOPYFILE + ) if self.has_fcopy_file: shutil._HAS_FCOPYFILE = False # type: ignore[attr-defined] - temp_dir = tempfile.gettempdir() with warnings.catch_warnings(): # ignore warnings, see #542 and #614 - warnings.filterwarnings( - 'ignore' - ) + warnings.filterwarnings("ignore") self._find_modules() self._refresh() @@ -768,11 +896,6 @@ class Patcher: linecache.open = self.original_open # type: ignore[attr-defined] tokenize._builtin_open = self.original_open # type: ignore - # the temp directory is assumed to exist at least in `tempfile1`, - # so we create it here for convenience - assert self.fs is not None - self.fs.create_dir(temp_dir) - def start_patching(self) -> None: if not self._patching: self._patching = True @@ -792,7 +915,9 @@ class Patcher: for (name, ft_name, ft_mod), modules in self.FS_FUNCTIONS.items(): method, mod_name = self._fake_module_functions[ft_name][ft_mod] fake_module = self.fake_modules[mod_name] - attr = method.__get__(fake_module, fake_module.__class__) + attr = method.__get__( + fake_module, fake_module.__class__ + ) # pytype: disable=attribute-error for module in modules: self._stubs.smart_set(module, name, attr) @@ -800,20 +925,22 @@ class Patcher: assert self._stubs is not None for name, modules in self.FS_MODULES.items(): for module, attr in modules: - self._stubs.smart_set( - module, name, self.fake_modules[attr]) + self._stubs.smart_set(module, name, self.fake_modules[attr]) for name, modules in self.SKIPPED_FS_MODULES.items(): for module, attr in modules: if attr in self.unfaked_modules: - self._stubs.smart_set( - module, name, self.unfaked_modules[attr]) + self._stubs.smart_set(module, name, self.unfaked_modules[attr]) + if sys.version_info >= (3, 12): + # workaround for patching open - does not work with skip modules + self._stubs.smart_set(builtins, "open", self.fake_open) def patch_defaults(self) -> None: - for (fct, idx, ft) in self.FS_DEFARGS: - method, mod_name = self._fake_module_functions[ - ft.__name__][ft.__module__] + for fct, idx, ft in self.FS_DEFARGS: + method, mod_name = self._fake_module_functions[ft.__name__][ft.__module__] fake_module = self.fake_modules[mod_name] - attr = method.__get__(fake_module, fake_module.__class__) + attr = method.__get__( + fake_module, fake_module.__class__ + ) # pytype: disable=attribute-error new_defaults = [] assert fct.__defaults__ is not None for i, d in enumerate(fct.__defaults__): @@ -834,11 +961,23 @@ class Patcher: def tearDown(self, doctester: Any = None): """Clear the fake filesystem bindings created by `setUp()`.""" + if self.is_doc_test: + self.__class__.DOC_REF_COUNT -= 1 + if self.__class__.DOC_REF_COUNT > 0: + return + else: + self.__class__.REF_COUNT -= 1 + if self.__class__.REF_COUNT > 0: + return self.stop_patching() if self.has_fcopy_file: shutil._HAS_FCOPYFILE = True # type: ignore[attr-defined] reset_ids() + if self.is_doc_test: + self.__class__.DOC_PATCHER = None + else: + self.__class__.PATCHER = None def stop_patching(self) -> None: if self._patching: @@ -852,7 +991,7 @@ class Patcher: sys.meta_path.pop(0) def unset_defaults(self) -> None: - for (fct, idx, ft) in self.FS_DEFARGS: + for fct, idx, ft in self.FS_DEFARGS: new_defaults = [] for i, d in enumerate(cast(Tuple, fct.__defaults__)): if i == idx: @@ -898,10 +1037,12 @@ class Pause: elif isinstance(caller, FakeFilesystem): self._fs = caller else: - raise ValueError('Invalid argument - should be of type ' - '"fake_filesystem_unittest.Patcher", ' - '"fake_filesystem_unittest.TestCase" ' - 'or "fake_filesystem.FakeFilesystem"') + raise ValueError( + "Invalid argument - should be of type " + '"fake_filesystem_unittest.Patcher", ' + '"fake_filesystem_unittest.TestCase" ' + 'or "fake_filesystem.FakeFilesystem"' + ) def __enter__(self) -> FakeFilesystem: self._fs.pause() @@ -939,8 +1080,9 @@ class DynamicPatcher(MetaPathFinder, Loader): for module in self._patcher.modules_to_reload: if module.__name__ in sys.modules: reload(module) - reloaded_module_names = [module.__name__ - for module in self._patcher.modules_to_reload] + reloaded_module_names = [ + module.__name__ for module in self._patcher.modules_to_reload + ] # Dereference all modules loaded during the test so they will reload on # the next use, ensuring that no faked modules are referenced after the # test. @@ -953,14 +1095,16 @@ class DynamicPatcher(MetaPathFinder, Loader): if name not in self.modules: self._loaded_module_names.add(name) return False - if (name in sys.modules and - type(sys.modules[name]) == self.modules[name]): + if name in sys.modules and type(sys.modules[name]) is self.modules[name]: return False return True - def find_spec(self, fullname: str, - path: Optional[Sequence[Union[bytes, str]]], - target: Optional[ModuleType] = None) -> Optional[ModuleSpec]: + def find_spec( + self, + fullname: str, + path: Optional[Sequence[Union[bytes, str]]], + target: Optional[ModuleType] = None, + ) -> Optional[ModuleSpec]: """Module finder.""" if self.needs_patch(fullname): return ModuleSpec(fullname, self) |