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]