1import importlib 2from importlib import abc 3from importlib import util 4import sys 5import types 6import unittest 7 8from . import util as test_util 9 10 11class CollectInit: 12 13 def __init__(self, *args, **kwargs): 14 self.args = args 15 self.kwargs = kwargs 16 17 def exec_module(self, module): 18 return self 19 20 21class LazyLoaderFactoryTests(unittest.TestCase): 22 23 def test_init(self): 24 factory = util.LazyLoader.factory(CollectInit) 25 # E.g. what importlib.machinery.FileFinder instantiates loaders with 26 # plus keyword arguments. 27 lazy_loader = factory('module name', 'module path', kw='kw') 28 loader = lazy_loader.loader 29 self.assertEqual(('module name', 'module path'), loader.args) 30 self.assertEqual({'kw': 'kw'}, loader.kwargs) 31 32 def test_validation(self): 33 # No exec_module(), no lazy loading. 34 with self.assertRaises(TypeError): 35 util.LazyLoader.factory(object) 36 37 38class TestingImporter(abc.MetaPathFinder, abc.Loader): 39 40 module_name = 'lazy_loader_test' 41 mutated_name = 'changed' 42 loaded = None 43 source_code = 'attr = 42; __name__ = {!r}'.format(mutated_name) 44 45 def find_spec(self, name, path, target=None): 46 if name != self.module_name: 47 return None 48 return util.spec_from_loader(name, util.LazyLoader(self)) 49 50 def exec_module(self, module): 51 exec(self.source_code, module.__dict__) 52 self.loaded = module 53 54 55class LazyLoaderTests(unittest.TestCase): 56 57 def test_init(self): 58 with self.assertRaises(TypeError): 59 # Classes that don't define exec_module() trigger TypeError. 60 util.LazyLoader(object) 61 62 def new_module(self, source_code=None): 63 loader = TestingImporter() 64 if source_code is not None: 65 loader.source_code = source_code 66 spec = util.spec_from_loader(TestingImporter.module_name, 67 util.LazyLoader(loader)) 68 module = spec.loader.create_module(spec) 69 if module is None: 70 module = types.ModuleType(TestingImporter.module_name) 71 module.__spec__ = spec 72 module.__loader__ = spec.loader 73 spec.loader.exec_module(module) 74 # Module is now lazy. 75 self.assertIsNone(loader.loaded) 76 return module 77 78 def test_e2e(self): 79 # End-to-end test to verify the load is in fact lazy. 80 importer = TestingImporter() 81 assert importer.loaded is None 82 with test_util.uncache(importer.module_name): 83 with test_util.import_state(meta_path=[importer]): 84 module = importlib.import_module(importer.module_name) 85 self.assertIsNone(importer.loaded) 86 # Trigger load. 87 self.assertEqual(module.__loader__, importer) 88 self.assertIsNotNone(importer.loaded) 89 self.assertEqual(module, importer.loaded) 90 91 def test_attr_unchanged(self): 92 # An attribute only mutated as a side-effect of import should not be 93 # changed needlessly. 94 module = self.new_module() 95 self.assertEqual(TestingImporter.mutated_name, module.__name__) 96 97 def test_new_attr(self): 98 # A new attribute should persist. 99 module = self.new_module() 100 module.new_attr = 42 101 self.assertEqual(42, module.new_attr) 102 103 def test_mutated_preexisting_attr(self): 104 # Changing an attribute that already existed on the module -- 105 # e.g. __name__ -- should persist. 106 module = self.new_module() 107 module.__name__ = 'bogus' 108 self.assertEqual('bogus', module.__name__) 109 110 def test_mutated_attr(self): 111 # Changing an attribute that comes into existence after an import 112 # should persist. 113 module = self.new_module() 114 module.attr = 6 115 self.assertEqual(6, module.attr) 116 117 def test_delete_eventual_attr(self): 118 # Deleting an attribute should stay deleted. 119 module = self.new_module() 120 del module.attr 121 self.assertFalse(hasattr(module, 'attr')) 122 123 def test_delete_preexisting_attr(self): 124 module = self.new_module() 125 del module.__name__ 126 self.assertFalse(hasattr(module, '__name__')) 127 128 def test_module_substitution_error(self): 129 with test_util.uncache(TestingImporter.module_name): 130 fresh_module = types.ModuleType(TestingImporter.module_name) 131 sys.modules[TestingImporter.module_name] = fresh_module 132 module = self.new_module() 133 with self.assertRaisesRegex(ValueError, "substituted"): 134 module.__name__ 135 136 def test_module_already_in_sys(self): 137 with test_util.uncache(TestingImporter.module_name): 138 module = self.new_module() 139 sys.modules[TestingImporter.module_name] = module 140 # Force the load; just care that no exception is raised. 141 module.__name__ 142 143 144if __name__ == '__main__': 145 unittest.main() 146