Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Doc/library/importlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,16 @@ an :term:`importer`.
compatibility warning for :class:`importlib.machinery.BuiltinImporter` and
:class:`importlib.machinery.ExtensionFileLoader`.

.. versionchanged:: 3.16
Reading a lazily-loaded module's :attr:`~module.__name__` and
:attr:`~module.__file__` attributes no longer triggers the load. Until
the module is loaded they act as aliases for the
:attr:`~importlib.machinery.ModuleSpec.name` and
:attr:`~importlib.machinery.ModuleSpec.origin` of its
:attr:`~module.__spec__`, and assigning to them updates the spec
instead. This avoids the unintentional loading caused by
introspection tools like :func:`inspect.getframeinfo` (:gh:`139669`).

.. classmethod:: factory(loader)

A class method which returns a callable that creates a lazy loader. This
Expand Down Expand Up @@ -1482,6 +1492,11 @@ The example below shows how to implement lazy imports::
>>> lazy_typing.TYPE_CHECKING
False

Reading the module's :attr:`~module.__name__` or :attr:`~module.__file__`
does not trigger the load; until then they mirror the module's
:attr:`~module.__spec__`. Accessing any other attribute (such as
``TYPE_CHECKING`` above) loads the module.


Setting up an importer
''''''''''''''''''''''
Expand Down
22 changes: 19 additions & 3 deletions Lib/importlib/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ class _LazyModule(types.ModuleType):
def __getattribute__(self, attr):
"""Trigger the load of the module and return the attribute."""
__spec__ = object.__getattribute__(self, '__spec__')

# gh-139669: avoid triggering lazy loading from these 2 attrs
if attr == "__name__":
return __spec__.name
if attr == "__file__":
return __spec__.origin

loader_state = __spec__.loader_state
with loader_state['lock']:
# Only the first thread to get the lock should trigger the load
Expand Down Expand Up @@ -223,11 +230,20 @@ def __getattribute__(self, attr):

return getattr(self, attr)

def __setattr__(self, attr, value):
"""Keep __name__/__file__ in sync with __spec__ without loading."""
__spec__ = object.__getattribute__(self, '__spec__')
if attr == "__name__":
__spec__.name = value
elif attr == "__file__":
__spec__.origin = value
else:
object.__setattr__(self, attr, value)

def __delattr__(self, attr):
"""Trigger the load and then perform the deletion."""
# To trigger the load and raise an exception if the attribute
# doesn't exist.
self.__getattribute__(attr)
self.__getattribute__('__spec__')
# Goes into ModuleType.__delattr__
delattr(self, attr)


Expand Down
22 changes: 20 additions & 2 deletions Lib/test/test_importlib/test_lazy.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import importlib
from importlib import abc
from importlib import util
import inspect
import sys
import time
import threading
Expand Down Expand Up @@ -99,6 +100,9 @@ def test_attr_unchanged(self):
# An attribute only mutated as a side-effect of import should not be
# changed needlessly.
module = self.new_module()
# __name__ itself doesn't trigger the lazy loading,
# trigger via another attr
module.__package__
self.assertEqual(TestingImporter.mutated_name, module.__name__)

def test_new_attr(self):
Expand Down Expand Up @@ -138,14 +142,14 @@ def test_module_substitution_error(self):
sys.modules[TestingImporter.module_name] = fresh_module
module = self.new_module()
with self.assertRaisesRegex(ValueError, "substituted"):
module.__name__
module.__package__

def test_module_already_in_sys(self):
with test_util.uncache(TestingImporter.module_name):
module = self.new_module()
sys.modules[TestingImporter.module_name] = module
# Force the load; just care that no exception is raised.
module.__name__
module.__package__

@threading_helper.requires_working_threading()
def test_module_load_race(self):
Expand Down Expand Up @@ -224,6 +228,20 @@ def __delattr__(self, name):
with self.assertRaises(AttributeError):
del module.CONSTANT

def test_inspect_does_not_trigger_lazy_load(self):
# gh-139669: introspecting an unrelated frame iterates over
# sys.modules and must not force lazy modules to be loaded.
loader = TestingImporter()
module = self.new_module(loader=loader)
with test_util.uncache(TestingImporter.module_name):
sys.modules[TestingImporter.module_name] = module
self.assertIsInstance(module, util._LazyModule)

inspect.getframeinfo(inspect.currentframe())
self.assertIsNone(loader.loaded)
self.assertEqual(0, loader.load_count)
self.assertIsInstance(module, util._LazyModule)


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Reading the :attr:`~module.__name__` and :attr:`~module.__file__` attributes
of a module created by :class:`importlib.util.LazyLoader` no longer triggers
the load. While the module remains unloaded these attributes are aliases for
its :attr:`~module.__spec__`, and assigning to them updates the spec. This
prevents introspection tools such as :func:`inspect.getframeinfo`, via
:func:`inspect.getmodule`, from forcing every lazily-loaded module in
:data:`sys.modules` to be imported.
Loading