Skip to content

Commit

Permalink
Add some support for loading fake modules
Browse files Browse the repository at this point in the history
 - works only if open_code patch mode is not off
 - see #1079
  • Loading branch information
mrbean-bremen committed Oct 20, 2024
1 parent 5647227 commit de8e415
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 13 deletions.
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 39 additions & 13 deletions pyfakefs/fake_filesystem_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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"
Expand Down Expand Up @@ -1225,14 +1224,32 @@ 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
if name in sys.modules and type(sys.modules[name]) is self.modules[name]:
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,
Expand All @@ -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:
Expand Down
28 changes: 28 additions & 0 deletions pyfakefs/tests/fake_filesystem_unittest_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

0 comments on commit de8e415

Please sign in to comment.