Skip to content

Commit f1beae8

Browse files
committed
gh-152298: Preserve lazy submodule sentinel errors
1 parent b408531 commit f1beae8

3 files changed

Lines changed: 67 additions & 4 deletions

File tree

Lib/test/test_lazy_import/__init__.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,47 @@ def __getitem__(self, name):
515515
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
516516
self.assertIn("OK", result.stdout)
517517

518+
@support.requires_subprocess()
519+
def test_lazy_submodule_sys_modules_none_sentinel_raises(self):
520+
"""The sys.modules None sentinel should preserve import failure."""
521+
code = textwrap.dedent("""
522+
import os
523+
import sys
524+
import tempfile
525+
526+
with tempfile.TemporaryDirectory() as tmpdir:
527+
pkg_dir = os.path.join(tmpdir, "lazy_sentinel_pkg")
528+
os.mkdir(pkg_dir)
529+
with open(os.path.join(pkg_dir, "__init__.py"), "w", encoding="utf-8") as f:
530+
f.write("")
531+
with open(os.path.join(pkg_dir, "sub.py"), "w", encoding="utf-8") as f:
532+
f.write("VALUE = 42\\n")
533+
534+
sys.path.insert(0, tmpdir)
535+
lazy import lazy_sentinel_pkg.sub
536+
try:
537+
sys.modules["lazy_sentinel_pkg.sub"] = None
538+
try:
539+
lazy_sentinel_pkg.sub
540+
except ModuleNotFoundError as exc:
541+
assert exc.name == "lazy_sentinel_pkg.sub", exc
542+
else:
543+
raise AssertionError("ModuleNotFoundError was not raised")
544+
finally:
545+
sys.path.remove(tmpdir)
546+
for name in ("lazy_sentinel_pkg", "lazy_sentinel_pkg.sub"):
547+
sys.modules.pop(name, None)
548+
549+
print("OK")
550+
""")
551+
result = subprocess.run(
552+
[sys.executable, "-c", code],
553+
capture_output=True,
554+
text=True
555+
)
556+
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
557+
self.assertIn("OK", result.stdout)
558+
518559
def test_lazy_import_pkg_cross_import(self):
519560
"""Cross-imports within package should preserve lazy imports."""
520561
import test.test_lazy_import.data.pkg.c
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
Fix :meth:`!types.LazyImportType.resolve` so resolving a module-level lazy
22
import replaces the original lazy proxy in the module globals.
3+
Lazy submodule attribute access now also preserves importlib's
4+
``sys.modules[name] is None`` sentinel behavior while treating absent requested
5+
lazy submodules as normal attribute misses.

Python/import.c

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4101,7 +4101,7 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
41014101
}
41024102
else if (obj != NULL) {
41034103
// Cache the result and update the original global before releasing
4104-
// the import lock, so competing reification cannot import again.
4104+
// the import lock, so later reification through this proxy reuses it.
41054105
if (_PyLazyImport_FinishResolve(lazy_import, obj) < 0) {
41064106
Py_CLEAR(obj);
41074107
}
@@ -4455,8 +4455,10 @@ register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name,
44554455
static int
44564456
is_module_not_found_for_name(PyThreadState *tstate, PyObject *name)
44574457
{
4458-
// Return true only for "name itself was not found", consuming the current
4459-
// exception so callers can continue normal attribute fallback.
4458+
// Return true only for the importlib._handle_fromlist() compatibility
4459+
// case where "name itself was not found" and sys.modules[name] is not the
4460+
// None sentinel. Consume the current exception so callers can continue
4461+
// normal attribute fallback.
44604462
if (!_PyErr_ExceptionMatches(tstate, PyExc_ModuleNotFoundError)) {
44614463
return 0;
44624464
}
@@ -4479,6 +4481,18 @@ is_module_not_found_for_name(PyThreadState *tstate, PyObject *name)
44794481
return 0;
44804482
}
44814483

4484+
PyObject *mod = import_get_module(tstate, name);
4485+
if (mod == Py_None) {
4486+
Py_DECREF(mod);
4487+
_PyErr_SetRaisedException(tstate, exc);
4488+
return 0;
4489+
}
4490+
if (mod == NULL && _PyErr_Occurred(tstate)) {
4491+
PyErr_Clear();
4492+
_PyErr_SetRaisedException(tstate, exc);
4493+
return 0;
4494+
}
4495+
Py_XDECREF(mod);
44824496
Py_DECREF(exc);
44834497
return 1;
44844498
}
@@ -4554,7 +4568,12 @@ _PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name,
45544568
return -1;
45554569
}
45564570

4557-
if (PySet_Discard(pending_set, attr_name) < 0) {
4571+
// This completes the successful lazy-submodule publish. pending_set is an
4572+
// internal set and attr_name is a Unicode attribute name, so discard is not
4573+
// expected to fail after the parent dict has been updated.
4574+
int discard_rc = PySet_Discard(pending_set, attr_name);
4575+
assert(discard_rc >= 0);
4576+
if (discard_rc < 0) {
45584577
Py_DECREF(pending_set);
45594578
Py_DECREF(submod);
45604579
return -1;

0 commit comments

Comments
 (0)