]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-148110: Resolve lazy import filter names for relative imports (#148111)
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Mon, 6 Apr 2026 21:29:02 +0000 (22:29 +0100)
committerGitHub <noreply@github.com>
Mon, 6 Apr 2026 21:29:02 +0000 (22:29 +0100)
Doc/c-api/import.rst
Doc/library/sys.rst
Lib/test/test_lazy_import/__init__.py
Misc/NEWS.d/next/Core_and_Builtins/2026-04-04-22-20-00.gh-issue-148110.cL5x2Q.rst [new file with mode: 0644]
Python/clinic/sysmodule.c.h
Python/import.c
Python/sysmodule.c

index 367490732b994fda4375dcf3a14ec8b0d9cca4ec..e2d363b911a87c65003a3f6c00a81e141873ae7d 100644 (file)
@@ -372,8 +372,10 @@ Importing Modules
 
    Sets the current lazy imports filter. The *filter* should be a callable that
    will receive ``(importing_module_name, imported_module_name, [fromlist])``
-   when an import can potentially be lazy and that must return ``True`` if
-   the import should be lazy and ``False`` otherwise.
+   when an import can potentially be lazy. The ``imported_module_name`` value
+   is the resolved module name, so ``lazy from .spam import eggs`` passes
+   ``package.spam``. The callable must return ``True`` if the import should be
+   lazy and ``False`` otherwise.
 
    Return ``0`` on success and ``-1`` with an exception set otherwise.
 
index b1461b0cbaf5284ba3199c1a1b9f8574a9e5018d..6946eb6eeaa5fae0e737973dda004fd06b8d7326 100644 (file)
@@ -1788,7 +1788,9 @@ always available. Unless explicitly noted otherwise, all variables are read-only
    Where:
 
    * *importing_module* is the name of the module doing the import
-   * *imported_module* is the name of the module being imported
+   * *imported_module* is the resolved name of the module being imported
+     (for example, ``lazy from .spam import eggs`` passes
+     ``package.spam``)
    * *fromlist* is the tuple of names being imported (for ``from ... import``
      statements), or ``None`` for regular imports
 
index 69cb96cf4a0c1af82b541fd82b952ba06f0ae44d..a9a8cd143e0d7562d86eb0b027c6f94de2f65cac 100644 (file)
@@ -1205,6 +1205,36 @@ class FilterFunctionSignatureTests(unittest.TestCase):
         sys.set_lazy_imports_filter(None)
         sys.set_lazy_imports("normal")
 
+    def _run_subprocess_with_modules(self, code, files):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            for relpath, contents in files.items():
+                path = os.path.join(tmpdir, relpath)
+                os.makedirs(os.path.dirname(path), exist_ok=True)
+                with open(path, "w", encoding="utf-8") as file:
+                    file.write(textwrap.dedent(contents))
+
+            env = os.environ.copy()
+            env["PYTHONPATH"] = os.pathsep.join(
+                entry for entry in (tmpdir, env.get("PYTHONPATH")) if entry
+            )
+            env["PYTHON_LAZY_IMPORTS"] = "normal"
+
+            result = subprocess.run(
+                [sys.executable, "-c", textwrap.dedent(code)],
+                capture_output=True,
+                cwd=tmpdir,
+                env=env,
+                text=True,
+            )
+        return result
+
+    def _assert_subprocess_ok(self, code, files):
+        result = self._run_subprocess_with_modules(code, files)
+        self.assertEqual(
+            result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}"
+        )
+        return result
+
     def test_filter_receives_correct_arguments_for_import(self):
         """Filter should receive (importer, name, fromlist=None) for 'import x'."""
         code = textwrap.dedent("""
@@ -1290,6 +1320,159 @@ class FilterFunctionSignatureTests(unittest.TestCase):
         self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
         self.assertIn("EAGER", result.stdout)
 
+    def test_filter_distinguishes_absolute_and_relative_from_imports(self):
+        """Relative imports should pass resolved module names to the filter."""
+        files = {
+            "target.py": """
+                VALUE = "absolute"
+            """,
+            "pkg/__init__.py": "",
+            "pkg/target.py": """
+                VALUE = "relative"
+            """,
+            "pkg/runner.py": """
+                import sys
+
+                seen = []
+
+                def my_filter(importer, name, fromlist):
+                    seen.append((importer, name, fromlist))
+                    return True
+
+                sys.set_lazy_imports_filter(my_filter)
+
+                lazy from target import VALUE as absolute_value
+                lazy from .target import VALUE as relative_value
+
+                assert seen == [
+                    (__name__, "target", ("VALUE",)),
+                    (__name__, "pkg.target", ("VALUE",)),
+                ], seen
+            """,
+        }
+
+        result = self._assert_subprocess_ok(
+            """
+            import pkg.runner
+            print("OK")
+            """,
+            files,
+        )
+        self.assertIn("OK", result.stdout)
+
+    def test_filter_receives_resolved_name_for_relative_package_import(self):
+        """'lazy from . import x' should report the resolved package name."""
+        files = {
+            "pkg/__init__.py": "",
+            "pkg/sibling.py": """
+                VALUE = 1
+            """,
+            "pkg/runner.py": """
+                import sys
+
+                seen = []
+
+                def my_filter(importer, name, fromlist):
+                    seen.append((importer, name, fromlist))
+                    return True
+
+                sys.set_lazy_imports_filter(my_filter)
+
+                lazy from . import sibling
+
+                assert seen == [
+                    (__name__, "pkg", ("sibling",)),
+                ], seen
+            """,
+        }
+
+        result = self._assert_subprocess_ok(
+            """
+            import pkg.runner
+            print("OK")
+            """,
+            files,
+        )
+        self.assertIn("OK", result.stdout)
+
+    def test_filter_receives_resolved_name_for_parent_relative_import(self):
+        """Parent relative imports should also use the resolved module name."""
+        files = {
+            "pkg/__init__.py": "",
+            "pkg/target.py": """
+                VALUE = 1
+            """,
+            "pkg/sub/__init__.py": "",
+            "pkg/sub/runner.py": """
+                import sys
+
+                seen = []
+
+                def my_filter(importer, name, fromlist):
+                    seen.append((importer, name, fromlist))
+                    return True
+
+                sys.set_lazy_imports_filter(my_filter)
+
+                lazy from ..target import VALUE
+
+                assert seen == [
+                    (__name__, "pkg.target", ("VALUE",)),
+                ], seen
+            """,
+        }
+
+        result = self._assert_subprocess_ok(
+            """
+            import pkg.sub.runner
+            print("OK")
+            """,
+            files,
+        )
+        self.assertIn("OK", result.stdout)
+
+    def test_filter_can_force_eager_only_for_resolved_relative_target(self):
+        """Resolved names should let filters treat relative and absolute imports differently."""
+        files = {
+            "target.py": """
+                VALUE = "absolute"
+            """,
+            "pkg/__init__.py": "",
+            "pkg/target.py": """
+                VALUE = "relative"
+            """,
+            "pkg/runner.py": """
+                import sys
+
+                def my_filter(importer, name, fromlist):
+                    return name != "pkg.target"
+
+                sys.set_lazy_imports_filter(my_filter)
+
+                lazy from target import VALUE as absolute_value
+                lazy from .target import VALUE as relative_value
+
+                assert "pkg.target" in sys.modules, sorted(
+                    name for name in sys.modules
+                    if name in {"target", "pkg.target"}
+                )
+                assert "target" not in sys.modules, sorted(
+                    name for name in sys.modules
+                    if name in {"target", "pkg.target"}
+                )
+                assert relative_value == "relative", relative_value
+            """,
+        }
+
+        result = self._assert_subprocess_ok(
+            """
+            import pkg.runner
+            print("OK")
+            """,
+            files,
+        )
+        self.assertIn("OK", result.stdout)
+
 
 class AdditionalSyntaxRestrictionTests(unittest.TestCase):
     """Additional syntax restriction tests per PEP 810."""
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-04-22-20-00.gh-issue-148110.cL5x2Q.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-04-22-20-00.gh-issue-148110.cL5x2Q.rst
new file mode 100644 (file)
index 0000000..dc7df0e
--- /dev/null
@@ -0,0 +1,2 @@
+Fix :func:`sys.set_lazy_imports_filter` so relative lazy imports pass the
+resolved imported module name to the filter callback. Patch by Pablo Galindo.
index f8ae7f18acc80903e0530c9124520e6fb48f3a35..86e942ec2b8afbc60551c7c7f0c78669e0dadda0 100644 (file)
@@ -1830,7 +1830,7 @@ PyDoc_STRVAR(sys_set_lazy_imports_filter__doc__,
 "would otherwise be enabled. Returns True if the import is still enabled\n"
 "or False to disable it. The callable is called with:\n"
 "\n"
-"(importing_module_name, imported_module_name, [fromlist])\n"
+"(importing_module_name, resolved_imported_module_name, [fromlist])\n"
 "\n"
 "Pass None to clear the filter.");
 
@@ -2121,4 +2121,4 @@ exit:
 #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF
     #define SYS_GETANDROIDAPILEVEL_METHODDEF
 #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */
-/*[clinic end generated code: output=adbadb629b98eabf input=a9049054013a1b77]*/
+/*[clinic end generated code: output=e8333fe10c01ae66 input=a9049054013a1b77]*/
index e298fbee536c1ba7053447701d3e8159560c2492..7aa96196ec1e1055483a24327581a53cc9a3cc10 100644 (file)
@@ -4523,7 +4523,7 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
             assert(!PyErr_Occurred());
             fromlist = Py_NewRef(Py_None);
         }
-        PyObject *args[] = {modname, name, fromlist};
+        PyObject *args[] = {modname, abs_name, fromlist};
         PyObject *res = PyObject_Vectorcall(filter, args, 3, NULL);
 
         Py_DECREF(modname);
index ce9c03bda7bd575fb6bf76138077320e938b530e..408d04684a91932b24a753afe1a831d98afe761c 100644 (file)
@@ -2796,14 +2796,14 @@ The filter is a callable which disables lazy imports when they
 would otherwise be enabled. Returns True if the import is still enabled
 or False to disable it. The callable is called with:
 
-(importing_module_name, imported_module_name, [fromlist])
+(importing_module_name, resolved_imported_module_name, [fromlist])
 
 Pass None to clear the filter.
 [clinic start generated code]*/
 
 static PyObject *
 sys_set_lazy_imports_filter_impl(PyObject *module, PyObject *filter)
-/*[clinic end generated code: output=10251d49469c278c input=2eb48786bdd4ee42]*/
+/*[clinic end generated code: output=10251d49469c278c input=fd51ed8df6ab54b7]*/
 {
     if (PyImport_SetLazyImportsFilter(filter) < 0) {
         return NULL;