diff -r 5641c0b50072 Lib/importlib/util.py --- a/Lib/importlib/util.py Thu Feb 06 09:49:53 2014 -0500 +++ b/Lib/importlib/util.py Thu Feb 06 14:01:24 2014 -0500 @@ -200,3 +200,57 @@ return fxn(self, module, *args, **kwargs) return module_for_loader_wrapper + + +from importlib import abc +import types + +class Module(types.ModuleType): + + """A subclass of the module type to be used in lazy loading after loading.""" + +class LazyModule(types.ModuleType): + + """A subclass of the module type which upon attribute access triggers the module's loading.""" + + def __getattribute__(self, attr): + """Trigger the load of the module and return the attribute.""" + # Stop triggering this method. + self.__class__ = Module + attributes = self.__dict__.copy() + self.__loader__.exec_module(self) + # Don't need to worry about deep-copying as trying to set an attribute + # on an object would have triggered the load, + # e.g. ``module.__spec__.loader = None`` would trigger a load from + # trying to access module.__spec__. + self.__dict__.update(attributes) + return getattr(self, attr) + + +class LazyLoaderFactory: + + """Create lazy loaders tied to a specific loader instance.""" + + def __init__(self, loader_class): + self.loader_class = loader_class + + def __call__(self, *args, **kwargs): + return LazyLoader(self.loader_class(*args, **kwargs)) + + +class LazyLoader(abc.Loader): + + """A loader which creates a module which upon attribute access triggers its loading.""" + + def __init__(self, loader): + self.loader = loader + + def create_module(self, spec): + """Create a module which is eager so that import can set attributes on it.""" + return Module(spec.name) + + def exec_module(self, module): + """Make the module load lazily.""" + module.__spec__.loader = self.loader + module.__loader__ = self.loader + module.__class__ = LazyModule diff -r 5641c0b50072 Lib/test/test_importlib/test_lazy.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/test/test_importlib/test_lazy.py Thu Feb 06 14:01:24 2014 -0500 @@ -0,0 +1,93 @@ +import importlib +from importlib import abc +from importlib import util +import unittest + +from . import util as test_util + +class CollectInit: + + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + +class LazyLoaderFactoryTests(unittest.TestCase): + + def test_init(self): + factory = util.LazyLoaderFactory(CollectInit) + # E.g. what importlib.machinery.FileFinder instantiates loaders with + # plus keyword arguments. + lazy_loader = factory('module name', 'module path', kw='kw') + loader = lazy_loader.loader + self.assertEqual(('module name', 'module path'), loader.args) + self.assertEqual({'kw': 'kw'}, loader.kwargs) + + +class TestingImporter(abc.MetaPathFinder, abc.Loader): + + module_name = 'lazy_loader_test' + loaded = None + + def find_spec(self, name, path, target=None): + if name != self.module_name: + return None + return util.spec_from_loader(name, util.LazyLoader(self)) + + def exec_module(self, module): + exec('attr = 42', module.__dict__) + self.loaded = module + + +class LazyLoaderTests(unittest.TestCase): + + def new_module(self): + loader = TestingImporter() + spec = util.spec_from_loader(TestingImporter.module_name, + util.LazyLoader(loader)) + module = spec.loader.create_module(spec) + module.__spec__ = spec + module.__loader__ = spec.loader + spec.loader.exec_module(module) + # Module is now lazy. + self.assertIsNone(loader.loaded) + return module + + + def test_e2e(self): + # End-to-end test to verify the load is in fact lazy. + importer = TestingImporter() + assert importer.loaded is None + with test_util.uncache(importer.module_name): + with test_util.import_state(meta_path=[importer]): + module = importlib.import_module(importer.module_name) + self.assertIsNone(importer.loaded) + # Trigger load. + self.assertEqual(importer.module_name, module.__name__) + self.assertIsNotNone(importer.loaded) + self.assertEqual(module, importer.loaded) + + + def test_new_attr(self): + # A new attribute should persist. + module = self.new_module() + module.new_attr = 42 + self.assertEqual(42, module.new_attr) + + def test_mutated_preexisting_attr(self): + # Changing an attribute that already existed on the module -- + # e.g. __name__ -- should persist. + module = self.new_module() + module.__name__ = 'bogus' + self.assertEqual('bogus', module.__name__) + + def test_mutated_attr(self): + # Changing an attribute that comes into existence after an import + # should persist. + module = self.new_module() + module.attr = 6 + self.assertEqual(6, module.attr) + + +if __name__ == '__main__': + unittest.main()