Skip to content

Commit 021f634

Browse files
pablogsalencukouhugovk
authored
[3.15] gh-142349: Simplify lazy import resolution (GH-158282) (#158569)
Backport the shared lazy import resolver to Python 3.15, regenerate interpreter files, and update the ABI snapshot. Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
1 parent 3105a34 commit 021f634

24 files changed

Lines changed: 9212 additions & 9124 deletions

‎Doc/data/python3.15.abi‎

Lines changed: 8286 additions & 8232 deletions
Large diffs are not rendered by default.

‎Include/internal/pycore_dict.h‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ PyAPI_FUNC(int) _PyDict_DelItemIf(PyObject *mp, PyObject *key,
2323
int (*predicate)(PyObject *value, void *arg),
2424
void *arg);
2525

26+
// Atomically replace an existing value if it is expected (by identity).
27+
// Return 1 if replaced, 0 if absent or different, or -1 on error.
28+
extern int _PyDict_ReplaceItemIf(PyObject *dict, PyObject *key,
29+
PyObject *expected, PyObject *replacement);
30+
2631
// "KnownHash" variants
2732
// Export for '_asyncio' shared extension
2833
PyAPI_FUNC(int) _PyDict_SetItem_KnownHash(PyObject *mp, PyObject *key,
@@ -146,8 +151,8 @@ PyAPI_FUNC(Py_ssize_t) _PyDictKeys_StringLookup(PyDictKeysObject* dictkeys, PyOb
146151
*/
147152
PyAPI_FUNC(Py_ssize_t) _PyDictKeys_StringLookupAndVersion(PyDictKeysObject* dictkeys, PyObject *key, uint32_t *version);
148153
PyAPI_FUNC(Py_ssize_t) _PyDictKeys_StringLookupSplit(PyDictKeysObject* dictkeys, PyObject *key);
149-
PyAPI_FUNC(PyObject *)_PyDict_LoadGlobal(PyDictObject *, PyDictObject *, PyObject *);
150-
PyAPI_FUNC(void) _PyDict_LoadGlobalStackRef(PyDictObject *, PyDictObject *, PyObject *, _PyStackRef *);
154+
// Return the borrowed source dictionary, or NULL if absent or on error.
155+
PyAPI_FUNC(PyObject *) _PyDict_LoadGlobalStackRef(PyDictObject *, PyDictObject *, PyObject *, _PyStackRef *);
151156

152157
// Loads the __builtins__ object from the globals dict. Returns a new reference.
153158
extern PyObject *_PyDict_LoadBuiltinsFromGlobals(PyObject *globals);

‎Include/internal/pycore_import.h‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,16 @@ extern int _PyImport_FixupBuiltin(
3232
PyObject *modules
3333
);
3434

35-
extern PyObject * _PyImport_ResolveName(
36-
PyThreadState *tstate, PyObject *name, PyObject *globals, int level);
3735
extern PyObject * _PyImport_GetAbsName(
3836
PyThreadState *tstate, PyObject *name, PyObject *globals, int level);
3937
// Symbol is exported for the JIT on Windows builds.
4038
PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate(
4139
PyThreadState *tstate, PyObject *lazy_import);
42-
typedef enum {
43-
_Py_LAZY_SUBMODULE_ERROR = -1,
44-
_Py_LAZY_SUBMODULE_NOT_FOUND = 0,
45-
_Py_LAZY_SUBMODULE_LOADED = 1,
46-
} _PyLazySubmoduleImportResult;
47-
extern _PyLazySubmoduleImportResult _PyImport_TryLoadLazySubmodule(
48-
PyObject *mod_name, PyObject *attr_name, PyObject **result);
40+
// Bind a pending child on module before removing its registration. Return a
41+
// new reference, NULL without an exception if absent, or NULL on error.
42+
// Set recheck_dict if a concurrent lookup may have already bound the child.
43+
extern PyObject * _PyImport_TryLoadLazySubmodule(
44+
PyObject *module, PyObject *attr_name, int *recheck_dict);
4945
extern PyObject * _PyImport_LazyImportModuleLevelObject(
5046
PyThreadState *tstate, PyObject *name, PyObject *builtins,
5147
PyObject *globals, PyObject *locals, PyObject *fromlist, int level);

‎Include/internal/pycore_interp_structs.h‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,15 +348,12 @@ struct _import_state {
348348
PyObject *lazy_import_func;
349349
int lazy_imports_mode;
350350
PyObject *lazy_imports_filter;
351-
PyObject *lazy_importing_modules;
352351
// The set stored in sys.lazy_modules if values that have been
353352
// lazily imported. This value is only for debugging/introspection
354353
// purposes and is not used by the runtime.
355354
PyObject *lazy_modules;
356355
// A dict mapping package names to a set of submodule names that
357-
// have been imported lazily from packages which have been imported
358-
// lazily. When the package is reified we need to add a
359-
// LazyImportObject which refers to the submodule on the module.
356+
// may need to be imported when an attribute is missing on the package.
360357
PyObject *lazy_pending_submodules;
361358
#ifdef Py_GIL_DISABLED
362359
PyMutex lazy_mutex;

‎Include/internal/pycore_lazyimportobject.h‎

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,18 @@ extern "C" {
1414
PyAPI_DATA(PyTypeObject) PyLazyImport_Type;
1515
#define PyLazyImport_CheckExact(op) Py_IS_TYPE((op), &PyLazyImport_Type)
1616

17-
typedef struct {
18-
PyObject_HEAD
19-
PyObject *lz_builtins;
20-
PyObject *lz_from;
21-
PyObject *lz_attr;
22-
// Frame information for the original import location.
23-
PyCodeObject *lz_code; // Code object where the lazy import was created.
24-
int lz_instr_offset; // Instruction offset where the lazy import was created.
25-
} PyLazyImportObject;
26-
27-
28-
PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import);
2917
PyAPI_FUNC(PyObject *) _PyLazyImport_New(
30-
struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from, PyObject *attr);
18+
struct _PyInterpreterFrame *frame, PyObject *builtins,
19+
PyObject *name, PyObject *fromlist);
20+
21+
extern int _PyLazyImport_IsResolving(PyThreadState *tstate, PyObject *op);
22+
23+
// Resolve a placeholder and replace its binding if it is unchanged or holds
24+
// the child module published by the normal importer during resolution.
25+
// namespace is the source captured during lookup, before resolution runs.
26+
PyAPI_FUNC(PyObject *) _PyLazyImport_Reify(
27+
PyThreadState *tstate, PyObject *placeholder,
28+
PyObject *name, PyObject *ns);
3129

3230
#ifdef __cplusplus
3331
}

‎Include/internal/pycore_opcode_metadata.h‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Include/internal/pycore_tstate.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ typedef struct _PyThreadStateImpl {
5151
PyObject *asyncio_running_loop; // Strong reference
5252
PyObject *asyncio_running_task; // Strong reference
5353

54+
// Reused for the thread's lifetime; owns placeholders while they resolve.
55+
PyObject *lazy_imports;
56+
5457
// Distinguishes between yield and return from PyEval_EvalFrame().
5558
// See gen_send_ex2() in Objects/genobject.c
5659
enum {

‎Include/internal/pycore_uop_metadata.h‎

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎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
@@ -625,6 +625,14 @@ def test_dunder_lazy_import_invalid_arguments(self):
625625
with self.assertRaises(TypeError):
626626
__lazy_import__("sys", globals=1)
627627

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+
628636
def test_dunder_lazy_import_builtins(self):
629637
"""__lazy_import__ should use module's __builtins__ for __import__."""
630638
from test.test_lazy_import.data import dunder_lazy_import_builtins
@@ -774,6 +782,28 @@ def test_missing_lazy_from_import_shows_chained_traceback(self):
774782
""")
775783
assert_python_ok("-c", code)
776784

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+
777807
def test_reification_retries_on_failure(self):
778808
"""Failed reification should allow retry on subsequent access.
779809
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

0 commit comments

Comments
 (0)