Skip to content

Commit a6a63b6

Browse files
pablogsalencukouhugovk
authored
gh-142349: Simplify lazy import resolution (#158282)
Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
1 parent c7a79bd commit a6a63b6

23 files changed

Lines changed: 935 additions & 940 deletions

‎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,
@@ -147,8 +152,8 @@ PyAPI_FUNC(Py_ssize_t) _PyDictKeys_StringLookup(PyDictKeysObject* dictkeys, PyOb
147152
*/
148153
PyAPI_FUNC(Py_ssize_t) _PyDictKeys_StringLookupAndVersion(PyDictKeysObject* dictkeys, PyObject *key, uint32_t *version);
149154
PyAPI_FUNC(Py_ssize_t) _PyDictKeys_StringLookupSplit(PyDictKeysObject* dictkeys, PyObject *key);
150-
PyAPI_FUNC(PyObject *)_PyDict_LoadGlobal(PyDictObject *, PyDictObject *, PyObject *);
151-
PyAPI_FUNC(void) _PyDict_LoadGlobalStackRef(PyDictObject *, PyDictObject *, PyObject *, _PyStackRef *);
155+
// Return the borrowed source dictionary, or NULL if absent or on error.
156+
PyAPI_FUNC(PyObject *) _PyDict_LoadGlobalStackRef(PyDictObject *, PyDictObject *, PyObject *, _PyStackRef *);
152157

153158
// Loads the __builtins__ object from the globals dict. Returns a new reference.
154159
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
@@ -352,15 +352,12 @@ struct _import_state {
352352
PyObject *lazy_import_func;
353353
int lazy_imports_mode;
354354
PyObject *lazy_imports_filter;
355-
PyObject *lazy_importing_modules;
356355
// The set stored in sys.lazy_modules if values that have been
357356
// lazily imported. This value is only for debugging/introspection
358357
// purposes and is not used by the runtime.
359358
PyObject *lazy_modules;
360359
// A dict mapping package names to a set of submodule names that
361-
// have been imported lazily from packages which have been imported
362-
// lazily. When the package is reified we need to add a
363-
// LazyImportObject which refers to the submodule on the module.
360+
// may need to be imported when an attribute is missing on the package.
364361
PyObject *lazy_pending_submodules;
365362
#ifdef Py_GIL_DISABLED
366363
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
@@ -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")

0 commit comments

Comments
 (0)