Created
April 16, 2020 18:06
-
-
Save DylanModesitt/3dc159de9987b61ff3f7766183660ac1 to your computer and use it in GitHub Desktop.
Gently import a python module, failing only at runtime if a downstream module object invokes __call__
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
gentleimport is a context manager that patches | |
__import__ to allow for softly importing mocked modules. To use gentleimport, | |
with gentleimport(): | |
import typing | |
import asdf | |
typing.Any # this is a real object as typing was a found module | |
asdf # this is a mock module as asdf was not a real module | |
asdf() # (or asdf.<anything>....()) | |
# will raise a ModuleNotFoundError to catch late usage of the library at runtime | |
""" | |
import builtins | |
import contextlib | |
import sys | |
import unittest.mock as mock | |
__all__ = ['gentleimport'] | |
class ModuleMock(mock.MagicMock): | |
""" module mock makes .<attribute> access have the same | |
side effect as the parent (which in this module is an error | |
for the invalid module) | |
""" | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
def __getattr__(self, name): | |
m = super().__getattr__(name) | |
m.side_effect = self.side_effect | |
return m | |
def _module_mock(name: str) -> mock.Mock: | |
side_effect = ModuleNotFoundError( | |
f'{name} was gently imported and could not be found - please install it' | |
) | |
return ModuleMock(name=name, side_effect=side_effect) | |
@contextlib.contextmanager | |
def gentleimport(): | |
"""gentleimport is a context manager that allows for flexible | |
importing of modules that can not be found so that import exceptions | |
can be raised upon *module use* and not on import for soft dependencies | |
Example:: | |
with gentleimport(): | |
import numpy as np | |
import asdf | |
np.ndarray # (valid if numpy is installed) | |
asdf.test # raises ModuleNotFoundError | |
""" | |
orig_import = builtins.__import__ | |
def import_mock(name, *args): | |
try: | |
return orig_import(name, *args) | |
except ModuleNotFoundError: | |
module_mock = _module_mock(name) | |
sys.modules[name] = module_mock | |
return module_mock | |
with mock.patch('builtins.__import__', side_effect=import_mock): | |
yield | |
builtins.__import__ = orig_import |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment