Skip to content

Commit b09a509

Browse files
authored
Merge pull request #145 from encukou/simplify-lazy-imports-c
Tests & nitpicks for the lazy import refactor
2 parents 3dae4a6 + e44cc18 commit b09a509

6 files changed

Lines changed: 69 additions & 12 deletions

File tree

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 31 additions & 1 deletion
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
@@ -617,6 +617,14 @@ def test_dunder_lazy_import_invalid_arguments(self):
617617
with self.assertRaises(TypeError):
618618
__lazy_import__("sys", globals=1)
619619

620+
code = textwrap.dedent("""
621+
__lazy_import__("sys", fromlist=(1, 2, 3))
622+
""")
623+
result = assert_python_failure("-c", code, NO_COLOR='y')
624+
self.assertIn(
625+
b"TypeError: Item in ``from list'' must be str, not int",
626+
result.err)
627+
620628
def test_dunder_lazy_import_builtins(self):
621629
"""__lazy_import__ should use module's __builtins__ for __import__."""
622630
from test.test_lazy_import.data import dunder_lazy_import_builtins
@@ -766,6 +774,28 @@ def test_missing_lazy_from_import_shows_chained_traceback(self):
766774
""")
767775
assert_python_ok("-c", code)
768776

777+
@support.subTests('name', (
778+
'test.test_lazy_import.data.broken_module_chained_cause',
779+
'test.test_lazy_import.data.broken_module_chained_context',
780+
'test.test_lazy_import.data.broken_module_chained_suppressed',
781+
))
782+
def test_chained_exception_import_shows_notes(self, name):
783+
"""Accessing missing attribute from lazy from-import should chain errors."""
784+
code = textwrap.dedent(f"""
785+
lazy import {name}
786+
787+
try:
788+
_ = test
789+
except ValueError as e:
790+
assert any(
791+
note.startswith("lazy import of '{name}' declared in ")
792+
for note in e.__notes__
793+
), e.__notes__
794+
else:
795+
raise AssertionError("ImportError was not raised")
796+
""")
797+
assert_python_ok("-c", code)
798+
769799
def test_reification_retries_on_failure(self):
770800
"""Failed reification should allow retry on subsequent access.
771801
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

‎Objects/dictobject.c‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2014,9 +2014,11 @@ static void
20142014
replace_value(PyDictObject *mp, PyObject *key, Py_ssize_t ix,
20152015
PyObject *old_value, PyObject *value)
20162016
{
2017+
assert(can_modify_dict(mp));
2018+
assert(old_value != NULL);
2019+
20172020
if (old_value != value) {
20182021
_PyDict_NotifyEvent(PyDict_EVENT_MODIFIED, mp, key, value);
2019-
assert(old_value != NULL);
20202022
if (DK_IS_UNICODE(mp->ma_keys)) {
20212023
if (_PyDict_HasSplitTable(mp)) {
20222024
STORE_SPLIT_VALUE(mp, ix, value);
@@ -2031,7 +2033,9 @@ replace_value(PyDictObject *mp, PyObject *key, Py_ssize_t ix,
20312033
STORE_VALUE(ep, value);
20322034
}
20332035
}
2034-
Py_DECREF(old_value); /* which **CAN** re-enter (see issue #22653) */
2036+
Py_DECREF(old_value); /* which **CAN** re-enter (see gh-66843) */
2037+
2038+
ASSERT_CONSISTENT(mp);
20352039
}
20362040

20372041
/*
@@ -2082,7 +2086,6 @@ insertdict(PyDictObject *mp,
20822086
}
20832087

20842088
replace_value(mp, key, ix, old_value, value);
2085-
ASSERT_CONSISTENT(mp);
20862089
Py_DECREF(key);
20872090
return 0;
20882091

@@ -3082,7 +3085,9 @@ _PyDict_ReplaceItemIf(PyObject *op, PyObject *key,
30823085
PyObject *expected, PyObject *replacement)
30833086
{
30843087
assert(PyDict_Check(op));
3085-
assert(expected != NULL && replacement != NULL);
3088+
assert(expected != NULL);
3089+
assert(replacement != NULL);
3090+
30863091
Py_hash_t hash = PyObject_Hash(key);
30873092
if (hash == -1) {
30883093
return -1;
@@ -3098,7 +3103,6 @@ _PyDict_ReplaceItemIf(PyObject *op, PyObject *key,
30983103
else if (current == expected) {
30993104
// Do not look up the key again: equality can execute Python code.
31003105
replace_value(mp, key, ix, current, Py_NewRef(replacement));
3101-
ASSERT_CONSISTENT(mp);
31023106
result = 1;
31033107
}
31043108
Py_END_CRITICAL_SECTION();

‎Objects/lazyimportobject.c‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
typedef struct {
1717
PyObject_HEAD
1818
PyObject *lz_builtins; // Roots own the mapping; projections retain the root.
19-
// A root stores its absolute name and original fromlist. A projection
20-
// stores its source placeholder and the attribute to import from it.
19+
// A root stores its absolute name (PyUnicode) in lz_from, and original
20+
// fromlist in lz_attr.
21+
// A projection stores its source placeholder (a PyLazyImportObject)
22+
// in lz_from, and the attribute to import from (PyUnicode) it in lz_attr.
2123
PyObject *lz_from;
2224
PyObject *lz_attr;
2325
// Declaration location.
@@ -45,9 +47,18 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins,
4547
"lazy_import: fromlist must be None, a string, or a tuple");
4648
return NULL;
4749
}
48-
assert(PyLazyImport_CheckExact(name) ? builtins == NULL : builtins != NULL);
49-
assert(!PyLazyImport_CheckExact(name) ||
50-
(fromlist != NULL && PyUnicode_Check(fromlist)));
50+
#ifndef NDEBUG
51+
if (PyLazyImport_CheckExact(name)) {
52+
// projection
53+
assert(builtins == NULL);
54+
assert(fromlist != NULL);
55+
assert(PyUnicode_Check(fromlist));
56+
}
57+
else {
58+
// root
59+
assert(builtins != NULL);
60+
}
61+
#endif
5162
PyLazyImportObject *m = PyObject_GC_New(
5263
PyLazyImportObject, &PyLazyImport_Type);
5364
if (m == NULL) {
@@ -71,6 +82,7 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins,
7182

7283
// Reuse concrete attributes of initialized modules without waiting for imports
7384
// or resolving lazy attributes. Failed cache lookups are retried at resolution.
85+
// May return NULL with or without an exception set.
7486
static PyObject *
7587
lazy_import_get_loaded_attr(PyThreadState *tstate, PyObject *name,
7688
PyObject *attr_name)
@@ -466,6 +478,7 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
466478

467479
// Loading pkg.child can replace a placeholder in pkg.child with the module
468480
// before a from-import retrieves the value that belongs in that binding.
481+
// This is an optimization that can be safely skipped.
469482
static int
470483
lazy_import_replace_child(PyThreadState *tstate, PyObject *placeholder,
471484
PyObject *name, PyObject *namespace,
@@ -563,7 +576,7 @@ lazy_import_resolve(PyObject *self, PyObject *args)
563576
static PyMethodDef lazy_import_methods[] = {
564577
{
565578
"resolve", lazy_import_resolve, METH_NOARGS,
566-
PyDoc_STR("resolves the lazy import and returns the actual object")
579+
PyDoc_STR("Resolve the lazy import and return the imported object.")
567580
},
568581
{NULL, NULL}
569582
};

0 commit comments

Comments
 (0)