https://github.com/python/cpython/commit/ca960b6f38b16a7fdf5453abc6d715cb03467279
commit: ca960b6f38b16a7fdf5453abc6d715cb03467279
branch: main
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-04-06T22:29:02+01:00
summary:

gh-148110: Resolve lazy import filter names for relative imports (#148111)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-04-04-22-20-00.gh-issue-148110.cL5x2Q.rst
M Doc/c-api/import.rst
M Doc/library/sys.rst
M Lib/test/test_lazy_import/__init__.py
M Python/clinic/sysmodule.c.h
M Python/import.c
M Python/sysmodule.c

diff --git a/Doc/c-api/import.rst b/Doc/c-api/import.rst
index 367490732b994f..e2d363b911a87c 100644
--- a/Doc/c-api/import.rst
+++ b/Doc/c-api/import.rst
@@ -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.
 
diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst
index b1461b0cbaf528..6946eb6eeaa5fa 100644
--- a/Doc/library/sys.rst
+++ b/Doc/library/sys.rst
@@ -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
 
diff --git a/Lib/test/test_lazy_import/__init__.py 
b/Lib/test/test_lazy_import/__init__.py
index 69cb96cf4a0c1a..a9a8cd143e0d75 100644
--- a/Lib/test/test_lazy_import/__init__.py
+++ b/Lib/test/test_lazy_import/__init__.py
@@ -1205,6 +1205,36 @@ def tearDown(self):
         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 @@ def deny_filter(importer, name, fromlist):
         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
index 00000000000000..dc7df0e4a299c9
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-04-22-20-00.gh-issue-148110.cL5x2Q.rst
@@ -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.
diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h
index f8ae7f18acc809..86e942ec2b8afb 100644
--- a/Python/clinic/sysmodule.c.h
+++ b/Python/clinic/sysmodule.c.h
@@ -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 @@ _jit_is_active(PyObject *module, PyObject 
*Py_UNUSED(ignored))
 #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]*/
diff --git a/Python/import.c b/Python/import.c
index e298fbee536c1b..7aa96196ec1e10 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -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);
diff --git a/Python/sysmodule.c b/Python/sysmodule.c
index ce9c03bda7bd57..408d04684a9193 100644
--- a/Python/sysmodule.c
+++ b/Python/sysmodule.c
@@ -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;

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to