Skip to content

Commit 1e26ba3

Browse files
brittanyreypablogsalencukou
authored andcommitted
gh-155695: Remove resolved names from sys.lazy_modules more consistently (GH-157714)
(cherry picked from commit be87a85) Co-authored-by: Brittany Reynoso <breynoso@meta.com> Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> Co-authored-by: Petr Viktorin <encukou@gmail.com>
1 parent 06ef3d4 commit 1e26ba3

7 files changed

Lines changed: 345 additions & 14 deletions

File tree

‎Include/internal/pycore_import.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ extern void _PyImport_ClearModulesByIndex(PyInterpreterState *interp);
8787
extern PyObject * _PyImport_InitLazyModules(
8888
PyInterpreterState *interp);
8989
extern void _PyImport_ClearLazyModules(PyInterpreterState *interp);
90+
extern int _PyImport_DiscardLazyModule(
91+
PyInterpreterState *interp, PyObject *name);
9092

9193
extern int _PyImport_InitDefaultImportFunc(PyInterpreterState *interp);
9294
extern int _PyImport_IsDefaultImportFunc(

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 258 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ def test_sys_lazy_modules(self):
5353
self.fail('lazy import failed')
5454

5555
self.assertFalse("test.test_lazy_import.data.basic2" in sys.modules)
56-
self.assertIn("test.test_lazy_import.data", sys.lazy_modules)
56+
# The package is already loaded, so it is not a pending import.
57+
self.assertNotIn("test.test_lazy_import.data", sys.lazy_modules)
5758
self.assertIn("test.test_lazy_import.data.basic2", sys.lazy_modules)
5859
test.test_lazy_import.data.basic_from_unused.basic2
5960
self.assertNotIn("test.test_import.data", sys.lazy_modules)
@@ -1313,6 +1314,262 @@ def test_lazy_module_without_children_is_tracked(self):
13131314
""")
13141315
assert_python_ok("-c", code)
13151316

1317+
def test_already_loaded_module_is_not_tracked(self):
1318+
"""A lazy import of an already loaded module should not be tracked."""
1319+
code = textwrap.dedent("""
1320+
import sys
1321+
1322+
# Loaded by a regular import.
1323+
import json
1324+
lazy import json as lazy_json
1325+
assert "json" not in sys.lazy_modules, (
1326+
f"expected 'json' not in sys.lazy_modules, got {sys.lazy_modules}"
1327+
)
1328+
1329+
# Loaded by reifying an earlier lazy import.
1330+
lazy import base64
1331+
_ = base64.b64encode
1332+
lazy import base64 as lazy_base64
1333+
assert "base64" not in sys.lazy_modules, (
1334+
f"expected 'base64' not in sys.lazy_modules, got {sys.lazy_modules}"
1335+
)
1336+
""")
1337+
assert_python_ok("-c", code)
1338+
1339+
def test_already_loaded_submodule_is_not_tracked(self):
1340+
"""`lazy from` a loaded submodule should not be tracked either."""
1341+
code = textwrap.dedent("""
1342+
import sys
1343+
import test.test_lazy_import.data.pkg.b
1344+
lazy from test.test_lazy_import.data.pkg import b
1345+
assert "test.test_lazy_import.data.pkg.b" not in sys.lazy_modules, (
1346+
f"expected 'pkg.b' untracked, got {sys.lazy_modules}"
1347+
)
1348+
""")
1349+
assert_python_ok("-c", code)
1350+
1351+
def test_attribute_entry_removed_on_reification(self):
1352+
"""`lazy from x import attr` should untrack "x.attr" once resolved."""
1353+
code = textwrap.dedent("""
1354+
import sys
1355+
lazy from test.test_lazy_import.data.basic2 import x
1356+
assert "test.test_lazy_import.data.basic2.x" in sys.lazy_modules, (
1357+
f"expected 'basic2.x' tracked, got {sys.lazy_modules}"
1358+
)
1359+
_ = x
1360+
assert "test.test_lazy_import.data.basic2.x" not in sys.lazy_modules, (
1361+
f"expected 'basic2.x' untracked, got {sys.lazy_modules}"
1362+
)
1363+
""")
1364+
assert_python_ok("-c", code)
1365+
1366+
def test_already_loaded_attribute_is_not_tracked(self):
1367+
code = textwrap.dedent("""
1368+
import sys
1369+
import math
1370+
1371+
lazy from math import pi
1372+
assert "math.pi" not in sys.lazy_modules, sys.lazy_modules
1373+
assert pi == math.pi
1374+
""")
1375+
assert_python_ok("-c", code)
1376+
1377+
def test_cached_lazy_attribute_tracks_its_target(self):
1378+
code = textwrap.dedent("""
1379+
import sys
1380+
import test.test_lazy_import.data.basic_from_unused
1381+
1382+
holder = "test.test_lazy_import.data.basic_from_unused"
1383+
target = "test.test_lazy_import.data.basic2"
1384+
1385+
namespace = {"__name__": "cached_import_test"}
1386+
exec(f"lazy from {holder} import basic2", namespace)
1387+
assert holder + ".basic2" not in sys.lazy_modules, sys.lazy_modules
1388+
assert target in sys.lazy_modules, sys.lazy_modules
1389+
exec("assert basic2.x == 42", namespace)
1390+
assert target not in sys.lazy_modules, sys.lazy_modules
1391+
""")
1392+
assert_python_ok("-c", code)
1393+
1394+
def test_cached_attribute_keeps_pending_module_tracked(self):
1395+
code = textwrap.dedent("""
1396+
import sys
1397+
import test.test_lazy_import.data.pkg as pkg
1398+
pkg.b = 42
1399+
1400+
lazy import test.test_lazy_import.data.pkg.b as pending
1401+
lazy from test.test_lazy_import.data.pkg import b
1402+
name = "test.test_lazy_import.data.pkg.b"
1403+
assert b == 42, b
1404+
assert name not in sys.modules, sys.modules
1405+
assert name in sys.lazy_modules, sys.lazy_modules
1406+
assert pending.foo() == "foo"
1407+
assert name not in sys.lazy_modules, sys.lazy_modules
1408+
""")
1409+
assert_python_ok("-c", code)
1410+
1411+
def test_failed_reification_stays_tracked(self):
1412+
"""A lazy import that fails to resolve must stay tracked."""
1413+
code = textwrap.dedent("""
1414+
import sys
1415+
lazy import test.test_lazy_import.data.broken_module
1416+
try:
1417+
_ = test.test_lazy_import.data.broken_module
1418+
except ValueError:
1419+
pass
1420+
else:
1421+
raise AssertionError("ValueError was not raised")
1422+
assert "test.test_lazy_import.data.broken_module" in sys.lazy_modules, (
1423+
f"failed reification must stay tracked, got {sys.lazy_modules}"
1424+
)
1425+
""")
1426+
assert_python_ok("-c", code)
1427+
1428+
def test_blocked_module_is_still_tracked(self):
1429+
"""A ``None`` entry in sys.modules must not count as loaded."""
1430+
code = textwrap.dedent("""
1431+
import sys
1432+
sys.modules['test.test_lazy_import.data.basic2'] = None
1433+
lazy import test.test_lazy_import.data.basic2
1434+
assert "test.test_lazy_import.data.basic2" in sys.lazy_modules, (
1435+
f"blocked module must stay tracked, got {sys.lazy_modules}"
1436+
)
1437+
""")
1438+
assert_python_ok("-c", code)
1439+
1440+
def test_initializing_module_is_still_tracked(self):
1441+
"""A module that is still executing must not count as loaded."""
1442+
code = textwrap.dedent("""
1443+
import sys
1444+
name = "test.test_lazy_import.data.init_fails"
1445+
try:
1446+
import test.test_lazy_import.data.init_fails
1447+
except ValueError:
1448+
pass
1449+
else:
1450+
raise AssertionError("ValueError was not raised")
1451+
assert name not in sys.modules, "failed import left a module behind"
1452+
assert name in sys.lazy_modules, (
1453+
f"expected {name!r} tracked, got {sys.lazy_modules}"
1454+
)
1455+
""")
1456+
assert_python_ok("-c", code)
1457+
1458+
def test_module_spec_descriptor_is_not_run(self):
1459+
"""Checking whether a module is loaded must not run its descriptors."""
1460+
code = textwrap.dedent("""
1461+
import sys
1462+
import types
1463+
1464+
class RaisingSpec(types.ModuleType):
1465+
@property
1466+
def __spec__(self):
1467+
raise RuntimeError("__spec__ descriptor was run")
1468+
1469+
sys.modules["raising_spec"] = RaisingSpec("raising_spec")
1470+
lazy import raising_spec
1471+
assert "raising_spec" not in sys.lazy_modules, (
1472+
f"expected 'raising_spec' untracked, got {sys.lazy_modules}"
1473+
)
1474+
""")
1475+
assert_python_ok("-c", code)
1476+
1477+
def test_spec_initializing_descriptor_is_not_run(self):
1478+
code = textwrap.dedent("""
1479+
import sys
1480+
import types
1481+
1482+
class Spec:
1483+
@property
1484+
def _initializing(self):
1485+
raise RuntimeError("_initializing descriptor was run")
1486+
1487+
@property
1488+
def __dict__(self):
1489+
raise RuntimeError("__dict__ descriptor was run")
1490+
1491+
class SlottedSpec:
1492+
__slots__ = ()
1493+
1494+
@property
1495+
def _initializing(self):
1496+
raise RuntimeError("_initializing descriptor was run")
1497+
1498+
for spec in (Spec(), SlottedSpec()):
1499+
module = types.ModuleType("custom_spec")
1500+
module.__spec__ = spec
1501+
sys.modules["custom_spec"] = module
1502+
lazy import custom_spec
1503+
""")
1504+
assert_python_ok("-c", code)
1505+
1506+
def test_cached_attribute_does_not_check_spec_twice(self):
1507+
code = textwrap.dedent("""
1508+
import sys
1509+
import types
1510+
1511+
class Spec:
1512+
def __init__(self):
1513+
self.calls = 0
1514+
1515+
@property
1516+
def _initializing(self):
1517+
self.calls += 1
1518+
if self.calls == 2:
1519+
raise RuntimeError("_initializing was read twice")
1520+
return False
1521+
1522+
module = types.ModuleType("cached_spec")
1523+
module.__spec__ = Spec()
1524+
module.attr = 1
1525+
sys.modules["cached_spec"] = module
1526+
lazy from cached_spec import attr
1527+
assert attr == 1, attr
1528+
""")
1529+
assert_python_ok("-c", code)
1530+
1531+
def test_spec_initializing_truth_conversion_is_not_run(self):
1532+
code = textwrap.dedent("""
1533+
import sys
1534+
import types
1535+
from importlib.machinery import ModuleSpec
1536+
1537+
class Flag:
1538+
def __bool__(self):
1539+
raise RuntimeError("_initializing truth conversion was run")
1540+
1541+
released = memoryview(b"")
1542+
released.release()
1543+
for flag in (Flag(), released):
1544+
sys.lazy_modules.discard("custom_spec")
1545+
module = types.ModuleType("custom_spec")
1546+
module.__spec__ = ModuleSpec("custom_spec", None)
1547+
module.__spec__._initializing = flag
1548+
sys.modules["custom_spec"] = module
1549+
lazy import custom_spec
1550+
assert "custom_spec" in sys.lazy_modules, sys.lazy_modules
1551+
""")
1552+
assert_python_ok("-c", code)
1553+
1554+
def test_pending_submodule_is_still_tracked(self):
1555+
"""`lazy from` a submodule that is not loaded must stay tracked."""
1556+
code = textwrap.dedent("""
1557+
import sys
1558+
lazy from test.test_lazy_import.data.pkg import b
1559+
assert "test.test_lazy_import.data.pkg.b" in sys.lazy_modules, (
1560+
f"expected 'pkg.b' tracked, got {sys.lazy_modules}"
1561+
)
1562+
import test.test_lazy_import.data.pkg
1563+
assert "test.test_lazy_import.data.pkg.b" not in sys.modules, (
1564+
"loading the package must not load the submodule"
1565+
)
1566+
assert "test.test_lazy_import.data.pkg.b" in sys.lazy_modules, (
1567+
f"loading the package must not untrack the submodule, "
1568+
f"got {sys.lazy_modules}"
1569+
)
1570+
""")
1571+
assert_python_ok("-c", code)
1572+
13161573

13171574
@support.requires_subprocess()
13181575
class CommandLineAndEnvVarTests(unittest.TestCase):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Imported by test_initializing_module_is_still_tracked. The module it imports
2+
# lazily imports this one back while this one is still initializing.
3+
import test.test_lazy_import.data.lazy_on_init_fails
4+
raise ValueError("initialization failed")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lazy import test.test_lazy_import.data.init_fails
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Resolved names are now removed from :data:`sys.lazy_modules` more
2+
consistently: a lazy import of an already loaded module is no longer recorded,
3+
and reifying ``lazy from pkg import attr`` now discards the ``pkg.attr`` entry.

‎Objects/lazyimportobject.c‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,14 @@ lazy_import_resolve_impl(PyThreadState *tstate, PyObject *lazy_import,
452452
lazy_import_add_exception_cause(tstate, lz);
453453
}
454454
assert(obj == NULL || !PyLazyImport_CheckExact(obj));
455+
if (obj != NULL) {
456+
PyObject *name = lazy_import_name(lz);
457+
if (name == NULL ||
458+
_PyImport_DiscardLazyModule(tstate->interp, name) < 0) {
459+
Py_CLEAR(obj);
460+
}
461+
Py_XDECREF(name);
462+
}
455463
if (resolving != NULL) {
456464
// A failed set resize can leave the placeholder inserted. Removing by
457465
// identity also permits greenlets to finish in a different order.

0 commit comments

Comments
 (0)