Skip to content

Commit be87a85

Browse files
brittanyreypablogsalencukou
authored
gh-155695: Remove resolved names from sys.lazy_modules more consistently (#157714)
Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> Co-authored-by: Petr Viktorin <encukou@gmail.com>
1 parent b5aa195 commit be87a85

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)
@@ -1305,6 +1306,262 @@ def test_lazy_module_without_children_is_tracked(self):
13051306
""")
13061307
assert_python_ok("-c", code)
13071308

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

13091566
@support.requires_subprocess()
13101567
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)