Issue #26186: Remove the restriction that built-in and extension
modules can't be lazily loaded.
Thanks to Python 3.6 allowing for types.ModuleType to have its
__class__ mutated, the restriction can be lifted by calling
create_module() on the wrapped loader.
diff --git a/Lib/importlib/util.py b/Lib/importlib/util.py
index a9d0f1e..6bdf0d4 100644
--- a/Lib/importlib/util.py
+++ b/Lib/importlib/util.py
@@ -204,11 +204,6 @@
return module_for_loader_wrapper
-class _Module(types.ModuleType):
-
- """A subclass of the module type to allow __class__ manipulation."""
-
-
class _LazyModule(types.ModuleType):
"""A subclass of the module type which triggers loading upon attribute access."""
@@ -218,13 +213,14 @@
# All module metadata must be garnered from __spec__ in order to avoid
# using mutated values.
# Stop triggering this method.
- self.__class__ = _Module
+ self.__class__ = types.ModuleType
# Get the original name to make sure no object substitution occurred
# in sys.modules.
original_name = self.__spec__.name
# Figure out exactly what attributes were mutated between the creation
# of the module and now.
- attrs_then = self.__spec__.loader_state
+ attrs_then = self.__spec__.loader_state['__dict__']
+ original_type = self.__spec__.loader_state['__class__']
attrs_now = self.__dict__
attrs_updated = {}
for key, value in attrs_now.items():
@@ -239,9 +235,9 @@
# object was put into sys.modules.
if original_name in sys.modules:
if id(self) != id(sys.modules[original_name]):
- msg = ('module object for {!r} substituted in sys.modules '
- 'during a lazy load')
- raise ValueError(msg.format(original_name))
+ raise ValueError(f"module object for {original_name!r} "
+ "substituted in sys.modules during a lazy "
+ "load")
# Update after loading since that's what would happen in an eager
# loading situation.
self.__dict__.update(attrs_updated)
@@ -275,8 +271,7 @@
self.loader = loader
def create_module(self, spec):
- """Create a module which can have its __class__ manipulated."""
- return _Module(spec.name)
+ return self.loader.create_module(spec)
def exec_module(self, module):
"""Make the module load lazily."""
@@ -286,5 +281,8 @@
# on an object would have triggered the load,
# e.g. ``module.__spec__.loader = None`` would trigger a load from
# trying to access module.__spec__.
- module.__spec__.loader_state = module.__dict__.copy()
+ loader_state = {}
+ loader_state['__dict__'] = module.__dict__.copy()
+ loader_state['__class__'] = module.__class__
+ module.__spec__.loader_state = loader_state
module.__class__ = _LazyModule
diff --git a/Lib/test/test_importlib/test_lazy.py b/Lib/test/test_importlib/test_lazy.py
index cc383c2..ffd8dc6 100644
--- a/Lib/test/test_importlib/test_lazy.py
+++ b/Lib/test/test_importlib/test_lazy.py
@@ -66,6 +66,8 @@
spec = util.spec_from_loader(TestingImporter.module_name,
util.LazyLoader(loader))
module = spec.loader.create_module(spec)
+ if module is None:
+ module = types.ModuleType(TestingImporter.module_name)
module.__spec__ = spec
module.__loader__ = spec.loader
spec.loader.exec_module(module)