]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-149642: Fix interaction between exec and lazy_imports=all (#149643)
authorJelle Zijlstra <jelle.zijlstra@gmail.com>
Wed, 13 May 2026 02:59:09 +0000 (19:59 -0700)
committerGitHub <noreply@github.com>
Wed, 13 May 2026 02:59:09 +0000 (19:59 -0700)
Lib/test/test_lazy_import/__init__.py
Misc/NEWS.d/next/Core_and_Builtins/2026-05-10-07-42-36.gh-issue-149642.6ZksML.rst [new file with mode: 0644]
Python/ceval.c

index ea534a8ee5b981129a3a466bc3073a0794814588..5d770eeae07a15f3b7f71e825ed851bceb2e1195 100644 (file)
@@ -301,6 +301,15 @@ class SyntaxRestrictionTests(unittest.TestCase):
             f()
         self.assertIn("only allowed at module level", str(cm.exception))
 
+    def test_lazy_import_exec_in_class(self):
+        """lazy import via exec() inside a class should raise SyntaxError."""
+        # exec() inside a class body also has non-module-level locals.
+        with self.assertRaises(SyntaxError) as cm:
+            class C:
+                exec("lazy import json")
+
+        self.assertIn("only allowed at module level", str(cm.exception))
+
     @support.requires_subprocess()
     def test_lazy_import_exec_at_module_level(self):
         """lazy import via exec() at module level should work."""
@@ -352,6 +361,50 @@ class EagerImportInLazyModeTests(unittest.TestCase):
         f = test.test_lazy_import.data.eager_import_func.f
         self.assertEqual(type(f()), type(sys))
 
+    def test_exec_import_func(self):
+        """Implicit lazy imports via exec() inside functions should be eager."""
+        sys.set_lazy_imports("all")
+
+        def f():
+            exec("import test.test_lazy_import.data.basic2")
+
+        f()
+        self.assertIn("test.test_lazy_import.data.basic2", sys.modules)
+
+    def test_exec_import_func_with_lazy_modules(self):
+        """__lazy_modules__ should not make exec() imports lazy inside functions."""
+        globals()["__lazy_modules__"] = ["test.test_lazy_import.data.basic2"]
+        try:
+            def f():
+                exec("import test.test_lazy_import.data.basic2")
+
+            f()
+            self.assertIn("test.test_lazy_import.data.basic2", sys.modules)
+        finally:
+            del globals()["__lazy_modules__"]
+
+    def test_exec_import_class(self):
+        """Implicit lazy imports via exec() inside classes should be eager."""
+        sys.set_lazy_imports("all")
+
+        class C:
+            exec("import test.test_lazy_import.data.basic2")
+
+        self.assertIsNotNone(C)
+        self.assertIn("test.test_lazy_import.data.basic2", sys.modules)
+
+    def test_exec_import_class_with_lazy_modules(self):
+        """__lazy_modules__ should not make exec() imports lazy inside classes."""
+        globals()["__lazy_modules__"] = ["test.test_lazy_import.data.basic2"]
+        try:
+            class C:
+                exec("import test.test_lazy_import.data.basic2")
+
+            self.assertIsNotNone(C)
+            self.assertIn("test.test_lazy_import.data.basic2", sys.modules)
+        finally:
+            del globals()["__lazy_modules__"]
+
 
 class WithStatementTests(unittest.TestCase):
     """Tests for lazy imports in with statement context."""
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-10-07-42-36.gh-issue-149642.6ZksML.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-10-07-42-36.gh-issue-149642.6ZksML.rst
new file mode 100644 (file)
index 0000000..815a084
--- /dev/null
@@ -0,0 +1,2 @@
+Allow imports inside ``exec()`` calls within functions under
+``PYTHON_LAZY_IMPORTS=all``.
index 060e948e6b01c9f0db59717363e72f45f0657ae8..a080ae42b937667f79c114058001bb91de5b8a0e 100644 (file)
@@ -3059,25 +3059,35 @@ error:
     return res;
 }
 
+static int
+is_lazy_import_module_level(void)
+{
+    _PyInterpreterFrame *frame = _PyEval_GetFrame();
+    return frame != NULL && frame->f_globals == frame->f_locals;
+}
+
 PyObject *
 _PyEval_LazyImportName(PyThreadState *tstate, PyObject *builtins,
                        PyObject *globals, PyObject *locals, PyObject *name,
                        PyObject *fromlist, PyObject *level, int lazy)
 {
     PyObject *res = NULL;
+    PyImport_LazyImportsMode mode = PyImport_GetLazyImportsMode();
     // Check if global policy overrides the local syntax
-    switch (PyImport_GetLazyImportsMode()) {
+    switch (mode) {
         case PyImport_LAZY_NONE:
             lazy = 0;
             break;
         case PyImport_LAZY_ALL:
-            lazy = 1;
+            if (!lazy) {
+                lazy = is_lazy_import_module_level();
+            }
             break;
         case PyImport_LAZY_NORMAL:
             break;
     }
 
-    if (!lazy && PyImport_GetLazyImportsMode() != PyImport_LAZY_NONE) {
+    if (!lazy && mode != PyImport_LAZY_NONE && is_lazy_import_module_level()) {
         // See if __lazy_modules__ forces this to be lazy.
         lazy = check_lazy_import_compatibility(tstate, globals, name, level);
         if (lazy < 0) {