diff --git a/CHANGES.md b/CHANGES.md index 8425ebbb..8e46c30a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,10 @@ The released versions correspond to PyPI releases. ## Unreleased +### Enhancements +* added some support for loading fake modules in `AUTO` patch mode + using `importlib.import_module` (see [#1079](../../issues/1079)) + ### Performance * avoid reloading `tempfile` in Posix systems diff --git a/docs/usage.rst b/docs/usage.rst index e8c768eb..6c1f31d7 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -587,6 +587,20 @@ set ``patch_open_code`` to ``PatchMode.AUTO``: def test_something(fs): assert foo() +In this mode, it is also possible to import modules created in the fake filesystem +using `importlib.import_module`. Make sure that the `sys.path` contains the parent path in this case: + +.. code:: python + + @patchfs(patch_open_code=PatchMode.AUTO) + def test_fake_import(fs): + fake_module_path = Path("/") / "site-packages" / "fake_module.py" + self.fs.create_file(fake_module_path, contents="x = 5") + sys.path.insert(0, str(fake_module_path.parent)) + module = importlib.import_module("fake_module") + assert module.x == 5 + + .. _patch_default_args: patch_default_args diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py index 20c3955d..83ced2c2 100644 --- a/pyfakefs/fake_filesystem_unittest.py +++ b/pyfakefs/fake_filesystem_unittest.py @@ -49,7 +49,12 @@ import sys import tempfile import tokenize +import unittest +import warnings +from importlib import reload from importlib.abc import Loader, MetaPathFinder +from importlib.machinery import ModuleSpec +from importlib.util import spec_from_file_location, module_from_spec from types import ModuleType, TracebackType, FunctionType from typing import ( Any, @@ -66,10 +71,13 @@ ItemsView, Sequence, ) -import unittest -import warnings from unittest import TestSuite +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_legacy_modules +from pyfakefs import fake_pathlib +from pyfakefs import mox3_stubout from pyfakefs.fake_filesystem import ( set_uid, set_gid, @@ -79,17 +87,8 @@ ) from pyfakefs.fake_os import use_original_os from pyfakefs.helpers import IS_PYPY -from pyfakefs.mox3_stubout import StubOutForTesting - -from importlib.machinery import ModuleSpec -from importlib import reload - -from pyfakefs import fake_filesystem, fake_io, fake_os, fake_open, fake_path, fake_file -from pyfakefs import fake_legacy_modules -from pyfakefs import fake_filesystem_shutil -from pyfakefs import fake_pathlib -from pyfakefs import mox3_stubout from pyfakefs.legacy_packages import pathlib2, scandir +from pyfakefs.mox3_stubout import StubOutForTesting OS_MODULE = "nt" if sys.platform == "win32" else "posix" PATH_MODULE = "ntpath" if sys.platform == "win32" else "posixpath" @@ -1225,7 +1224,7 @@ def cleanup(self) -> None: del sys.modules[name] def needs_patch(self, name: str) -> bool: - """Check if the module with the given name shall be replaced.""" + """Checks if the module with the given name shall be replaced.""" if name not in self.modules: self._loaded_module_names.add(name) return False @@ -1233,6 +1232,24 @@ def needs_patch(self, name: str) -> bool: return False return True + def fake_module_path(self, name: str) -> str: + """Checks if the module with the given name is a module existing in the fake + filesystem and returns its path in this case. + """ + fs = self._patcher.fs + # we assume that the module name is the absolute module path + if fs is not None: + base_path = name.replace(".", fs.path_separator) + for path in sys.path: + module_path = fs.joinpaths(path, base_path) + py_module_path = module_path + ".py" + if fs.exists(py_module_path): + return py_module_path + init_path = fs.joinpaths(module_path, "__init__.py") + if fs.exists(init_path): + return init_path + return "" + def find_spec( self, fullname: str, @@ -1242,6 +1259,15 @@ def find_spec( """Module finder.""" if self.needs_patch(fullname): return ModuleSpec(fullname, self) + if self._patcher.patch_open_code != PatchMode.OFF: + # handle modules created in the fake filesystem + module_path = self.fake_module_path(fullname) + if module_path: + spec = spec_from_file_location(fullname, module_path) + if spec: + module = module_from_spec(spec) + sys.modules[fullname] = module + return ModuleSpec(fullname, self) return None def load_module(self, fullname: str) -> ModuleType: diff --git a/pyfakefs/tests/fake_filesystem_unittest_test.py b/pyfakefs/tests/fake_filesystem_unittest_test.py index a812c5c0..fec9a840 100644 --- a/pyfakefs/tests/fake_filesystem_unittest_test.py +++ b/pyfakefs/tests/fake_filesystem_unittest_test.py @@ -951,5 +951,33 @@ def test_write_tmp_windows(self): self.check_write_tmp_after_reset(OSType.WINDOWS) +@unittest.skipIf(sys.version_info < (3, 8), "Not available before Python 3.8") +class FakeImportTest(fake_filesystem_unittest.TestCase): + """Checks that a fake module can be imported in AUTO patch mode.""" + + def setUp(self): + self.setUpPyfakefs(patch_open_code=PatchMode.AUTO) + + def test_simple_fake_import(self): + fake_module_path = Path("/") / "site-packages" / "fake_module.py" + self.fs.create_file(fake_module_path, contents="number = 42") + sys.path.insert(0, str(fake_module_path.parent)) + module = importlib.import_module("fake_module") + del sys.path[0] + assert module.__name__ == "fake_module" + assert module.number == 42 + + def test_fake_import_dotted_module(self): + fake_pkg_path = Path("/") / "site-packages" + self.fs.create_file(fake_pkg_path / "fakepkg" / "__init__.py") + fake_module_path = fake_pkg_path / "fakepkg" / "fake_module.py" + self.fs.create_file(fake_module_path, contents="number = 42") + sys.path.insert(0, str(fake_pkg_path)) + module = importlib.import_module("fakepkg.fake_module") + del sys.path[0] + assert module.__name__ == "fakepkg.fake_module" + assert module.number == 42 + + if __name__ == "__main__": unittest.main()