Skip to content

Commit 44ebbaa

Browse files
committed
gh-155695: Track cached exports without invoking spec callbacks
Inspect stored spec flags without invoking descriptors or user-defined truth conversions. Built-in false values still mark a module as loaded. Skip registration for attributes already stored on loaded modules. Their existing placeholders keep their original import context, and pending modules with the same name remain tracked.
1 parent 0456068 commit 44ebbaa

2 files changed

Lines changed: 204 additions & 1 deletion

File tree

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,61 @@ def test_attribute_entry_removed_on_reification(self):
13251325
""")
13261326
assert_python_ok("-c", code)
13271327

1328+
def test_already_loaded_attribute_is_not_tracked(self):
1329+
code = textwrap.dedent("""
1330+
import sys
1331+
import math
1332+
1333+
lazy from math import pi
1334+
assert "math.pi" not in sys.lazy_modules, sys.lazy_modules
1335+
assert pi == math.pi
1336+
""")
1337+
assert_python_ok("-c", code)
1338+
1339+
def test_cached_lazy_attribute_keeps_its_import_context(self):
1340+
code = textwrap.dedent("""
1341+
import builtins
1342+
import sys
1343+
import test.test_lazy_import.data.basic_from_unused
1344+
1345+
holder = "test.test_lazy_import.data.basic_from_unused"
1346+
target = "test.test_lazy_import.data.basic2"
1347+
default_import = builtins.__import__
1348+
1349+
def import_hook(name, *args):
1350+
if name == holder:
1351+
raise RuntimeError("cached placeholder imported its holder")
1352+
return default_import(name, *args)
1353+
1354+
namespace = {
1355+
"__builtins__": dict(builtins.__dict__, __import__=import_hook),
1356+
"__name__": "cached_import_test",
1357+
}
1358+
exec(f"lazy from {holder} import basic2", namespace)
1359+
assert holder + ".basic2" not in sys.lazy_modules, sys.lazy_modules
1360+
assert target in sys.lazy_modules, sys.lazy_modules
1361+
exec("assert basic2.x == 42", namespace)
1362+
assert target not in sys.lazy_modules, sys.lazy_modules
1363+
""")
1364+
assert_python_ok("-c", code)
1365+
1366+
def test_cached_attribute_keeps_pending_module_tracked(self):
1367+
code = textwrap.dedent("""
1368+
import sys
1369+
import test.test_lazy_import.data.pkg as pkg
1370+
pkg.b = 42
1371+
1372+
lazy import test.test_lazy_import.data.pkg.b as pending
1373+
lazy from test.test_lazy_import.data.pkg import b
1374+
name = "test.test_lazy_import.data.pkg.b"
1375+
assert b == 42, b
1376+
assert name not in sys.modules, sys.modules
1377+
assert name in sys.lazy_modules, sys.lazy_modules
1378+
assert pending.foo() == "foo"
1379+
assert name not in sys.lazy_modules, sys.lazy_modules
1380+
""")
1381+
assert_python_ok("-c", code)
1382+
13281383
def test_failed_reification_stays_tracked(self):
13291384
"""A lazy import that fails to resolve must stay tracked."""
13301385
code = textwrap.dedent("""
@@ -1391,6 +1446,93 @@ def __spec__(self):
13911446
""")
13921447
assert_python_ok("-c", code)
13931448

1449+
def test_spec_initializing_descriptor_is_not_run(self):
1450+
code = textwrap.dedent("""
1451+
import sys
1452+
import types
1453+
1454+
class Spec:
1455+
@property
1456+
def _initializing(self):
1457+
raise RuntimeError("_initializing descriptor was run")
1458+
1459+
@property
1460+
def __dict__(self):
1461+
raise RuntimeError("__dict__ descriptor was run")
1462+
1463+
class SlottedSpec:
1464+
__slots__ = ()
1465+
1466+
@property
1467+
def _initializing(self):
1468+
raise RuntimeError("_initializing descriptor was run")
1469+
1470+
for spec in (Spec(), SlottedSpec()):
1471+
module = types.ModuleType("custom_spec")
1472+
module.__spec__ = spec
1473+
sys.modules["custom_spec"] = module
1474+
lazy import custom_spec
1475+
assert "custom_spec" in sys.lazy_modules, sys.lazy_modules
1476+
""")
1477+
assert_python_ok("-c", code)
1478+
1479+
def test_cached_attribute_does_not_check_spec_twice(self):
1480+
code = textwrap.dedent("""
1481+
import sys
1482+
import types
1483+
1484+
class Spec:
1485+
def __init__(self):
1486+
self.calls = 0
1487+
1488+
@property
1489+
def _initializing(self):
1490+
self.calls += 1
1491+
if self.calls == 2:
1492+
raise RuntimeError("_initializing was read twice")
1493+
return False
1494+
1495+
module = types.ModuleType("cached_spec")
1496+
module.__spec__ = Spec()
1497+
module.attr = 1
1498+
sys.modules["cached_spec"] = module
1499+
lazy from cached_spec import attr
1500+
assert attr == 1, attr
1501+
""")
1502+
assert_python_ok("-c", code)
1503+
1504+
def test_spec_builtin_false_initializing_is_not_tracked(self):
1505+
code = textwrap.dedent("""
1506+
import sys
1507+
import types
1508+
1509+
for flag in (False, None, 0, 0.0, 0j, "", b"", bytearray(),
1510+
(), [], {}, set(), frozenset(), range(0), memoryview(b"")):
1511+
module = types.ModuleType("custom_spec")
1512+
module.__spec__ = types.SimpleNamespace(_initializing=flag)
1513+
sys.modules["custom_spec"] = module
1514+
lazy import custom_spec
1515+
assert "custom_spec" not in sys.lazy_modules, (flag, sys.lazy_modules)
1516+
""")
1517+
assert_python_ok("-c", code)
1518+
1519+
def test_spec_initializing_truth_conversion_is_not_run(self):
1520+
code = textwrap.dedent("""
1521+
import sys
1522+
import types
1523+
1524+
class Flag(int):
1525+
def __bool__(self):
1526+
raise RuntimeError("_initializing truth conversion was run")
1527+
1528+
module = types.ModuleType("custom_spec")
1529+
module.__spec__ = types.SimpleNamespace(_initializing=Flag())
1530+
sys.modules["custom_spec"] = module
1531+
lazy import custom_spec
1532+
assert "custom_spec" in sys.lazy_modules, sys.lazy_modules
1533+
""")
1534+
assert_python_ok("-c", code)
1535+
13941536
def test_pending_submodule_is_still_tracked(self):
13951537
"""`lazy from` a submodule that is not loaded must stay tracked."""
13961538
code = textwrap.dedent("""

‎Python/import.c‎

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4376,6 +4376,50 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
43764376
return final_mod;
43774377
}
43784378

4379+
// Inspect stored initialization state without running spec descriptors or
4380+
// converting user-defined values to bool. Unknown state stays pending.
4381+
static int
4382+
lazy_import_spec_is_initializing(PyObject *spec)
4383+
{
4384+
if (spec == Py_None) {
4385+
return 0;
4386+
}
4387+
PyObject *dict = PyObject_GenericGetDict(spec, NULL);
4388+
if (dict == NULL) {
4389+
if (PyErr_ExceptionMatches(PyExc_AttributeError)) {
4390+
PyErr_Clear();
4391+
return 1;
4392+
}
4393+
return -1;
4394+
}
4395+
PyObject *initializing;
4396+
int rc = PyDict_GetItemRef(dict, &_Py_ID(_initializing), &initializing);
4397+
Py_DECREF(dict);
4398+
if (rc < 0) {
4399+
return -1;
4400+
}
4401+
if (rc == 0) {
4402+
initializing = _PyType_LookupRef(Py_TYPE(spec), &_Py_ID(_initializing));
4403+
}
4404+
if (initializing == NULL || initializing == Py_None) {
4405+
rc = 0;
4406+
}
4407+
else if (PyBool_Check(initializing) || PyLong_CheckExact(initializing) ||
4408+
PyFloat_CheckExact(initializing) || PyComplex_CheckExact(initializing) ||
4409+
PyUnicode_CheckExact(initializing) || PyBytes_CheckExact(initializing) ||
4410+
PyByteArray_CheckExact(initializing) || PyTuple_CheckExact(initializing) ||
4411+
PyList_CheckExact(initializing) || PyDict_CheckExact(initializing) ||
4412+
PyAnySet_CheckExact(initializing) || PyMemoryView_Check(initializing) ||
4413+
Py_IS_TYPE(initializing, &PyRange_Type)) {
4414+
rc = PyObject_IsTrue(initializing);
4415+
}
4416+
else {
4417+
rc = 1;
4418+
}
4419+
Py_XDECREF(initializing);
4420+
return rc;
4421+
}
4422+
43794423
// Check if a module is already loaded before adding it to sys.lazy_modules
43804424
static int
43814425
lazy_modules_add(PyThreadState *tstate, PyObject *name)
@@ -4397,7 +4441,7 @@ lazy_modules_add(PyThreadState *tstate, PyObject *name)
43974441
int rc = PyDict_GetItemRef(_PyModule_GetDict(existing),
43984442
&_Py_ID(__spec__), &spec);
43994443
if (rc > 0) {
4400-
rc = _PyModuleSpec_IsInitializing(spec);
4444+
rc = lazy_import_spec_is_initializing(spec);
44014445
Py_DECREF(spec);
44024446
}
44034447
if (rc < 0) {
@@ -4501,6 +4545,23 @@ static int
45014545
register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name,
45024546
PyObject *from)
45034547
{
4548+
// IMPORT_FROM returns stored attributes directly. Their imports are
4549+
// already resolved or tracked by their own placeholders, so skip the alias.
4550+
PyObject *mod = import_get_module(tstate, abs_name);
4551+
if (mod == NULL && PyErr_Occurred()) {
4552+
return -1;
4553+
}
4554+
if (mod != NULL && PyModule_Check(mod)) {
4555+
int rc = PyDict_Contains(_PyModule_GetDict(mod), from);
4556+
Py_DECREF(mod);
4557+
if (rc != 0) {
4558+
return rc < 0 ? -1 : 0;
4559+
}
4560+
}
4561+
else {
4562+
Py_XDECREF(mod);
4563+
}
4564+
45044565
PyObject *fromname = PyUnicode_FromFormat("%U.%U", abs_name, from);
45054566
if (fromname == NULL) {
45064567
return -1;

0 commit comments

Comments
 (0)