Visitar URL original
Backport GH-158282 lazy-import behaviors and refresh tests to the 3.1… · Kronuz/cpython@e26052a · GitHub
Skip to content

Commit e26052a

Browse files
committed
Backport pythonGH-158282 lazy-import behaviors and refresh tests to the 3.15 tip
Ports two user-visible behaviors from upstream's pythonGH-158282 lazy-import resolver refactor without its 3.15-only dict/eval infrastructure: - Chained exception notes: when a lazy import's resolution fails and the exception already carries a cause/context (including `raise ... from None`), attach a "lazy import of '<name>' declared in ..." note instead of replacing the cause (import.c error path). - __lazy_import__ fromlist validation: reject a non-str item in the from list with TypeError, matching eager __import__, instead of letting it reach code that assumes str and crash a debug build. Also refreshes Lib/test/test_lazy_import to the 3.15 tip (021f634) so the latest upstream lazy-import tests run here. Full suite passes (133 tests).
1 parent 0bf1009 commit e26052a

5 files changed

Lines changed: 96 additions & 7 deletions

File tree

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import contextlib
1414

1515
from test import support
16-
from test.support.script_helper import assert_python_ok
16+
from test.support.script_helper import assert_python_ok, assert_python_failure
1717

1818
try:
1919
import _testcapi
@@ -285,6 +285,23 @@ def test_lazy_import_type_attributes_accessible(self):
285285
proc = assert_python_ok("-c", code)
286286
self.assertIn(b"<built-in method resolve of lazy_import object at", proc.out)
287287

288+
@support.requires_subprocess()
289+
def test_lazy_import_type_attribute_error_message(self):
290+
"""Check that LazyImportType attribute error message is helpful."""
291+
code = textwrap.dedent("""
292+
lazy import asyncio
293+
try:
294+
globals()["asyncio"].Task
295+
except AttributeError as exc:
296+
assert str(exc) == (
297+
"cannot access attribute 'Task' "
298+
"on unresolved lazy import 'asyncio'"
299+
), repr(str(exc))
300+
else:
301+
assert False, 'AttributeError is not raised'
302+
""")
303+
assert_python_ok("-c", code)
304+
288305

289306
class SyntaxRestrictionTests(LazyImportTestCase):
290307
"""Tests for syntax restrictions on lazy imports."""
@@ -608,6 +625,14 @@ def test_dunder_lazy_import_invalid_arguments(self):
608625
with self.assertRaises(TypeError):
609626
__lazy_import__("sys", globals=1)
610627

628+
code = textwrap.dedent("""
629+
__lazy_import__("sys", fromlist=(1, 2, 3))
630+
""")
631+
result = assert_python_failure("-c", code, NO_COLOR='y')
632+
self.assertIn(
633+
b"TypeError: Item in ``from list'' must be str, not int",
634+
result.err)
635+
611636
def test_dunder_lazy_import_builtins(self):
612637
"""__lazy_import__ should use module's __builtins__ for __import__."""
613638
from test.test_lazy_import.data import dunder_lazy_import_builtins
@@ -658,7 +683,7 @@ def my_filter(name):
658683
sys.set_lazy_imports_filter(my_filter)
659684
self.assertIs(sys.get_lazy_imports_filter(), my_filter)
660685

661-
def test_lazy_modules_attribute_is_set(self):
686+
def test_lazy_modules_attribute_is_dict(self):
662687
"""sys.lazy_modules should be a set per PEP 810."""
663688
self.assertIsInstance(sys.lazy_modules, set)
664689

@@ -687,7 +712,7 @@ class ErrorHandlingTests(LazyImportTestCase):
687712
"""Tests for error handling during lazy import reification."""
688713

689714
def test_missing_lazy_submodule_raises_module_not_found_error(self):
690-
"""Accessing a nonexistent lazy submodule raises ModuleNotFoundError."""
715+
"""Accessing a nonexistent lazy submodule via parent attr raises ModuleNotFoundError."""
691716
code = textwrap.dedent("""
692717
lazy import test.test_lazy_import.data.nonexistent_module
693718
@@ -757,6 +782,28 @@ def test_missing_lazy_from_import_shows_chained_traceback(self):
757782
""")
758783
assert_python_ok("-c", code)
759784

785+
@support.subTests('name', (
786+
'test.test_lazy_import.data.broken_module_chained_cause',
787+
'test.test_lazy_import.data.broken_module_chained_context',
788+
'test.test_lazy_import.data.broken_module_chained_suppressed',
789+
))
790+
def test_chained_exception_import_shows_notes(self, name):
791+
"""Accessing missing attribute from lazy from-import should chain errors."""
792+
code = textwrap.dedent(f"""
793+
lazy import {name}
794+
795+
try:
796+
_ = test
797+
except ValueError as e:
798+
assert any(
799+
note.startswith("lazy import of '{name}' declared in ")
800+
for note in e.__notes__
801+
), e.__notes__
802+
else:
803+
raise AssertionError("ImportError was not raised")
804+
""")
805+
assert_python_ok("-c", code)
806+
760807
def test_reification_retries_on_failure(self):
761808
"""Failed reification should allow retry on subsequent access.
762809
@@ -1280,7 +1327,6 @@ def test_cli_lazy_imports_all_makes_regular_imports_lazy(self):
12801327
"""-X lazy_imports=all should make all imports potentially lazy."""
12811328
code = textwrap.dedent("""
12821329
import sys
1283-
assert sys.flags.lazy_imports == 1
12841330
# In 'all' mode, regular imports become lazy
12851331
import json
12861332
# json should not be in sys.modules yet (lazy)
@@ -1315,7 +1361,6 @@ def test_cli_lazy_imports_normal_respects_lazy_keyword_only(self):
13151361
# modules already loaded by the interpreter startup
13161362
code = textwrap.dedent("""
13171363
import sys
1318-
assert sys.flags.lazy_imports == -1
13191364
import test.test_lazy_import.data.basic2 # Should be eager
13201365
lazy import test.test_lazy_import.data.pkg.b # Should be lazy
13211366
@@ -1339,7 +1384,6 @@ def test_env_var_lazy_imports_all_enables_global_lazy(self):
13391384
"""PYTHON_LAZY_IMPORTS=all should enable global lazy imports."""
13401385
code = textwrap.dedent("""
13411386
import sys
1342-
assert sys.flags.lazy_imports == 1
13431387
import json
13441388
if 'json' not in sys.modules:
13451389
print("LAZY")
@@ -1944,7 +1988,7 @@ def access_modules(idx):
19441988
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
19451989
self.assertIn("OK", result.stdout)
19461990

1947-
def test_concurrent_lazy_modules_set_updates(self):
1991+
def test_concurrent_lazy_modules_dict_updates(self):
19481992
"""Multiple threads creating lazy imports should safely update sys.lazy_modules."""
19491993
code = textwrap.dedent("""
19501994
import sys
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Module that raises an exception with explicit cause during import
2+
cause = ValueError("Cause of failure")
3+
raise ValueError("This module always fails to import") from cause
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Module that raises an exception with context during import
2+
try:
3+
raise ValueError("Cause of failure")
4+
except:
5+
raise ValueError("This module always fails to import")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Module that raises an exception with suppressed context during import
2+
raise ValueError("This module always fails to import") from None

‎Python/import.c‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4041,6 +4041,25 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
40414041
goto ok;
40424042
}
40434043

4044+
// If the exception already carries a cause/context (a chained error,
4045+
// including `raise ... from None`), attach a note recording where the
4046+
// lazy import was declared rather than replacing the cause. Matches
4047+
// upstream GH-158282's lazy_import_add_exception_cause.
4048+
PyBaseExceptionObject *base_exc = (PyBaseExceptionObject *)exc;
4049+
if (base_exc->cause != NULL || base_exc->context != NULL ||
4050+
base_exc->suppress_context) {
4051+
PyObject *note = PyUnicode_FromFormat(
4052+
"lazy import of '%U' declared in %s at %s:%d",
4053+
import_name, funcname_str, filename_str, lineno);
4054+
if (note != NULL) {
4055+
(void)_PyException_AddNote(exc, note);
4056+
Py_DECREF(note);
4057+
}
4058+
Py_DECREF(import_name);
4059+
_PyErr_SetRaisedException(tstate, exc);
4060+
goto ok;
4061+
}
4062+
40444063
// Create a cause exception showing where the lazy import was declared.
40454064
PyObject *msg = PyUnicode_FromFormat(
40464065
"lazy import of '%U' raised an exception during resolution",
@@ -4624,6 +4643,22 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
46244643
else {
46254644
Py_XINCREF(fromlist);
46264645
}
4646+
// Validate fromlist items are strings before creating the lazy import,
4647+
// matching upstream GH-158282 and eager __import__. Without this a non-str
4648+
// item reaches code that assumes str and crashes a debug build.
4649+
if (fromlist != NULL && PyTuple_Check(fromlist)) {
4650+
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(fromlist); i++) {
4651+
PyObject *item = PyTuple_GET_ITEM(fromlist, i);
4652+
if (!PyUnicode_Check(item)) {
4653+
_PyErr_Format(tstate, PyExc_TypeError,
4654+
"Item in ``from list'' must be str, not %.200s",
4655+
Py_TYPE(item)->tp_name);
4656+
Py_XDECREF(fromlist);
4657+
Py_DECREF(abs_name);
4658+
return NULL;
4659+
}
4660+
}
4661+
}
46274662
PyObject *res = _PyLazyImport_New(frame, builtins, abs_name, fromlist);
46284663
if (res == NULL) {
46294664
Py_XDECREF(fromlist);

0 commit comments

Comments
 (0)