]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.12] gh-117178: Recover lazy loading of self-referential modules (GH-117179) (...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Thu, 28 Mar 2024 11:15:16 +0000 (12:15 +0100)
committerGitHub <noreply@github.com>
Thu, 28 Mar 2024 11:15:16 +0000 (11:15 +0000)
Co-authored-by: Chris Markiewicz <effigies@gmail.com>
Lib/importlib/util.py
Lib/test/test_importlib/test_lazy.py
Misc/NEWS.d/next/Library/2024-03-23-14-26-18.gh-issue-117178.vTisTG.rst [new file with mode: 0644]

index f1bd06469cfc6140880cdb22daf40905b3bd3cac..3743e6aa912fefb814f63d0a80d998e0f5413187 100644 (file)
@@ -178,12 +178,11 @@ 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:
-                # The first thread comes here multiple times as it descends the
-                # call stack. The first time, it sets is_loading and triggers
-                # exec_module(), which will access module.__dict__, module.__name__,
-                # and/or module.__spec__, reentering this method. These accesses
-                # need to be allowed to proceed without triggering the load again.
-                if loader_state['is_loading'] and attr.startswith('__') and attr.endswith('__'):
+                # 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)
                 loader_state['is_loading'] = True
 
index 38ab21907b58d9dcdd9fcb73b368656bdaf81220..4d2cc4eb62b67cddb59662116b9b86ca6d3c0e21 100644 (file)
@@ -178,6 +178,24 @@ class LazyLoaderTests(unittest.TestCase):
             # Or multiple load attempts
             self.assertEqual(loader.load_count, 1)
 
+    def test_lazy_self_referential_modules(self):
+        # Directory modules with submodules that reference the parent can attempt to access
+        # the parent module during a load. Verify that this common pattern works with lazy loading.
+        # json is a good example in the stdlib.
+        json_modules = [name for name in sys.modules if name.startswith('json')]
+        with test_util.uncache(*json_modules):
+            # Standard lazy loading, unwrapped
+            spec = util.find_spec('json')
+            loader = util.LazyLoader(spec.loader)
+            spec.loader = loader
+            module = util.module_from_spec(spec)
+            sys.modules['json'] = module
+            loader.exec_module(module)
+
+            # Trigger load with attribute lookup, ensure expected behavior
+            test_load = module.loads('{}')
+            self.assertEqual(test_load, {})
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2024-03-23-14-26-18.gh-issue-117178.vTisTG.rst b/Misc/NEWS.d/next/Library/2024-03-23-14-26-18.gh-issue-117178.vTisTG.rst
new file mode 100644 (file)
index 0000000..f9c53eb
--- /dev/null
@@ -0,0 +1,2 @@
+Fix regression in lazy loading of self-referential modules, introduced in
+gh-114781.