Skip to content

Commit a45fd4d

Browse files
committed
gh-148110: Resolve lazy import filter names for relative imports
1 parent 21fb9dc commit a45fd4d

File tree

7 files changed

+195
-6
lines changed

7 files changed

+195
-6
lines changed

Doc/c-api/import.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,10 @@ Importing Modules
372372
373373
Sets the current lazy imports filter. The *filter* should be a callable that
374374
will receive ``(importing_module_name, imported_module_name, [fromlist])``
375-
when an import can potentially be lazy and that must return ``True`` if
376-
the import should be lazy and ``False`` otherwise.
375+
when an import can potentially be lazy. The ``imported_module_name`` value
376+
is the resolved module name, so ``lazy from .spam import eggs`` passes
377+
``package.spam``. The callable must return ``True`` if the import should be
378+
lazy and ``False`` otherwise.
377379
378380
Return ``0`` on success and ``-1`` with an exception set otherwise.
379381

Doc/library/sys.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1788,7 +1788,9 @@ always available. Unless explicitly noted otherwise, all variables are read-only
17881788
Where:
17891789

17901790
* *importing_module* is the name of the module doing the import
1791-
* *imported_module* is the name of the module being imported
1791+
* *imported_module* is the resolved name of the module being imported
1792+
(for example, ``lazy from .spam import eggs`` passes
1793+
``package.spam``)
17921794
* *fromlist* is the tuple of names being imported (for ``from ... import``
17931795
statements), or ``None`` for regular imports
17941796

Lib/test/test_lazy_import/__init__.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,36 @@ def tearDown(self):
12051205
sys.set_lazy_imports_filter(None)
12061206
sys.set_lazy_imports("normal")
12071207

1208+
def _run_subprocess_with_modules(self, code, files):
1209+
with tempfile.TemporaryDirectory() as tmpdir:
1210+
for relpath, contents in files.items():
1211+
path = os.path.join(tmpdir, relpath)
1212+
os.makedirs(os.path.dirname(path), exist_ok=True)
1213+
with open(path, "w", encoding="utf-8") as file:
1214+
file.write(textwrap.dedent(contents))
1215+
1216+
env = os.environ.copy()
1217+
env["PYTHONPATH"] = os.pathsep.join(
1218+
entry for entry in (tmpdir, env.get("PYTHONPATH")) if entry
1219+
)
1220+
env["PYTHON_LAZY_IMPORTS"] = "normal"
1221+
1222+
result = subprocess.run(
1223+
[sys.executable, "-c", textwrap.dedent(code)],
1224+
capture_output=True,
1225+
cwd=tmpdir,
1226+
env=env,
1227+
text=True,
1228+
)
1229+
return result
1230+
1231+
def _assert_subprocess_ok(self, code, files):
1232+
result = self._run_subprocess_with_modules(code, files)
1233+
self.assertEqual(
1234+
result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}"
1235+
)
1236+
return result
1237+
12081238
def test_filter_receives_correct_arguments_for_import(self):
12091239
"""Filter should receive (importer, name, fromlist=None) for 'import x'."""
12101240
code = textwrap.dedent("""
@@ -1290,6 +1320,159 @@ def deny_filter(importer, name, fromlist):
12901320
self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
12911321
self.assertIn("EAGER", result.stdout)
12921322

1323+
def test_filter_distinguishes_absolute_and_relative_from_imports(self):
1324+
"""Relative imports should pass resolved module names to the filter."""
1325+
files = {
1326+
"target.py": """
1327+
VALUE = "absolute"
1328+
""",
1329+
"pkg/__init__.py": "",
1330+
"pkg/target.py": """
1331+
VALUE = "relative"
1332+
""",
1333+
"pkg/runner.py": """
1334+
import sys
1335+
1336+
seen = []
1337+
1338+
def my_filter(importer, name, fromlist):
1339+
seen.append((importer, name, fromlist))
1340+
return True
1341+
1342+
sys.set_lazy_imports_filter(my_filter)
1343+
1344+
lazy from target import VALUE as absolute_value
1345+
lazy from .target import VALUE as relative_value
1346+
1347+
assert seen == [
1348+
(__name__, "target", ("VALUE",)),
1349+
(__name__, "pkg.target", ("VALUE",)),
1350+
], seen
1351+
""",
1352+
}
1353+
1354+
result = self._assert_subprocess_ok(
1355+
"""
1356+
import pkg.runner
1357+
print("OK")
1358+
""",
1359+
files,
1360+
)
1361+
self.assertIn("OK", result.stdout)
1362+
1363+
def test_filter_receives_resolved_name_for_relative_package_import(self):
1364+
"""'lazy from . import x' should report the resolved package name."""
1365+
files = {
1366+
"pkg/__init__.py": "",
1367+
"pkg/sibling.py": """
1368+
VALUE = 1
1369+
""",
1370+
"pkg/runner.py": """
1371+
import sys
1372+
1373+
seen = []
1374+
1375+
def my_filter(importer, name, fromlist):
1376+
seen.append((importer, name, fromlist))
1377+
return True
1378+
1379+
sys.set_lazy_imports_filter(my_filter)
1380+
1381+
lazy from . import sibling
1382+
1383+
assert seen == [
1384+
(__name__, "pkg", ("sibling",)),
1385+
], seen
1386+
""",
1387+
}
1388+
1389+
result = self._assert_subprocess_ok(
1390+
"""
1391+
import pkg.runner
1392+
print("OK")
1393+
""",
1394+
files,
1395+
)
1396+
self.assertIn("OK", result.stdout)
1397+
1398+
def test_filter_receives_resolved_name_for_parent_relative_import(self):
1399+
"""Parent relative imports should also use the resolved module name."""
1400+
files = {
1401+
"pkg/__init__.py": "",
1402+
"pkg/target.py": """
1403+
VALUE = 1
1404+
""",
1405+
"pkg/sub/__init__.py": "",
1406+
"pkg/sub/runner.py": """
1407+
import sys
1408+
1409+
seen = []
1410+
1411+
def my_filter(importer, name, fromlist):
1412+
seen.append((importer, name, fromlist))
1413+
return True
1414+
1415+
sys.set_lazy_imports_filter(my_filter)
1416+
1417+
lazy from ..target import VALUE
1418+
1419+
assert seen == [
1420+
(__name__, "pkg.target", ("VALUE",)),
1421+
], seen
1422+
""",
1423+
}
1424+
1425+
result = self._assert_subprocess_ok(
1426+
"""
1427+
import pkg.sub.runner
1428+
print("OK")
1429+
""",
1430+
files,
1431+
)
1432+
self.assertIn("OK", result.stdout)
1433+
1434+
def test_filter_can_force_eager_only_for_resolved_relative_target(self):
1435+
"""Resolved names should let filters treat relative and absolute imports differently."""
1436+
files = {
1437+
"target.py": """
1438+
VALUE = "absolute"
1439+
""",
1440+
"pkg/__init__.py": "",
1441+
"pkg/target.py": """
1442+
VALUE = "relative"
1443+
""",
1444+
"pkg/runner.py": """
1445+
import sys
1446+
1447+
def my_filter(importer, name, fromlist):
1448+
return name != "pkg.target"
1449+
1450+
sys.set_lazy_imports_filter(my_filter)
1451+
1452+
lazy from target import VALUE as absolute_value
1453+
lazy from .target import VALUE as relative_value
1454+
1455+
assert "pkg.target" in sys.modules, sorted(
1456+
name for name in sys.modules
1457+
if name in {"target", "pkg.target"}
1458+
)
1459+
assert "target" not in sys.modules, sorted(
1460+
name for name in sys.modules
1461+
if name in {"target", "pkg.target"}
1462+
)
1463+
assert relative_value == "relative", relative_value
1464+
""",
1465+
}
1466+
1467+
result = self._assert_subprocess_ok(
1468+
"""
1469+
import pkg.runner
1470+
print("OK")
1471+
""",
1472+
files,
1473+
)
1474+
self.assertIn("OK", result.stdout)
1475+
12931476

12941477
class AdditionalSyntaxRestrictionTests(unittest.TestCase):
12951478
"""Additional syntax restriction tests per PEP 810."""
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :func:`sys.set_lazy_imports_filter` so relative lazy imports pass the
2+
resolved imported module name to the filter callback. Patch by Pablo Galindo.

Python/clinic/sysmodule.c.h

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/import.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4523,7 +4523,7 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
45234523
assert(!PyErr_Occurred());
45244524
fromlist = Py_NewRef(Py_None);
45254525
}
4526-
PyObject *args[] = {modname, name, fromlist};
4526+
PyObject *args[] = {modname, abs_name, fromlist};
45274527
PyObject *res = PyObject_Vectorcall(filter, args, 3, NULL);
45284528

45294529
Py_DECREF(modname);

Python/sysmodule.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2796,7 +2796,7 @@ The filter is a callable which disables lazy imports when they
27962796
would otherwise be enabled. Returns True if the import is still enabled
27972797
or False to disable it. The callable is called with:
27982798
2799-
(importing_module_name, imported_module_name, [fromlist])
2799+
(importing_module_name, resolved_imported_module_name, [fromlist])
28002800
28012801
Pass None to clear the filter.
28022802
[clinic start generated code]*/

0 commit comments

Comments
 (0)