Skip to content

Commit de8e415

Browse files
committed
Add some support for loading fake modules
- works only if open_code patch mode is not off - see #1079
1 parent 5647227 commit de8e415

File tree

4 files changed

+85
-13
lines changed

4 files changed

+85
-13
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ The released versions correspond to PyPI releases.
1414

1515
## Unreleased
1616

17+
### Enhancements
18+
* added some support for loading fake modules in `AUTO` patch mode
19+
using `importlib.import_module` (see [#1079](../../issues/1079))
20+
1721
### Performance
1822
* avoid reloading `tempfile` in Posix systems
1923

docs/usage.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,20 @@ set ``patch_open_code`` to ``PatchMode.AUTO``:
587587
def test_something(fs):
588588
assert foo()
589589
590+
In this mode, it is also possible to import modules created in the fake filesystem
591+
using `importlib.import_module`. Make sure that the `sys.path` contains the parent path in this case:
592+
593+
.. code:: python
594+
595+
@patchfs(patch_open_code=PatchMode.AUTO)
596+
def test_fake_import(fs):
597+
fake_module_path = Path("/") / "site-packages" / "fake_module.py"
598+
self.fs.create_file(fake_module_path, contents="x = 5")
599+
sys.path.insert(0, str(fake_module_path.parent))
600+
module = importlib.import_module("fake_module")
601+
assert module.x == 5
602+
603+
590604
.. _patch_default_args:
591605

592606
patch_default_args

pyfakefs/fake_filesystem_unittest.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@
4949
import sys
5050
import tempfile
5151
import tokenize
52+
import unittest
53+
import warnings
54+
from importlib import reload
5255
from importlib.abc import Loader, MetaPathFinder
56+
from importlib.machinery import ModuleSpec
57+
from importlib.util import spec_from_file_location, module_from_spec
5358
from types import ModuleType, TracebackType, FunctionType
5459
from typing import (
5560
Any,
@@ -66,10 +71,13 @@
6671
ItemsView,
6772
Sequence,
6873
)
69-
import unittest
70-
import warnings
7174
from unittest import TestSuite
7275

76+
from pyfakefs import fake_filesystem, fake_io, fake_os, fake_open, fake_path, fake_file
77+
from pyfakefs import fake_filesystem_shutil
78+
from pyfakefs import fake_legacy_modules
79+
from pyfakefs import fake_pathlib
80+
from pyfakefs import mox3_stubout
7381
from pyfakefs.fake_filesystem import (
7482
set_uid,
7583
set_gid,
@@ -79,17 +87,8 @@
7987
)
8088
from pyfakefs.fake_os import use_original_os
8189
from pyfakefs.helpers import IS_PYPY
82-
from pyfakefs.mox3_stubout import StubOutForTesting
83-
84-
from importlib.machinery import ModuleSpec
85-
from importlib import reload
86-
87-
from pyfakefs import fake_filesystem, fake_io, fake_os, fake_open, fake_path, fake_file
88-
from pyfakefs import fake_legacy_modules
89-
from pyfakefs import fake_filesystem_shutil
90-
from pyfakefs import fake_pathlib
91-
from pyfakefs import mox3_stubout
9290
from pyfakefs.legacy_packages import pathlib2, scandir
91+
from pyfakefs.mox3_stubout import StubOutForTesting
9392

9493
OS_MODULE = "nt" if sys.platform == "win32" else "posix"
9594
PATH_MODULE = "ntpath" if sys.platform == "win32" else "posixpath"
@@ -1225,14 +1224,32 @@ def cleanup(self) -> None:
12251224
del sys.modules[name]
12261225

12271226
def needs_patch(self, name: str) -> bool:
1228-
"""Check if the module with the given name shall be replaced."""
1227+
"""Checks if the module with the given name shall be replaced."""
12291228
if name not in self.modules:
12301229
self._loaded_module_names.add(name)
12311230
return False
12321231
if name in sys.modules and type(sys.modules[name]) is self.modules[name]:
12331232
return False
12341233
return True
12351234

1235+
def fake_module_path(self, name: str) -> str:
1236+
"""Checks if the module with the given name is a module existing in the fake
1237+
filesystem and returns its path in this case.
1238+
"""
1239+
fs = self._patcher.fs
1240+
# we assume that the module name is the absolute module path
1241+
if fs is not None:
1242+
base_path = name.replace(".", fs.path_separator)
1243+
for path in sys.path:
1244+
module_path = fs.joinpaths(path, base_path)
1245+
py_module_path = module_path + ".py"
1246+
if fs.exists(py_module_path):
1247+
return py_module_path
1248+
init_path = fs.joinpaths(module_path, "__init__.py")
1249+
if fs.exists(init_path):
1250+
return init_path
1251+
return ""
1252+
12361253
def find_spec(
12371254
self,
12381255
fullname: str,
@@ -1242,6 +1259,15 @@ def find_spec(
12421259
"""Module finder."""
12431260
if self.needs_patch(fullname):
12441261
return ModuleSpec(fullname, self)
1262+
if self._patcher.patch_open_code != PatchMode.OFF:
1263+
# handle modules created in the fake filesystem
1264+
module_path = self.fake_module_path(fullname)
1265+
if module_path:
1266+
spec = spec_from_file_location(fullname, module_path)
1267+
if spec:
1268+
module = module_from_spec(spec)
1269+
sys.modules[fullname] = module
1270+
return ModuleSpec(fullname, self)
12451271
return None
12461272

12471273
def load_module(self, fullname: str) -> ModuleType:

pyfakefs/tests/fake_filesystem_unittest_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,5 +951,33 @@ def test_write_tmp_windows(self):
951951
self.check_write_tmp_after_reset(OSType.WINDOWS)
952952

953953

954+
@unittest.skipIf(sys.version_info < (3, 8), "Not available before Python 3.8")
955+
class FakeImportTest(fake_filesystem_unittest.TestCase):
956+
"""Checks that a fake module can be imported in AUTO patch mode."""
957+
958+
def setUp(self):
959+
self.setUpPyfakefs(patch_open_code=PatchMode.AUTO)
960+
961+
def test_simple_fake_import(self):
962+
fake_module_path = Path("/") / "site-packages" / "fake_module.py"
963+
self.fs.create_file(fake_module_path, contents="number = 42")
964+
sys.path.insert(0, str(fake_module_path.parent))
965+
module = importlib.import_module("fake_module")
966+
del sys.path[0]
967+
assert module.__name__ == "fake_module"
968+
assert module.number == 42
969+
970+
def test_fake_import_dotted_module(self):
971+
fake_pkg_path = Path("/") / "site-packages"
972+
self.fs.create_file(fake_pkg_path / "fakepkg" / "__init__.py")
973+
fake_module_path = fake_pkg_path / "fakepkg" / "fake_module.py"
974+
self.fs.create_file(fake_module_path, contents="number = 42")
975+
sys.path.insert(0, str(fake_pkg_path))
976+
module = importlib.import_module("fakepkg.fake_module")
977+
del sys.path[0]
978+
assert module.__name__ == "fakepkg.fake_module"
979+
assert module.number == 42
980+
981+
954982
if __name__ == "__main__":
955983
unittest.main()

0 commit comments

Comments
 (0)