Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some support for loading fake modules #1080

Merged
merged 1 commit into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()