Skip to content

Commit 30cbbc0

Browse files
committed
gh-152298: Report missing lazy submodules
1 parent bf48d37 commit 30cbbc0

2 files changed

Lines changed: 65 additions & 2 deletions

File tree

Lib/test/test_lazy_import/__init__.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,62 @@ def test_lazy_submodule_stored_in_parent_dict(self):
457457
self.assertIs(pkg.__dict__["bar"], sys.modules["test.test_lazy_import.data.pkg.bar"])
458458
self.assertIn("BAR_MODULE_LOADED", out.getvalue())
459459

460+
@support.requires_subprocess()
461+
def test_lazy_submodule_missing_from_sys_modules_raises_key_error(self):
462+
"""A sys.modules race after import should surface the hard error."""
463+
code = textwrap.dedent("""
464+
import os
465+
import sys
466+
import tempfile
467+
468+
class HidingModules(dict):
469+
hide = False
470+
hide_name = None
471+
472+
def __getitem__(self, name):
473+
if self.hide and name == self.hide_name:
474+
raise KeyError(name)
475+
return super().__getitem__(name)
476+
477+
with tempfile.TemporaryDirectory() as tmpdir:
478+
pkg_dir = os.path.join(tmpdir, "lazy_sysmodules_pkg")
479+
os.mkdir(pkg_dir)
480+
with open(os.path.join(pkg_dir, "__init__.py"), "w", encoding="utf-8") as f:
481+
f.write("")
482+
with open(os.path.join(pkg_dir, "sub.py"), "w", encoding="utf-8") as f:
483+
f.write("VALUE = 42\\n")
484+
485+
original_modules = sys.modules
486+
modules = HidingModules(original_modules)
487+
sys.modules = modules
488+
sys.path.insert(0, tmpdir)
489+
try:
490+
lazy import lazy_sysmodules_pkg.sub
491+
modules.hide_name = "lazy_sysmodules_pkg.sub"
492+
modules.hide = True
493+
494+
try:
495+
lazy_sysmodules_pkg.sub
496+
except KeyError as exc:
497+
assert exc.args == ("lazy_sysmodules_pkg.sub",), exc
498+
else:
499+
raise AssertionError("KeyError was not raised")
500+
finally:
501+
sys.path.remove(tmpdir)
502+
sys.modules = original_modules
503+
for name in ("lazy_sysmodules_pkg", "lazy_sysmodules_pkg.sub"):
504+
sys.modules.pop(name, None)
505+
506+
print("OK")
507+
""")
508+
result = subprocess.run(
509+
[sys.executable, "-c", code],
510+
capture_output=True,
511+
text=True
512+
)
513+
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
514+
self.assertIn("OK", result.stdout)
515+
460516
def test_lazy_import_pkg_cross_import(self):
461517
"""Cross-imports within package should preserve lazy imports."""
462518
import test.test_lazy_import.data.pkg.c

Python/import.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4458,7 +4458,8 @@ _PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name,
44584458
{
44594459
*result = NULL;
44604460

4461-
PyInterpreterState *interp = _PyInterpreterState_GET();
4461+
PyThreadState *tstate = _PyThreadState_GET();
4462+
PyInterpreterState *interp = tstate->interp;
44624463
PyObject *lazy_pending = LAZY_PENDING_SUBMODULES(interp);
44634464
if (lazy_pending == NULL) {
44644465
return 0;
@@ -4499,11 +4500,17 @@ _PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name,
44994500
Py_DECREF(mod);
45004501

45014502
PyObject *submod = PyImport_GetModule(full_name);
4502-
Py_DECREF(full_name);
45034503
if (submod == NULL) {
4504+
if (!_PyErr_Occurred(tstate)) {
4505+
_PyErr_Format(tstate, PyExc_KeyError,
4506+
"%R not in sys.modules as expected",
4507+
full_name);
4508+
}
4509+
Py_DECREF(full_name);
45044510
Py_DECREF(pending_set);
45054511
return -1;
45064512
}
4513+
Py_DECREF(full_name);
45074514

45084515
if (PyDict_SetItem(mod_dict, attr_name, submod) < 0) {
45094516
Py_DECREF(pending_set);

0 commit comments

Comments
 (0)