Skip to content

Commit ad36b56

Browse files
committed
gh-155695: Preserve import behavior in lazy module tracking
Read stored initialization flags without invoking spec callbacks or materializing dictionaries. Keep unsupported spec state conservatively tracked. Skip cached exports without changing their import context or clearing independently pending modules.
1 parent 0456068 commit ad36b56

2 files changed

Lines changed: 177 additions & 48 deletions

File tree

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 132 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,83 @@ 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+
""")
1476+
assert_python_ok("-c", code)
1477+
1478+
def test_cached_attribute_does_not_check_spec_twice(self):
1479+
code = textwrap.dedent("""
1480+
import sys
1481+
import types
1482+
1483+
class Spec:
1484+
def __init__(self):
1485+
self.calls = 0
1486+
1487+
@property
1488+
def _initializing(self):
1489+
self.calls += 1
1490+
if self.calls == 2:
1491+
raise RuntimeError("_initializing was read twice")
1492+
return False
1493+
1494+
module = types.ModuleType("cached_spec")
1495+
module.__spec__ = Spec()
1496+
module.attr = 1
1497+
sys.modules["cached_spec"] = module
1498+
lazy from cached_spec import attr
1499+
assert attr == 1, attr
1500+
""")
1501+
assert_python_ok("-c", code)
1502+
1503+
def test_spec_initializing_truth_conversion_is_not_run(self):
1504+
code = textwrap.dedent("""
1505+
import sys
1506+
import types
1507+
from importlib.machinery import ModuleSpec
1508+
1509+
class Flag:
1510+
def __bool__(self):
1511+
raise RuntimeError("_initializing truth conversion was run")
1512+
1513+
released = memoryview(b"")
1514+
released.release()
1515+
for flag in (Flag(), released):
1516+
sys.lazy_modules.discard("custom_spec")
1517+
module = types.ModuleType("custom_spec")
1518+
module.__spec__ = ModuleSpec("custom_spec", None)
1519+
module.__spec__._initializing = flag
1520+
sys.modules["custom_spec"] = module
1521+
lazy import custom_spec
1522+
assert "custom_spec" in sys.lazy_modules, sys.lazy_modules
1523+
""")
1524+
assert_python_ok("-c", code)
1525+
13941526
def test_pending_submodule_is_still_tracked(self):
13951527
"""`lazy from` a submodule that is not loaded must stay tracked."""
13961528
code = textwrap.dedent("""

‎Python/import.c‎

Lines changed: 45 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3917,20 +3917,6 @@ lazy_import_replay_from(PyThreadState *tstate, PyObject *mod,
39173917
return obj;
39183918
}
39193919

3920-
// The import machinery only discards module names, so this is what removes
3921-
// the "pkg.attr" entry left by `lazy from pkg import attr`, submodule or not.
3922-
static int
3923-
discard_reified_lazy_import(PyInterpreterState *interp, PyObject *lazy_import)
3924-
{
3925-
PyObject *name = _PyLazyImport_GetName(lazy_import);
3926-
if (name == NULL) {
3927-
return -1;
3928-
}
3929-
int res = PySet_Discard(LAZY_MODULES(interp), name);
3930-
Py_DECREF(name);
3931-
return res < 0 ? -1 : 0;
3932-
}
3933-
39343920
PyObject *
39353921
_PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
39363922
{
@@ -4114,8 +4100,12 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
41144100
}
41154101

41164102
ok:
4117-
if (obj != NULL && discard_reified_lazy_import(interp, lazy_import) < 0) {
4118-
Py_CLEAR(obj);
4103+
if (obj != NULL) {
4104+
PyObject *name = _PyLazyImport_GetName(lazy_import);
4105+
if (name == NULL || PySet_Discard(LAZY_MODULES(interp), name) < 0) {
4106+
Py_CLEAR(obj);
4107+
}
4108+
Py_XDECREF(name);
41194109
}
41204110

41214111
if (PySet_Discard(importing, lazy_import) < 0) {
@@ -4376,41 +4366,35 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
43764366
return final_mod;
43774367
}
43784368

4379-
// Check if a module is already loaded before adding it to sys.lazy_modules
43804369
static int
43814370
lazy_modules_add(PyThreadState *tstate, PyObject *name)
43824371
{
4383-
PyObject *modules = get_modules_dict(tstate, false);
4384-
if (modules == NULL) {
4372+
PyObject *mod = import_get_module(tstate, name);
4373+
if (mod == NULL && PyErr_Occurred()) {
43854374
return -1;
43864375
}
4387-
PyObject *existing;
4388-
if (PyDict_GetItemRef(modules, name, &existing) < 0) {
4389-
return -1;
4390-
}
4391-
// A None entry blocks the import rather than satisfying it.
4392-
int loaded = (existing != NULL && existing != Py_None);
4393-
if (loaded && PyModule_Check(existing)) {
4394-
// Check if the module is still initializing. Read __spec__ from the
4395-
// module dict so that a descriptor on a module subclass is not run.
4376+
int loaded = mod != NULL && mod != Py_None;
4377+
if (loaded && PyModule_Check(mod)) {
43964378
PyObject *spec;
4397-
int rc = PyDict_GetItemRef(_PyModule_GetDict(existing),
4398-
&_Py_ID(__spec__), &spec);
4399-
if (rc > 0) {
4400-
rc = _PyModuleSpec_IsInitializing(spec);
4401-
Py_DECREF(spec);
4402-
}
4403-
if (rc < 0) {
4404-
Py_DECREF(existing);
4379+
if (PyDict_GetItemRef(_PyModule_GetDict(mod), &_Py_ID(__spec__), &spec) < 0) {
4380+
Py_DECREF(mod);
44054381
return -1;
44064382
}
4407-
loaded = !rc;
4408-
}
4409-
Py_XDECREF(existing);
4410-
if (loaded) {
4411-
return 0;
4383+
loaded = spec == NULL || spec == Py_None;
4384+
if (!loaded) {
4385+
// Read the usual flag without callbacks or dictionary allocation.
4386+
// Unsupported spec representations stay conservatively tracked.
4387+
PyObject *initializing = NULL;
4388+
if ((Py_TYPE(spec)->tp_flags & Py_TPFLAGS_INLINE_VALUES) &&
4389+
_PyObject_TryGetInstanceAttribute(spec, &_Py_ID(_initializing), &initializing)) {
4390+
loaded = initializing == NULL || initializing == Py_False;
4391+
}
4392+
Py_XDECREF(initializing);
4393+
}
4394+
Py_XDECREF(spec);
44124395
}
4413-
return PySet_Add(LAZY_MODULES(tstate->interp), name);
4396+
Py_XDECREF(mod);
4397+
return loaded ? 0 : PySet_Add(LAZY_MODULES(tstate->interp), name);
44144398
}
44154399

44164400
// ensure we have the set for the parent module name in sys.lazy_modules.
@@ -4501,19 +4485,32 @@ static int
45014485
register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name,
45024486
PyObject *from)
45034487
{
4504-
PyObject *fromname = PyUnicode_FromFormat("%U.%U", abs_name, from);
4505-
if (fromname == NULL) {
4488+
// IMPORT_FROM returns stored attributes directly. Their imports are
4489+
// already resolved or tracked by their own placeholders, so skip the alias.
4490+
PyObject *mod = import_get_module(tstate, abs_name);
4491+
if (mod == NULL && PyErr_Occurred()) {
45064492
return -1;
45074493
}
4494+
int rc = 0;
4495+
if (mod != NULL && PyModule_Check(mod)) {
4496+
rc = PyDict_Contains(_PyModule_GetDict(mod), from);
4497+
}
4498+
Py_XDECREF(mod);
4499+
if (rc != 0) {
4500+
return rc < 0 ? -1 : 0;
4501+
}
45084502

4509-
if (lazy_modules_add(tstate, fromname) < 0) {
4510-
Py_DECREF(fromname);
4503+
PyObject *fromname = PyUnicode_FromFormat("%U.%U", abs_name, from);
4504+
if (fromname == NULL) {
45114505
return -1;
45124506
}
45134507

4514-
int res = register_lazy_on_parent(tstate, fromname);
4508+
rc = lazy_modules_add(tstate, fromname);
4509+
if (rc == 0) {
4510+
rc = register_lazy_on_parent(tstate, fromname);
4511+
}
45154512
Py_DECREF(fromname);
4516-
return res;
4513+
return rc;
45174514
}
45184515

45194516
_PyLazySubmoduleImportResult

0 commit comments

Comments
 (0)