]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-117182: Allow lazily loaded modules to modify their own __class__
authorChris Markiewicz <effigies@gmail.com>
Tue, 9 Apr 2024 03:08:48 +0000 (23:08 -0400)
committerGitHub <noreply@github.com>
Tue, 9 Apr 2024 03:08:48 +0000 (04:08 +0100)
Lib/importlib/util.py
Lib/test/test_importlib/test_lazy.py
Misc/NEWS.d/next/Library/2024-03-23-12-28-05.gh-issue-117182.a0KANW.rst [new file with mode: 0644]

index f1bb4b1fb415766b3b0f528b6f7a327017250ca2..c94a148e4c50e0fa0d25ba01632ca78d40b8efe1 100644 (file)
@@ -178,15 +178,17 @@ class _LazyModule(types.ModuleType):
             # Only the first thread to get the lock should trigger the load
             # and reset the module's class. The rest can now getattr().
             if object.__getattribute__(self, '__class__') is _LazyModule:
+                __class__ = loader_state['__class__']
+
                 # Reentrant calls from the same thread must be allowed to proceed without
                 # triggering the load again.
                 # exec_module() and self-referential imports are the primary ways this can
                 # happen, but in any case we must return something to avoid deadlock.
                 if loader_state['is_loading']:
-                    return object.__getattribute__(self, attr)
+                    return __class__.__getattribute__(self, attr)
                 loader_state['is_loading'] = True
 
-                __dict__ = object.__getattribute__(self, '__dict__')
+                __dict__ = __class__.__getattribute__(self, '__dict__')
 
                 # All module metadata must be gathered from __spec__ in order to avoid
                 # using mutated values.
@@ -216,8 +218,10 @@ class _LazyModule(types.ModuleType):
                 # Update after loading since that's what would happen in an eager
                 # loading situation.
                 __dict__.update(attrs_updated)
-                # Finally, stop triggering this method.
-                self.__class__ = types.ModuleType
+                # Finally, stop triggering this method, if the module did not
+                # already update its own __class__.
+                if isinstance(self, _LazyModule):
+                    object.__setattr__(self, '__class__', __class__)
 
         return getattr(self, attr)
 
index 4d2cc4eb62b67cddb59662116b9b86ca6d3c0e21..5c6e030352890601b4add03f79c537063f09c5d3 100644 (file)
@@ -196,6 +196,34 @@ class LazyLoaderTests(unittest.TestCase):
             test_load = module.loads('{}')
             self.assertEqual(test_load, {})
 
+    def test_lazy_module_type_override(self):
+        # Verify that lazy loading works with a module that modifies
+        # its __class__ to be a custom type.
+
+        # Example module from PEP 726
+        module = self.new_module(source_code="""\
+import sys
+from types import ModuleType
+
+CONSTANT = 3.14
+
+class ImmutableModule(ModuleType):
+    def __setattr__(self, name, value):
+        raise AttributeError('Read-only attribute!')
+
+    def __delattr__(self, name):
+        raise AttributeError('Read-only attribute!')
+
+sys.modules[__name__].__class__ = ImmutableModule
+""")
+        sys.modules[TestingImporter.module_name] = module
+        self.assertIsInstance(module, util._LazyModule)
+        self.assertEqual(module.CONSTANT, 3.14)
+        with self.assertRaises(AttributeError):
+            module.CONSTANT = 2.71
+        with self.assertRaises(AttributeError):
+            del module.CONSTANT
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2024-03-23-12-28-05.gh-issue-117182.a0KANW.rst b/Misc/NEWS.d/next/Library/2024-03-23-12-28-05.gh-issue-117182.a0KANW.rst
new file mode 100644 (file)
index 0000000..6b3b841
--- /dev/null
@@ -0,0 +1,2 @@
+Lazy-loading of modules that modify their own ``__class__`` no longer
+reverts the ``__class__`` to :class:`types.ModuleType`.