From 731c59e9faa19fc4e638a1b0b0926259c7e7bd04 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Mon, 29 Jun 2026 03:23:15 +0800 Subject: [PATCH 01/14] gh-152298: Replace lazy import globals on resolve --- Include/internal/pycore_lazyimportobject.h | 1 + Lib/test/test_lazy_import/__init__.py | 49 ++++++++++++------- ...-06-29-03-18-22.gh-issue-152298.X3zQpA.rst | 2 + Objects/lazyimportobject.c | 46 ++++++++++++++++- 4 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-03-18-22.gh-issue-152298.X3zQpA.rst diff --git a/Include/internal/pycore_lazyimportobject.h b/Include/internal/pycore_lazyimportobject.h index b81e4211b08ff39..c3dbda0dd5969b4 100644 --- a/Include/internal/pycore_lazyimportobject.h +++ b/Include/internal/pycore_lazyimportobject.h @@ -19,6 +19,7 @@ typedef struct { PyObject *lz_builtins; PyObject *lz_from; PyObject *lz_attr; + PyObject *lz_globals; // Frame information for the original import location. PyCodeObject *lz_code; // Code object where the lazy import was created. int lz_instr_offset; // Instruction offset where the lazy import was created. diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 4658882243d65ff..dd7b87e68136752 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -881,32 +881,45 @@ def test_direct_access_triggers_reification(self): self.assertIn("test.test_lazy_import.data.basic2", sys.modules) def test_resolve_method_forces_reification(self): - """Calling resolve() on lazy proxy should force reification. - - Note: Must access lazy proxy from within a function to avoid automatic - reification by LOAD_NAME at module level. - """ + """Calling resolve() on lazy proxy should force reification.""" code = textwrap.dedent(""" - import sys + import builtins import types - lazy from test.test_lazy_import.data.basic2 import x + real_import = builtins.__import__ + calls = [] - assert 'test.test_lazy_import.data.basic2' not in sys.modules + lazy import target_module as target - def test_resolve(): - g = globals() - lazy_obj = g['x'] - assert type(lazy_obj) is types.LazyImportType, f"Expected lazy proxy, got {type(lazy_obj)}" + def custom_import(name, globals=None, locals=None, fromlist=None, level=0): + if name == "target_module": + index = len(calls) + 1 + calls.append((index, name, globals.get("__name__"), fromlist)) + module = types.ModuleType(name) + module.VALUE = f"value-{index}" + return module + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + def test_resolve(): + g = globals() + lazy_obj = g["target"] + assert type(lazy_obj) is types.LazyImportType, ( + f"Expected lazy proxy, got {type(lazy_obj)}" + ) - resolved = lazy_obj.resolve() + resolved = lazy_obj.resolve() - # Now module should be loaded - assert 'test.test_lazy_import.data.basic2' in sys.modules - assert resolved == 42 # x is 42 in basic2.py - return True + assert resolved.VALUE == "value-1" + assert g["target"] is resolved + return True - assert test_resolve() + assert test_resolve() + assert target.VALUE == "value-1" + assert calls == [(1, "target_module", "__main__", None)], calls + finally: + builtins.__import__ = real_import print("OK") """) result = subprocess.run( diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-03-18-22.gh-issue-152298.X3zQpA.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-03-18-22.gh-issue-152298.X3zQpA.rst new file mode 100644 index 000000000000000..873199579192004 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-03-18-22.gh-issue-152298.X3zQpA.rst @@ -0,0 +1,2 @@ +Fix :meth:`!types.LazyImportType.resolve` so resolving a module-level lazy +import replaces the original lazy proxy in the module globals. diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index fa1eb25047d9617..100d07db9f447ff 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -2,6 +2,8 @@ #include "Python.h" #include "pycore_ceval.h" +#include "pycore_critical_section.h" +#include "pycore_dict.h" #include "pycore_frame.h" #include "pycore_import.h" #include "pycore_interpframe.h" @@ -33,12 +35,16 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins, PyObject *name m->lz_builtins = Py_XNewRef(builtins); m->lz_from = Py_NewRef(name); m->lz_attr = Py_XNewRef(fromlist); + m->lz_globals = NULL; // Capture frame information for the original import location. m->lz_code = NULL; m->lz_instr_offset = -1; if (frame != NULL) { + if (frame->f_globals != NULL) { + m->lz_globals = Py_NewRef(frame->f_globals); + } PyCodeObject *code = _PyFrame_GetCode(frame); if (code != NULL) { m->lz_code = (PyCodeObject *)Py_NewRef(code); @@ -58,6 +64,7 @@ lazy_import_traverse(PyObject *op, visitproc visit, void *arg) Py_VISIT(m->lz_builtins); Py_VISIT(m->lz_from); Py_VISIT(m->lz_attr); + Py_VISIT(m->lz_globals); Py_VISIT(m->lz_code); return 0; } @@ -69,6 +76,7 @@ lazy_import_clear(PyObject *op) Py_CLEAR(m->lz_builtins); Py_CLEAR(m->lz_from); Py_CLEAR(m->lz_attr); + Py_CLEAR(m->lz_globals); Py_CLEAR(m->lz_code); return 0; } @@ -116,10 +124,46 @@ _PyLazyImport_GetName(PyObject *op) return lazy_import_name(lazy_import); } +static int +lazy_import_replace_globals(PyObject *op, PyObject *resolved) +{ + PyLazyImportObject *m = PyLazyImportObject_CAST(op); + if (m->lz_globals == NULL || !PyDict_Check(m->lz_globals)) { + return 0; + } + + int err = 0; + Py_ssize_t pos = 0; + PyObject *key, *value; + Py_hash_t hash; + + Py_BEGIN_CRITICAL_SECTION(m->lz_globals); + while (_PyDict_Next(m->lz_globals, &pos, &key, &value, &hash)) { + if (value == op) { + err = _PyDict_SetItem_KnownHash_LockHeld( + (PyDictObject *)m->lz_globals, key, resolved, hash); + if (err < 0) { + break; + } + } + } + Py_END_CRITICAL_SECTION(); + + return err; +} + static PyObject * lazy_import_resolve(PyObject *self, PyObject *args) { - return _PyImport_LoadLazyImportTstate(PyThreadState_GET(), self); + PyObject *resolved = _PyImport_LoadLazyImportTstate(PyThreadState_GET(), self); + if (resolved == NULL) { + return NULL; + } + if (lazy_import_replace_globals(self, resolved) < 0) { + Py_DECREF(resolved); + return NULL; + } + return resolved; } static PyMethodDef lazy_import_methods[] = { From fec811deb01f5298b3892f3a8a103b75ff4d85ff Mon Sep 17 00:00:00 2001 From: 1sgtpepper <1sgtpepper@users.noreply.github.com> Date: Sat, 4 Jul 2026 11:04:36 +0800 Subject: [PATCH 02/14] gh-152298: Preserve lazy import resolve binding --- Include/internal/pycore_lazyimportobject.h | 4 + Lib/test/test_lazy_import/__init__.py | 195 ++++++++++++++++++++- Objects/lazyimportobject.c | 59 +++++-- Python/bytecodes.c | 20 ++- Python/executor_cases.c.h | 24 ++- Python/generated_cases.c.h | 24 ++- 6 files changed, 296 insertions(+), 30 deletions(-) diff --git a/Include/internal/pycore_lazyimportobject.h b/Include/internal/pycore_lazyimportobject.h index c3dbda0dd5969b4..24d952613c70af8 100644 --- a/Include/internal/pycore_lazyimportobject.h +++ b/Include/internal/pycore_lazyimportobject.h @@ -20,6 +20,8 @@ typedef struct { PyObject *lz_from; PyObject *lz_attr; PyObject *lz_globals; + PyObject *lz_key; + Py_hash_t lz_key_hash; // Frame information for the original import location. PyCodeObject *lz_code; // Code object where the lazy import was created. int lz_instr_offset; // Instruction offset where the lazy import was created. @@ -27,6 +29,8 @@ typedef struct { PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import); +PyAPI_FUNC(int) _PyLazyImport_SetGlobalBinding( + PyObject *lazy_import, PyObject *globals, PyObject *name); PyAPI_FUNC(PyObject *) _PyLazyImport_New( struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from, PyObject *attr); diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index dd7b87e68136752..46715c9380bae4c 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -793,7 +793,7 @@ class GlobalsAndDictTests(LazyImportTestCase): """Tests for globals() and __dict__ behavior with lazy imports. PEP 810: "Calling globals() or accessing a module's __dict__ does not trigger - reification – they return the module's dictionary, and accessing lazy objects + reification - they return the module's dictionary, and accessing lazy objects through that dictionary still returns lazy proxy objects." """ @@ -880,8 +880,8 @@ def test_direct_access_triggers_reification(self): result = test.test_lazy_import.data.globals_access.get_direct() self.assertIn("test.test_lazy_import.data.basic2", sys.modules) - def test_resolve_method_forces_reification(self): - """Calling resolve() on lazy proxy should force reification.""" + def test_resolve_method_updates_original_global(self): + """resolve() should update the original global binding.""" code = textwrap.dedent(""" import builtins import types @@ -908,11 +908,13 @@ def test_resolve(): assert type(lazy_obj) is types.LazyImportType, ( f"Expected lazy proxy, got {type(lazy_obj)}" ) + g["alias"] = lazy_obj resolved = lazy_obj.resolve() assert resolved.VALUE == "value-1" assert g["target"] is resolved + assert g["alias"] is lazy_obj return True assert test_resolve() @@ -930,6 +932,193 @@ def test_resolve(): self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") self.assertIn("OK", result.stdout) + def test_resolve_method_respects_rebound_global(self): + """resolve() should not overwrite a rebound original global.""" + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + calls = [] + sentinel = object() + + lazy import target_module as target + + def custom_import(name, globals=None, locals=None, fromlist=None, level=0): + if name == "target_module": + calls.append(name) + module = types.ModuleType(name) + module.VALUE = "resolved" + return module + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + def test_resolve(): + g = globals() + lazy_obj = g["target"] + g["target"] = sentinel + resolved = lazy_obj.resolve() + assert resolved.VALUE == "resolved" + assert g["target"] is sentinel + return True + + assert test_resolve() + assert calls == ["target_module"], calls + finally: + builtins.__import__ = real_import + print("OK") + """) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True + ) + self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") + self.assertIn("OK", result.stdout) + + def test_resolve_method_respects_deleted_global(self): + """resolve() should not recreate a deleted original global.""" + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + + lazy import target_module as target + + def custom_import(name, globals=None, locals=None, fromlist=None, level=0): + if name == "target_module": + module = types.ModuleType(name) + module.VALUE = "resolved" + return module + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + def test_resolve(): + g = globals() + lazy_obj = g["target"] + del g["target"] + resolved = lazy_obj.resolve() + assert resolved.VALUE == "resolved" + assert "target" not in g + return True + + assert test_resolve() + finally: + builtins.__import__ = real_import + print("OK") + """) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True + ) + self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") + self.assertIn("OK", result.stdout) + + def test_resolve_method_failure_preserves_global_proxy(self): + """Failed resolve() should keep the global proxy for retry.""" + code = textwrap.dedent(""" + import builtins + + real_import = builtins.__import__ + calls = [] + + lazy import broken_target as target + + def custom_import(name, globals=None, locals=None, fromlist=None, level=0): + if name == "broken_target": + calls.append(name) + raise ImportError("boom") + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + def test_resolve(): + g = globals() + lazy_obj = g["target"] + try: + lazy_obj.resolve() + except ImportError: + pass + else: + raise AssertionError("resolve() unexpectedly succeeded") + assert g["target"] is lazy_obj + + try: + lazy_obj.resolve() + except ImportError: + pass + else: + raise AssertionError("resolve() unexpectedly succeeded") + assert g["target"] is lazy_obj + return True + + assert test_resolve() + assert calls == ["broken_target", "broken_target"], calls + finally: + builtins.__import__ = real_import + print("OK") + """) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True + ) + self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") + self.assertIn("OK", result.stdout) + + def test_resolve_method_updates_from_import_binding(self): + """resolve() should update the stored from-import binding key.""" + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + calls = [] + + lazy from target_module import VALUE as value + + def custom_import(name, globals=None, locals=None, fromlist=None, level=0): + if name == "target_module": + calls.append((name, fromlist)) + module = types.ModuleType(name) + module.VALUE = "value-1" + return module + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + def test_resolve(): + g = globals() + lazy_obj = g["value"] + assert type(lazy_obj) is types.LazyImportType + g["alias"] = lazy_obj + + resolved = lazy_obj.resolve() + + assert resolved == "value-1" + assert g["value"] == "value-1" + assert g["alias"] is lazy_obj + return True + + assert test_resolve() + assert value == "value-1" + assert calls == [("target_module", ("VALUE",))], calls + finally: + builtins.__import__ = real_import + print("OK") + """) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True + ) + self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") + self.assertIn("OK", result.stdout) + def test_add_lazy_to_globals(self): code = textwrap.dedent(""" import sys diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index 100d07db9f447ff..a7e4c93fcc7ecff 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -36,15 +36,14 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins, PyObject *name m->lz_from = Py_NewRef(name); m->lz_attr = Py_XNewRef(fromlist); m->lz_globals = NULL; + m->lz_key = NULL; + m->lz_key_hash = -1; // Capture frame information for the original import location. m->lz_code = NULL; m->lz_instr_offset = -1; if (frame != NULL) { - if (frame->f_globals != NULL) { - m->lz_globals = Py_NewRef(frame->f_globals); - } PyCodeObject *code = _PyFrame_GetCode(frame); if (code != NULL) { m->lz_code = (PyCodeObject *)Py_NewRef(code); @@ -65,6 +64,7 @@ lazy_import_traverse(PyObject *op, visitproc visit, void *arg) Py_VISIT(m->lz_from); Py_VISIT(m->lz_attr); Py_VISIT(m->lz_globals); + Py_VISIT(m->lz_key); Py_VISIT(m->lz_code); return 0; } @@ -77,6 +77,7 @@ lazy_import_clear(PyObject *op) Py_CLEAR(m->lz_from); Py_CLEAR(m->lz_attr); Py_CLEAR(m->lz_globals); + Py_CLEAR(m->lz_key); Py_CLEAR(m->lz_code); return 0; } @@ -124,31 +125,55 @@ _PyLazyImport_GetName(PyObject *op) return lazy_import_name(lazy_import); } +int +_PyLazyImport_SetGlobalBinding(PyObject *op, PyObject *globals, PyObject *name) +{ + assert(PyLazyImport_CheckExact(op)); + + PyLazyImportObject *m = PyLazyImportObject_CAST(op); + if (m->lz_key != NULL) { + return 0; + } + + assert(PyDict_CheckExact(globals)); + Py_hash_t hash = PyObject_Hash(name); + if (hash == -1) { + return -1; + } + + m->lz_globals = Py_NewRef(globals); + m->lz_key = Py_NewRef(name); + m->lz_key_hash = hash; + return 0; +} + static int -lazy_import_replace_globals(PyObject *op, PyObject *resolved) +lazy_import_replace_global_binding(PyObject *op, PyObject *resolved) { PyLazyImportObject *m = PyLazyImportObject_CAST(op); - if (m->lz_globals == NULL || !PyDict_Check(m->lz_globals)) { + if (m->lz_globals == NULL || m->lz_key == NULL) { return 0; } + assert(PyDict_CheckExact(m->lz_globals)); + int err = 0; - Py_ssize_t pos = 0; - PyObject *key, *value; - Py_hash_t hash; + PyObject *current = NULL; Py_BEGIN_CRITICAL_SECTION(m->lz_globals); - while (_PyDict_Next(m->lz_globals, &pos, &key, &value, &hash)) { - if (value == op) { - err = _PyDict_SetItem_KnownHash_LockHeld( - (PyDictObject *)m->lz_globals, key, resolved, hash); - if (err < 0) { - break; - } - } + int found = _PyDict_GetItemRef_KnownHash_LockHeld( + (PyDictObject *)m->lz_globals, m->lz_key, m->lz_key_hash, ¤t); + if (found < 0) { + err = -1; + } + else if (found && current == op) { + err = _PyDict_SetItem_KnownHash_LockHeld( + (PyDictObject *)m->lz_globals, m->lz_key, resolved, + m->lz_key_hash); } Py_END_CRITICAL_SECTION(); + Py_XDECREF(current); return err; } @@ -159,7 +184,7 @@ lazy_import_resolve(PyObject *self, PyObject *args) if (resolved == NULL) { return NULL; } - if (lazy_import_replace_globals(self, resolved) < 0) { + if (lazy_import_replace_global_binding(self, resolved) < 0) { Py_DECREF(resolved); return NULL; } diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 31596e0bc7a31d2..b0a310295623c65 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -19,7 +19,7 @@ #include "pycore_instruments.h" #include "pycore_interpolation.h" // _PyInterpolation_Build() #include "pycore_intrinsics.h" -#include "pycore_lazyimportobject.h" // PyLazyImport_CheckExact() +#include "pycore_lazyimportobject.h" // PyLazyImport_CheckExact(), _PyLazyImport_SetGlobalBinding() #include "pycore_long.h" // _PyLong_ExactDealloc(), _PyLong_GetZero() #include "pycore_moduleobject.h" // PyModuleObject #include "pycore_object.h" // _PyObject_GC_TRACK() @@ -1997,6 +1997,7 @@ dummy_func( inst(STORE_NAME, (v -- )) { PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *ns = LOCALS(); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); int err; if (ns == NULL) { _PyErr_Format(tstate, PyExc_SystemError, @@ -2005,10 +2006,15 @@ dummy_func( ERROR_IF(true); } if (PyDict_CheckExact(ns)) { - err = PyDict_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); + err = PyDict_SetItem(ns, name, value); + if (err == 0 && ns == GLOBALS() && + PyLazyImport_CheckExact(value)) + { + err = _PyLazyImport_SetGlobalBinding(value, ns, name); + } } else { - err = PyObject_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); + err = PyObject_SetItem(ns, name, value); } PyStackRef_CLOSE(v); ERROR_IF(err); @@ -2189,7 +2195,13 @@ dummy_func( inst(STORE_GLOBAL, (v --)) { PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); - int err = PyDict_SetItem(GLOBALS(), name, PyStackRef_AsPyObjectBorrow(v)); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); + int err = PyDict_SetItem(GLOBALS(), name, value); + if (err == 0 && PyDict_CheckExact(GLOBALS()) && + PyLazyImport_CheckExact(value)) + { + err = _PyLazyImport_SetGlobalBinding(value, GLOBALS(), name); + } PyStackRef_CLOSE(v); ERROR_IF(err); } diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 943d4c557886822..5828ee829458d55 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -9567,6 +9567,7 @@ v = _stack_item_0; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *ns = LOCALS(); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); int err; if (ns == NULL) { stack_pointer[0] = v; @@ -9592,8 +9593,16 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - err = PyDict_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); + err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && ns == GLOBALS() && + PyLazyImport_CheckExact(value)) + { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_SetGlobalBinding(value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } } else { stack_pointer[0] = v; @@ -9601,7 +9610,7 @@ ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - err = PyObject_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); + err = PyObject_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); } stack_pointer += -1; @@ -10076,13 +10085,22 @@ oparg = CURRENT_OPARG(); v = _stack_item_0; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); stack_pointer[0] = v; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, PyStackRef_AsPyObjectBorrow(v)); + int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && PyDict_CheckExact(GLOBALS()) && + PyLazyImport_CheckExact(value)) + { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_SetGlobalBinding(value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 0da86abed67f63b..e44dcf24622d285 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -12568,10 +12568,19 @@ _PyStackRef v; v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, PyStackRef_AsPyObjectBorrow(v)); + int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && PyDict_CheckExact(GLOBALS()) && + PyLazyImport_CheckExact(value)) + { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_SetGlobalBinding(value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); @@ -12596,6 +12605,7 @@ v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *ns = LOCALS(); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); int err; if (ns == NULL) { _PyFrame_SetStackPointer(frame, stack_pointer); @@ -12614,13 +12624,21 @@ if (PyDict_CheckExact(ns)) { _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - err = PyDict_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); + err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && ns == GLOBALS() && + PyLazyImport_CheckExact(value)) + { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_SetGlobalBinding(value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } } else { _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - err = PyObject_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); + err = PyObject_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); } stack_pointer += -1; From 25cb033be34c27c01d8d433d4a1198584f8d347c Mon Sep 17 00:00:00 2001 From: 1sgtpepper <1sgtpepper@users.noreply.github.com> Date: Sat, 4 Jul 2026 11:26:23 +0800 Subject: [PATCH 03/14] gh-152298: Make lazy resolve binding publication atomic --- Include/internal/pycore_lazyimportobject.h | 2 +- Lib/test/test_lazy_import/__init__.py | 2 +- Objects/lazyimportobject.c | 67 ++++++++++++++++------ Python/bytecodes.c | 25 ++++---- Python/executor_cases.c.h | 57 ++++++++++-------- Python/generated_cases.c.h | 39 +++++++------ 6 files changed, 121 insertions(+), 71 deletions(-) diff --git a/Include/internal/pycore_lazyimportobject.h b/Include/internal/pycore_lazyimportobject.h index 24d952613c70af8..c97ef8445ee10e3 100644 --- a/Include/internal/pycore_lazyimportobject.h +++ b/Include/internal/pycore_lazyimportobject.h @@ -29,7 +29,7 @@ typedef struct { PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import); -PyAPI_FUNC(int) _PyLazyImport_SetGlobalBinding( +PyAPI_FUNC(int) _PyLazyImport_SetGlobalBindingAndDictItem( PyObject *lazy_import, PyObject *globals, PyObject *name); PyAPI_FUNC(PyObject *) _PyLazyImport_New( struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from, PyObject *attr); diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 46715c9380bae4c..09afd05d17b0fd6 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -793,7 +793,7 @@ class GlobalsAndDictTests(LazyImportTestCase): """Tests for globals() and __dict__ behavior with lazy imports. PEP 810: "Calling globals() or accessing a module's __dict__ does not trigger - reification - they return the module's dictionary, and accessing lazy objects + reification – they return the module's dictionary, and accessing lazy objects through that dictionary still returns lazy proxy objects." """ diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index a7e4c93fcc7ecff..b9ffea9e35388d1 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -126,54 +126,89 @@ _PyLazyImport_GetName(PyObject *op) } int -_PyLazyImport_SetGlobalBinding(PyObject *op, PyObject *globals, PyObject *name) +_PyLazyImport_SetGlobalBindingAndDictItem(PyObject *op, PyObject *globals, + PyObject *name) { assert(PyLazyImport_CheckExact(op)); + assert(PyDict_CheckExact(globals)); PyLazyImportObject *m = PyLazyImportObject_CAST(op); - if (m->lz_key != NULL) { - return 0; - } - - assert(PyDict_CheckExact(globals)); Py_hash_t hash = PyObject_Hash(name); if (hash == -1) { return -1; } - m->lz_globals = Py_NewRef(globals); - m->lz_key = Py_NewRef(name); - m->lz_key_hash = hash; - return 0; + PyObject *discard_globals = NULL; + PyObject *discard_key = NULL; + int err; + int recorded = 0; + + Py_BEGIN_CRITICAL_SECTION2(op, globals); + if (m->lz_key == NULL) { + // Record the owner binding before publishing the proxy. resolve() + // may update only this key; aliases must not retarget it. + m->lz_globals = Py_NewRef(globals); + m->lz_key = Py_NewRef(name); + m->lz_key_hash = hash; + recorded = 1; + } + err = _PyDict_SetItem_KnownHash_LockHeld( + (PyDictObject *)globals, name, op, hash); + if (err < 0 && recorded) { + discard_globals = m->lz_globals; + discard_key = m->lz_key; + m->lz_globals = NULL; + m->lz_key = NULL; + m->lz_key_hash = -1; + } + Py_END_CRITICAL_SECTION2(); + + Py_XDECREF(discard_globals); + Py_XDECREF(discard_key); + return err; } static int lazy_import_replace_global_binding(PyObject *op, PyObject *resolved) { PyLazyImportObject *m = PyLazyImportObject_CAST(op); - if (m->lz_globals == NULL || m->lz_key == NULL) { + PyObject *globals = NULL; + PyObject *key = NULL; + Py_hash_t key_hash = -1; + + Py_BEGIN_CRITICAL_SECTION(op); + if (m->lz_globals != NULL && m->lz_key != NULL) { + globals = Py_NewRef(m->lz_globals); + key = Py_NewRef(m->lz_key); + key_hash = m->lz_key_hash; + } + Py_END_CRITICAL_SECTION(); + + if (globals == NULL) { return 0; } - assert(PyDict_CheckExact(m->lz_globals)); + assert(key != NULL); + assert(PyDict_CheckExact(globals)); int err = 0; PyObject *current = NULL; - Py_BEGIN_CRITICAL_SECTION(m->lz_globals); + Py_BEGIN_CRITICAL_SECTION(globals); int found = _PyDict_GetItemRef_KnownHash_LockHeld( - (PyDictObject *)m->lz_globals, m->lz_key, m->lz_key_hash, ¤t); + (PyDictObject *)globals, key, key_hash, ¤t); if (found < 0) { err = -1; } else if (found && current == op) { err = _PyDict_SetItem_KnownHash_LockHeld( - (PyDictObject *)m->lz_globals, m->lz_key, resolved, - m->lz_key_hash); + (PyDictObject *)globals, key, resolved, key_hash); } Py_END_CRITICAL_SECTION(); Py_XDECREF(current); + Py_DECREF(globals); + Py_DECREF(key); return err; } diff --git a/Python/bytecodes.c b/Python/bytecodes.c index b0a310295623c65..03b425f7fe1a45a 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -19,7 +19,7 @@ #include "pycore_instruments.h" #include "pycore_interpolation.h" // _PyInterpolation_Build() #include "pycore_intrinsics.h" -#include "pycore_lazyimportobject.h" // PyLazyImport_CheckExact(), _PyLazyImport_SetGlobalBinding() +#include "pycore_lazyimportobject.h" // PyLazyImport_CheckExact(), _PyLazyImport_SetGlobalBindingAndDictItem() #include "pycore_long.h" // _PyLong_ExactDealloc(), _PyLong_GetZero() #include "pycore_moduleobject.h" // PyModuleObject #include "pycore_object.h" // _PyObject_GC_TRACK() @@ -2006,11 +2006,12 @@ dummy_func( ERROR_IF(true); } if (PyDict_CheckExact(ns)) { - err = PyDict_SetItem(ns, name, value); - if (err == 0 && ns == GLOBALS() && - PyLazyImport_CheckExact(value)) - { - err = _PyLazyImport_SetGlobalBinding(value, ns, name); + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + err = _PyLazyImport_SetGlobalBindingAndDictItem( + value, ns, name); + } + else { + err = PyDict_SetItem(ns, name, value); } } else { @@ -2196,11 +2197,13 @@ dummy_func( inst(STORE_GLOBAL, (v --)) { PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - int err = PyDict_SetItem(GLOBALS(), name, value); - if (err == 0 && PyDict_CheckExact(GLOBALS()) && - PyLazyImport_CheckExact(value)) - { - err = _PyLazyImport_SetGlobalBinding(value, GLOBALS(), name); + int err; + if (PyDict_CheckExact(GLOBALS()) && PyLazyImport_CheckExact(value)) { + err = _PyLazyImport_SetGlobalBindingAndDictItem( + value, GLOBALS(), name); + } + else { + err = PyDict_SetItem(GLOBALS(), name, value); } PyStackRef_CLOSE(v); ERROR_IF(err); diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 5828ee829458d55..551cf29e615a9ef 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -9588,19 +9588,23 @@ JUMP_TO_ERROR(); } if (PyDict_CheckExact(ns)) { - stack_pointer[0] = v; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - err = PyDict_SetItem(ns, name, value); - _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && ns == GLOBALS() && - PyLazyImport_CheckExact(value)) - { - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + stack_pointer[0] = v; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_SetGlobalBindingAndDictItem( + value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } + else { + stack_pointer[0] = v; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - err = _PyLazyImport_SetGlobalBinding(value, ns, name); + err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); } } @@ -10086,19 +10090,24 @@ v = _stack_item_0; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - stack_pointer[0] = v; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, value); - _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && PyDict_CheckExact(GLOBALS()) && - PyLazyImport_CheckExact(value)) - { - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + int err; + if (PyDict_CheckExact(GLOBALS()) && PyLazyImport_CheckExact(value)) { + stack_pointer[0] = v; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_SetGlobalBindingAndDictItem( + value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } + else { + stack_pointer[0] = v; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - err = _PyLazyImport_SetGlobalBinding(value, GLOBALS(), name); + err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); } stack_pointer += -1; diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index e44dcf24622d285..4ffdcb92a55cc1b 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -12569,16 +12569,18 @@ v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, value); - _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && PyDict_CheckExact(GLOBALS()) && - PyLazyImport_CheckExact(value)) - { - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + int err; + if (PyDict_CheckExact(GLOBALS()) && PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_SetGlobalBindingAndDictItem( + value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } + else { + _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - err = _PyLazyImport_SetGlobalBinding(value, GLOBALS(), name); + err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); } stack_pointer += -1; @@ -12622,16 +12624,17 @@ JUMP_TO_LABEL(error); } if (PyDict_CheckExact(ns)) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - err = PyDict_SetItem(ns, name, value); - _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && ns == GLOBALS() && - PyLazyImport_CheckExact(value)) - { - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_SetGlobalBindingAndDictItem( + value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } + else { + _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - err = _PyLazyImport_SetGlobalBinding(value, ns, name); + err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); } } From a65f4cdfb75172e6eba1fc2263c768f7f7306af5 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 4 Jul 2026 16:25:06 +0800 Subject: [PATCH 04/14] gh-152298: Cache lazy import resolve results --- Include/internal/pycore_lazyimportobject.h | 4 + Lib/test/test_lazy_import/__init__.py | 100 +++++++++++++++++++ Objects/lazyimportobject.c | 109 ++++++++++++++------- Python/import.c | 13 +++ 4 files changed, 192 insertions(+), 34 deletions(-) diff --git a/Include/internal/pycore_lazyimportobject.h b/Include/internal/pycore_lazyimportobject.h index c97ef8445ee10e3..c6bfb2c4bde48bf 100644 --- a/Include/internal/pycore_lazyimportobject.h +++ b/Include/internal/pycore_lazyimportobject.h @@ -21,6 +21,7 @@ typedef struct { PyObject *lz_attr; PyObject *lz_globals; PyObject *lz_key; + PyObject *lz_resolved; // Protected by the lazy object lock. Py_hash_t lz_key_hash; // Frame information for the original import location. PyCodeObject *lz_code; // Code object where the lazy import was created. @@ -29,6 +30,9 @@ typedef struct { PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import); +PyAPI_FUNC(PyObject *) _PyLazyImport_GetResolved(PyObject *lazy_import); +PyAPI_FUNC(int) _PyLazyImport_FinishResolve( + PyObject *lazy_import, PyObject *resolved); PyAPI_FUNC(int) _PyLazyImport_SetGlobalBindingAndDictItem( PyObject *lazy_import, PyObject *globals, PyObject *name); PyAPI_FUNC(PyObject *) _PyLazyImport_New( diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 09afd05d17b0fd6..75c75e20488ded4 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -1119,6 +1119,106 @@ def test_resolve(): self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") self.assertIn("OK", result.stdout) + def test_resolve_method_caches_copied_proxy(self): + """Repeated resolve() on a copied proxy should not import again.""" + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + calls = [] + + lazy import target_module as target + + def custom_import(name, globals=None, locals=None, fromlist=None, level=0): + if name == "target_module": + index = len(calls) + 1 + calls.append(name) + module = types.ModuleType(name) + module.VALUE = f"value-{index}" + return module + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + def test_resolve(): + g = globals() + lazy_obj = g["target"] + g["alias"] = lazy_obj + + resolved = lazy_obj.resolve() + again = g["alias"].resolve() + + assert again is resolved + assert resolved.VALUE == "value-1" + assert g["target"] is resolved + assert g["alias"] is lazy_obj + return True + + assert test_resolve() + assert calls == ["target_module"], calls + finally: + builtins.__import__ = real_import + print("OK") + """) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True + ) + self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") + self.assertIn("OK", result.stdout) + + def test_resolve_method_does_not_reclaim_stale_binding(self): + """A stale owner binding should not be retargeted by a later resolve.""" + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + calls = [] + sentinel = object() + + lazy import target_module as target + + def custom_import(name, globals=None, locals=None, fromlist=None, level=0): + if name == "target_module": + calls.append(name) + module = types.ModuleType(name) + module.VALUE = "resolved" + return module + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + def test_resolve(): + g = globals() + lazy_obj = g["target"] + g["target"] = sentinel + + resolved = lazy_obj.resolve() + assert g["target"] is sentinel + + g["target"] = lazy_obj + again = lazy_obj.resolve() + assert again is resolved + assert g["target"] is lazy_obj + return True + + assert test_resolve() + assert calls == ["target_module"], calls + finally: + builtins.__import__ = real_import + print("OK") + """) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True + ) + self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") + self.assertIn("OK", result.stdout) + def test_add_lazy_to_globals(self): code = textwrap.dedent(""" import sys diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index b9ffea9e35388d1..f267984a07a3e7b 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -37,6 +37,7 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins, PyObject *name m->lz_attr = Py_XNewRef(fromlist); m->lz_globals = NULL; m->lz_key = NULL; + m->lz_resolved = NULL; m->lz_key_hash = -1; // Capture frame information for the original import location. @@ -65,6 +66,7 @@ lazy_import_traverse(PyObject *op, visitproc visit, void *arg) Py_VISIT(m->lz_attr); Py_VISIT(m->lz_globals); Py_VISIT(m->lz_key); + Py_VISIT(m->lz_resolved); Py_VISIT(m->lz_code); return 0; } @@ -78,6 +80,7 @@ lazy_import_clear(PyObject *op) Py_CLEAR(m->lz_attr); Py_CLEAR(m->lz_globals); Py_CLEAR(m->lz_key); + Py_CLEAR(m->lz_resolved); Py_CLEAR(m->lz_code); return 0; } @@ -125,6 +128,19 @@ _PyLazyImport_GetName(PyObject *op) return lazy_import_name(lazy_import); } +PyObject * +_PyLazyImport_GetResolved(PyObject *op) +{ + PyLazyImportObject *m = PyLazyImportObject_CAST(op); + assert(PyLazyImport_CheckExact(op)); + + PyObject *resolved = NULL; + Py_BEGIN_CRITICAL_SECTION(op); + resolved = Py_XNewRef(m->lz_resolved); + Py_END_CRITICAL_SECTION(); + return resolved; +} + int _PyLazyImport_SetGlobalBindingAndDictItem(PyObject *op, PyObject *globals, PyObject *name) @@ -144,7 +160,7 @@ _PyLazyImport_SetGlobalBindingAndDictItem(PyObject *op, PyObject *globals, int recorded = 0; Py_BEGIN_CRITICAL_SECTION2(op, globals); - if (m->lz_key == NULL) { + if (m->lz_key == NULL && m->lz_resolved == NULL) { // Record the owner binding before publishing the proxy. resolve() // may update only this key; aliases must not retarget it. m->lz_globals = Py_NewRef(globals); @@ -168,15 +184,22 @@ _PyLazyImport_SetGlobalBindingAndDictItem(PyObject *op, PyObject *globals, return err; } -static int -lazy_import_replace_global_binding(PyObject *op, PyObject *resolved) +int +_PyLazyImport_FinishResolve(PyObject *op, PyObject *resolved) { PyLazyImportObject *m = PyLazyImportObject_CAST(op); PyObject *globals = NULL; PyObject *key = NULL; Py_hash_t key_hash = -1; + assert(PyLazyImport_CheckExact(op)); + assert(!PyLazyImport_CheckExact(resolved)); + Py_BEGIN_CRITICAL_SECTION(op); + if (m->lz_resolved != NULL) { + Py_END_CRITICAL_SECTION(); + return 0; + } if (m->lz_globals != NULL && m->lz_key != NULL) { globals = Py_NewRef(m->lz_globals); key = Py_NewRef(m->lz_key); @@ -184,46 +207,63 @@ lazy_import_replace_global_binding(PyObject *op, PyObject *resolved) } Py_END_CRITICAL_SECTION(); - if (globals == NULL) { - return 0; - } - - assert(key != NULL); - assert(PyDict_CheckExact(globals)); - int err = 0; PyObject *current = NULL; - Py_BEGIN_CRITICAL_SECTION(globals); - int found = _PyDict_GetItemRef_KnownHash_LockHeld( - (PyDictObject *)globals, key, key_hash, ¤t); - if (found < 0) { - err = -1; + if (globals != NULL) { + assert(key != NULL); + assert(PyDict_CheckExact(globals)); + + Py_BEGIN_CRITICAL_SECTION(globals); + int found = _PyDict_GetItemRef_KnownHash_LockHeld( + (PyDictObject *)globals, key, key_hash, ¤t); + if (found < 0) { + err = -1; + } + else if (found && current == op) { + err = _PyDict_SetItem_KnownHash_LockHeld( + (PyDictObject *)globals, key, resolved, key_hash); + } + Py_END_CRITICAL_SECTION(); + + Py_XDECREF(current); + if (err < 0) { + Py_DECREF(globals); + Py_DECREF(key); + return err; + } } - else if (found && current == op) { - err = _PyDict_SetItem_KnownHash_LockHeld( - (PyDictObject *)globals, key, resolved, key_hash); + + PyObject *discard_globals = NULL; + PyObject *discard_key = NULL; + Py_BEGIN_CRITICAL_SECTION(op); + if (m->lz_resolved == NULL) { + m->lz_resolved = Py_NewRef(resolved); + } + if (globals != NULL && + m->lz_globals == globals && + m->lz_key == key && + m->lz_key_hash == key_hash) + { + discard_globals = m->lz_globals; + discard_key = m->lz_key; + m->lz_globals = NULL; + m->lz_key = NULL; + m->lz_key_hash = -1; } Py_END_CRITICAL_SECTION(); - Py_XDECREF(current); - Py_DECREF(globals); - Py_DECREF(key); - return err; + Py_XDECREF(discard_globals); + Py_XDECREF(discard_key); + Py_XDECREF(globals); + Py_XDECREF(key); + return 0; } static PyObject * lazy_import_resolve(PyObject *self, PyObject *args) { - PyObject *resolved = _PyImport_LoadLazyImportTstate(PyThreadState_GET(), self); - if (resolved == NULL) { - return NULL; - } - if (lazy_import_replace_global_binding(self, resolved) < 0) { - Py_DECREF(resolved); - return NULL; - } - return resolved; + return _PyImport_LoadLazyImportTstate(PyThreadState_GET(), self); } static PyMethodDef lazy_import_methods[] = { @@ -241,9 +281,10 @@ PyDoc_STRVAR(lazy_import_doc, "\n" "Represents a lazy import that will be resolved on first use.\n" "\n" -"Instances of this object accessed from the global scope will be\n" -"automatically imported based upon their name and then replaced with\n" -"the imported value."); +"A successful resolution is cached. Instances of this object accessed\n" +"from the global scope will be automatically imported based upon their\n" +"name. The original global is replaced only if it still refers to this\n" +"lazy import object."); PyTypeObject PyLazyImport_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) diff --git a/Python/import.c b/Python/import.c index 6da6faf5f28cc3b..355d9f7e476a670 100644 --- a/Python/import.c +++ b/Python/import.c @@ -3897,6 +3897,12 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) // Acquire the global import lock to serialize reification _PyImport_AcquireLock(interp); + obj = _PyLazyImport_GetResolved(lazy_import); + if (obj != NULL) { + _PyImport_ReleaseLock(interp); + return obj; + } + // Check if we are already importing this module, if so, then we want to // return an error that indicates we've hit a cycle which will indicate // the value isn't yet available. @@ -4093,6 +4099,13 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) if (PySet_Discard(importing, lazy_import) < 0) { Py_CLEAR(obj); } + else if (obj != NULL) { + // Cache the result and update the original global before releasing + // the import lock, so competing reification cannot import again. + if (_PyLazyImport_FinishResolve(lazy_import, obj) < 0) { + Py_CLEAR(obj); + } + } // Release the global import lock. _PyImport_ReleaseLock(interp); From c39edf58144bf4aea0e8e3b4042206f8538078d8 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 4 Jul 2026 17:01:50 +0800 Subject: [PATCH 05/14] gh-152298: Fix lazy resolve critical section --- Objects/lazyimportobject.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index f267984a07a3e7b..9f0ff316d25ca5b 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -191,23 +191,27 @@ _PyLazyImport_FinishResolve(PyObject *op, PyObject *resolved) PyObject *globals = NULL; PyObject *key = NULL; Py_hash_t key_hash = -1; + int err = 0; + int already_resolved = 0; assert(PyLazyImport_CheckExact(op)); assert(!PyLazyImport_CheckExact(resolved)); Py_BEGIN_CRITICAL_SECTION(op); if (m->lz_resolved != NULL) { - Py_END_CRITICAL_SECTION(); - return 0; + already_resolved = 1; } - if (m->lz_globals != NULL && m->lz_key != NULL) { + else if (m->lz_globals != NULL && m->lz_key != NULL) { globals = Py_NewRef(m->lz_globals); key = Py_NewRef(m->lz_key); key_hash = m->lz_key_hash; } Py_END_CRITICAL_SECTION(); - int err = 0; + if (already_resolved) { + return 0; + } + PyObject *current = NULL; if (globals != NULL) { From 7cbf44c155b8414f762fb51f475103f5239d84a6 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 4 Jul 2026 17:07:43 +0800 Subject: [PATCH 06/14] gh-152298: Regenerate lazy import test cases --- Modules/_testinternalcapi/test_cases.c.h | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 708aa4d966e5357..23edd4af8df63af 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -12525,10 +12525,20 @@ } } else { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - err = PyDict_SetItem(GLOBALS(), name, PyStackRef_AsPyObjectBorrow(v)); - _PyFrame_StackPointerInvalidate(frame); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); + if (PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_SetGlobalBindingAndDictItem( + value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } + else { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + err = PyDict_SetItem(GLOBALS(), name, value); + _PyFrame_StackPointerInvalidate(frame); + } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); From af3c994db4851c0356ee3edb087dd59c09ecf09a Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 4 Jul 2026 18:16:23 +0800 Subject: [PATCH 07/14] gh-152298: Guard lazy resolve writebacks --- Include/internal/pycore_lazyimportobject.h | 3 + Lib/test/test_lazy_import/__init__.py | 121 +++++++++++++++++++++ Modules/_testinternalcapi/test_cases.c.h | 18 ++- Objects/lazyimportobject.c | 60 +++++++--- Objects/moduleobject.c | 4 +- Python/bytecodes.c | 9 +- Python/ceval.c | 12 +- Python/executor_cases.c.h | 18 ++- Python/generated_cases.c.h | 18 ++- 9 files changed, 232 insertions(+), 31 deletions(-) diff --git a/Include/internal/pycore_lazyimportobject.h b/Include/internal/pycore_lazyimportobject.h index c6bfb2c4bde48bf..c74b03d3e02af82 100644 --- a/Include/internal/pycore_lazyimportobject.h +++ b/Include/internal/pycore_lazyimportobject.h @@ -33,6 +33,9 @@ PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import); PyAPI_FUNC(PyObject *) _PyLazyImport_GetResolved(PyObject *lazy_import); PyAPI_FUNC(int) _PyLazyImport_FinishResolve( PyObject *lazy_import, PyObject *resolved); +PyAPI_FUNC(int) _PyLazyImport_ReplaceDictItemIfCurrent( + PyObject *lazy_import, PyObject *dict, PyObject *name, + PyObject *resolved); PyAPI_FUNC(int) _PyLazyImport_SetGlobalBindingAndDictItem( PyObject *lazy_import, PyObject *globals, PyObject *name); PyAPI_FUNC(PyObject *) _PyLazyImport_New( diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 369201035790760..c54c2655db29b96 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -1244,6 +1244,127 @@ def test_resolve(): self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") self.assertIn("OK", result.stdout) + def test_load_global_respects_rebound_global_during_reification(self): + """LOAD_GLOBAL should not overwrite a global rebound by the import hook.""" + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + calls = [] + sentinel = object() + + lazy import target_module as target + + def custom_import(name, globals=None, locals=None, fromlist=None, level=0): + if name == "target_module": + calls.append(name) + globals["target"] = sentinel + module = types.ModuleType(name) + module.VALUE = "resolved" + return module + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + def trigger_load_global(): + return target + + resolved = trigger_load_global() + assert resolved.VALUE == "resolved" + assert globals()["target"] is sentinel + assert calls == ["target_module"], calls + finally: + builtins.__import__ = real_import + print("OK") + """) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True + ) + self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") + self.assertIn("OK", result.stdout) + + def test_load_name_respects_deleted_global_during_reification(self): + """LOAD_NAME should not recreate a global deleted by the import hook.""" + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + calls = [] + + lazy import target_module as target + + def custom_import(name, globals=None, locals=None, fromlist=None, level=0): + if name == "target_module": + calls.append(name) + del globals["target"] + module = types.ModuleType(name) + module.VALUE = "resolved" + return module + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + resolved = target + assert resolved.VALUE == "resolved" + assert "target" not in globals() + assert calls == ["target_module"], calls + finally: + builtins.__import__ = real_import + print("OK") + """) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True + ) + self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") + self.assertIn("OK", result.stdout) + + def test_module_attr_respects_rebound_global_during_reification(self): + """Module attribute access should not overwrite a rebound lazy binding.""" + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + calls = [] + sentinel = object() + holder = types.ModuleType("holder") + holder.__dict__["__builtins__"] = builtins + + exec("lazy import target_module as target", holder.__dict__) + + def custom_import(name, globals=None, locals=None, fromlist=None, level=0): + if name == "target_module": + calls.append(name) + holder.__dict__["target"] = sentinel + module = types.ModuleType(name) + module.VALUE = "resolved" + return module + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + resolved = holder.target + assert resolved.VALUE == "resolved" + assert holder.__dict__["target"] is sentinel + assert calls == ["target_module"], calls + finally: + builtins.__import__ = real_import + print("OK") + """) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True + ) + self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") + self.assertIn("OK", result.stdout) + def test_add_lazy_to_globals(self): code = textwrap.dedent(""" import sys diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 23edd4af8df63af..5017b4a76f45509 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -10336,10 +10336,20 @@ _PyFrame_StackPointerInvalidate(frame); JUMP_TO_LABEL(error); } - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); - _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, l_v); - _PyFrame_StackPointerInvalidate(frame); + int err; + if (PyDict_CheckExact(GLOBALS())) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_ReplaceDictItemIfCurrent( + v_o, GLOBALS(), name, l_v); + _PyFrame_StackPointerInvalidate(frame); + } + else { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = PyDict_SetItem(GLOBALS(), name, l_v); + _PyFrame_StackPointerInvalidate(frame); + } if (err < 0) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index 9f0ff316d25ca5b..f4149a25aeef1de 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -184,6 +184,49 @@ _PyLazyImport_SetGlobalBindingAndDictItem(PyObject *op, PyObject *globals, return err; } +static int +lazy_import_replace_dict_item_if_current(PyObject *op, PyObject *globals, + PyObject *key, Py_hash_t key_hash, + PyObject *resolved) +{ + assert(PyLazyImport_CheckExact(op)); + assert(PyDict_CheckExact(globals)); + assert(!PyLazyImport_CheckExact(resolved)); + + PyObject *current = NULL; + int err = 0; + + Py_BEGIN_CRITICAL_SECTION(globals); + int found = _PyDict_GetItemRef_KnownHash_LockHeld( + (PyDictObject *)globals, key, key_hash, ¤t); + if (found < 0) { + err = -1; + } + else if (found && current == op) { + err = _PyDict_SetItem_KnownHash_LockHeld( + (PyDictObject *)globals, key, resolved, key_hash); + } + Py_END_CRITICAL_SECTION(); + + Py_XDECREF(current); + return err; +} + +int +_PyLazyImport_ReplaceDictItemIfCurrent(PyObject *op, PyObject *globals, + PyObject *key, PyObject *resolved) +{ + assert(PyLazyImport_CheckExact(op)); + assert(PyDict_CheckExact(globals)); + + Py_hash_t key_hash = PyObject_Hash(key); + if (key_hash == -1) { + return -1; + } + return lazy_import_replace_dict_item_if_current( + op, globals, key, key_hash, resolved); +} + int _PyLazyImport_FinishResolve(PyObject *op, PyObject *resolved) { @@ -212,25 +255,12 @@ _PyLazyImport_FinishResolve(PyObject *op, PyObject *resolved) return 0; } - PyObject *current = NULL; - if (globals != NULL) { assert(key != NULL); assert(PyDict_CheckExact(globals)); - Py_BEGIN_CRITICAL_SECTION(globals); - int found = _PyDict_GetItemRef_KnownHash_LockHeld( - (PyDictObject *)globals, key, key_hash, ¤t); - if (found < 0) { - err = -1; - } - else if (found && current == op) { - err = _PyDict_SetItem_KnownHash_LockHeld( - (PyDictObject *)globals, key, resolved, key_hash); - } - Py_END_CRITICAL_SECTION(); - - Py_XDECREF(current); + err = lazy_import_replace_dict_item_if_current( + op, globals, key, key_hash, resolved); if (err < 0) { Py_DECREF(globals); Py_DECREF(key); diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index f447403ef31b43a..aa7234f856ad526 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -1368,7 +1368,9 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) return NULL; } - if (PyDict_SetItem(m->md_dict, name, new_value) < 0) { + if (_PyLazyImport_ReplaceDictItemIfCurrent( + attr, m->md_dict, name, new_value) < 0) + { Py_CLEAR(new_value); } Py_DECREF(attr); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 9c97593e3164aa3..f2d35a679eb7eba 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2264,7 +2264,14 @@ dummy_func( Py_DECREF(v_o); ERROR_IF(true); } - int err = PyDict_SetItem(GLOBALS(), name, l_v); + int err; + if (PyDict_CheckExact(GLOBALS())) { + err = _PyLazyImport_ReplaceDictItemIfCurrent( + v_o, GLOBALS(), name, l_v); + } + else { + err = PyDict_SetItem(GLOBALS(), name, l_v); + } if (err < 0) { Py_DECREF(v_o); Py_DECREF(l_v); diff --git a/Python/ceval.c b/Python/ceval.c index 054026a78fec9e6..5f69f30b9b181b9 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3688,13 +3688,21 @@ _PyEval_LoadGlobalStackRef(PyObject *globals, PyObject *builtins, PyObject *name PyObject *res_o = PyStackRef_AsPyObjectBorrow(*writeto); if (res_o != NULL && PyLazyImport_CheckExact(res_o)) { PyObject *l_v = _PyImport_LoadLazyImportTstate(PyThreadState_GET(), res_o); - PyStackRef_CLOSE(writeto[0]); if (l_v == NULL) { assert(PyErr_Occurred()); + PyStackRef_CLOSE(writeto[0]); *writeto = PyStackRef_NULL; return; } - int err = PyDict_SetItem(globals, name, l_v); + int err; + if (PyDict_CheckExact(globals)) { + err = _PyLazyImport_ReplaceDictItemIfCurrent( + res_o, globals, name, l_v); + } + else { + err = PyDict_SetItem(globals, name, l_v); + } + PyStackRef_CLOSE(writeto[0]); if (err < 0) { Py_DECREF(l_v); *writeto = PyStackRef_NULL; diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 4d6b777747e3a75..678687e59646369 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -10177,10 +10177,20 @@ SET_CURRENT_CACHED_VALUES(0); JUMP_TO_ERROR(); } - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); - _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, l_v); - _PyFrame_StackPointerInvalidate(frame); + int err; + if (PyDict_CheckExact(GLOBALS())) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_ReplaceDictItemIfCurrent( + v_o, GLOBALS(), name, l_v); + _PyFrame_StackPointerInvalidate(frame); + } + else { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = PyDict_SetItem(GLOBALS(), name, l_v); + _PyFrame_StackPointerInvalidate(frame); + } if (err < 0) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 278a6b370e1d79c..42561fdeb4d5246 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -10334,10 +10334,20 @@ _PyFrame_StackPointerInvalidate(frame); JUMP_TO_LABEL(error); } - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); - _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, l_v); - _PyFrame_StackPointerInvalidate(frame); + int err; + if (PyDict_CheckExact(GLOBALS())) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_ReplaceDictItemIfCurrent( + v_o, GLOBALS(), name, l_v); + _PyFrame_StackPointerInvalidate(frame); + } + else { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = PyDict_SetItem(GLOBALS(), name, l_v); + _PyFrame_StackPointerInvalidate(frame); + } if (err < 0) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); From fe0bfa39c605548f2a3800b6942e608213cf258b Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 4 Jul 2026 18:30:25 +0800 Subject: [PATCH 08/14] gh-152298: Publish lazy submodules before clearing pending --- Include/internal/pycore_import.h | 2 +- Objects/moduleobject.c | 7 ++----- Python/import.c | 21 ++++++++++++++++----- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Include/internal/pycore_import.h b/Include/internal/pycore_import.h index a1078828afa572e..9317e7693e5712f 100644 --- a/Include/internal/pycore_import.h +++ b/Include/internal/pycore_import.h @@ -40,7 +40,7 @@ extern PyObject * _PyImport_GetAbsName( PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate( PyThreadState *tstate, PyObject *lazy_import); extern PyObject * _PyImport_TryLoadLazySubmodule( - PyObject *mod_name, PyObject *attr_name); + PyObject *mod_name, PyObject *attr_name, PyObject *mod_dict); extern PyObject * _PyImport_LazyImportModuleLevelObject( PyThreadState *tstate, PyObject *name, PyObject *builtins, PyObject *globals, PyObject *locals, PyObject *fromlist, int level); diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index aa7234f856ad526..b4ee8459dd3483e 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -1313,16 +1313,13 @@ try_load_lazy_submodule(PyModuleObject *m, PyObject *name) Py_DECREF(mod_name); return NULL; } - PyObject *result = _PyImport_TryLoadLazySubmodule(mod_name, name); + PyObject *result = _PyImport_TryLoadLazySubmodule( + mod_name, name, m->md_dict); Py_DECREF(mod_name); if (result == NULL) { PyErr_Clear(); return NULL; } - if (PyDict_SetItem(m->md_dict, name, result) < 0) { - Py_DECREF(result); - return NULL; - } return result; } diff --git a/Python/import.c b/Python/import.c index a5af5a3a14045f5..baf5028174ebe54 100644 --- a/Python/import.c +++ b/Python/import.c @@ -4453,7 +4453,8 @@ register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name, } PyObject * -_PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name) +_PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name, + PyObject *mod_dict) { PyInterpreterState *interp = _PyInterpreterState_GET(); PyObject *lazy_pending = LAZY_PENDING_SUBMODULES(interp); @@ -4488,15 +4489,25 @@ _PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name) } Py_DECREF(mod); + PyObject *submod = PyImport_GetModule(full_name); + Py_DECREF(full_name); + if (submod == NULL) { + Py_DECREF(pending_set); + return NULL; + } + + if (PyDict_SetItem(mod_dict, attr_name, submod) < 0) { + Py_DECREF(pending_set); + Py_DECREF(submod); + return NULL; + } + if (PySet_Discard(pending_set, attr_name) < 0) { Py_DECREF(pending_set); - Py_DECREF(full_name); + Py_DECREF(submod); return NULL; } Py_DECREF(pending_set); - - PyObject *submod = PyImport_GetModule(full_name); - Py_DECREF(full_name); return submod; } From bf48d37c8c153529da83365fcc812dce5704af8d Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 4 Jul 2026 20:12:19 +0800 Subject: [PATCH 09/14] gh-152298: Propagate lazy submodule errors --- Include/internal/pycore_import.h | 6 ++++-- Objects/moduleobject.c | 10 ++++----- Python/import.c | 36 ++++++++++++++++++++------------ 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/Include/internal/pycore_import.h b/Include/internal/pycore_import.h index 9317e7693e5712f..cec63dba2ee30e4 100644 --- a/Include/internal/pycore_import.h +++ b/Include/internal/pycore_import.h @@ -39,8 +39,10 @@ extern PyObject * _PyImport_GetAbsName( // Symbol is exported for the JIT on Windows builds. PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate( PyThreadState *tstate, PyObject *lazy_import); -extern PyObject * _PyImport_TryLoadLazySubmodule( - PyObject *mod_name, PyObject *attr_name, PyObject *mod_dict); +// Returns 1 with a new reference in result, 0 if not pending, or -1 on error. +extern int _PyImport_TryLoadLazySubmodule( + PyObject *mod_name, PyObject *attr_name, PyObject *mod_dict, + PyObject **result); extern PyObject * _PyImport_LazyImportModuleLevelObject( PyThreadState *tstate, PyObject *name, PyObject *builtins, PyObject *globals, PyObject *locals, PyObject *fromlist, int level); diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index b4ee8459dd3483e..c57d7ce19aaa549 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -1300,7 +1300,7 @@ _PyModule_IsPossiblyShadowing(PyObject *origin) } // Check if `name` is a lazily pending submodule of module `m`. -// Returns a new reference on success, or NULL with no error set. +// Returns a new reference on success, or NULL on miss or error. static PyObject * try_load_lazy_submodule(PyModuleObject *m, PyObject *name) { @@ -1313,11 +1313,11 @@ try_load_lazy_submodule(PyModuleObject *m, PyObject *name) Py_DECREF(mod_name); return NULL; } - PyObject *result = _PyImport_TryLoadLazySubmodule( - mod_name, name, m->md_dict); + PyObject *result = NULL; + int lazy_rc = _PyImport_TryLoadLazySubmodule( + mod_name, name, m->md_dict, &result); Py_DECREF(mod_name); - if (result == NULL) { - PyErr_Clear(); + if (lazy_rc <= 0) { return NULL; } return result; diff --git a/Python/import.c b/Python/import.c index baf5028174ebe54..ddf703344c07d88 100644 --- a/Python/import.c +++ b/Python/import.c @@ -4452,32 +4452,41 @@ register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name, return res; } -PyObject * +int _PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name, - PyObject *mod_dict) + PyObject *mod_dict, PyObject **result) { + *result = NULL; + PyInterpreterState *interp = _PyInterpreterState_GET(); PyObject *lazy_pending = LAZY_PENDING_SUBMODULES(interp); if (lazy_pending == NULL) { - return NULL; + return 0; } PyObject *pending_set; int rc = PyDict_GetItemRef(lazy_pending, mod_name, &pending_set); - if (rc <= 0) { - return NULL; + if (rc < 0) { + return -1; + } + if (rc == 0) { + return 0; } int contains = PySet_Contains(pending_set, attr_name); - if (contains <= 0) { + if (contains < 0) { Py_DECREF(pending_set); - return NULL; + return -1; + } + if (contains == 0) { + Py_DECREF(pending_set); + return 0; } PyObject *full_name = PyUnicode_FromFormat("%U.%U", mod_name, attr_name); if (full_name == NULL) { Py_DECREF(pending_set); - return NULL; + return -1; } PyObject *mod = PyImport_ImportModuleLevelObject( @@ -4485,7 +4494,7 @@ _PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name, if (mod == NULL) { Py_DECREF(pending_set); Py_DECREF(full_name); - return NULL; + return -1; } Py_DECREF(mod); @@ -4493,22 +4502,23 @@ _PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name, Py_DECREF(full_name); if (submod == NULL) { Py_DECREF(pending_set); - return NULL; + return -1; } if (PyDict_SetItem(mod_dict, attr_name, submod) < 0) { Py_DECREF(pending_set); Py_DECREF(submod); - return NULL; + return -1; } if (PySet_Discard(pending_set, attr_name) < 0) { Py_DECREF(pending_set); Py_DECREF(submod); - return NULL; + return -1; } Py_DECREF(pending_set); - return submod; + *result = submod; + return 1; } PyObject * From 30cbbc060aeabb399c643cfd3a81cc05d44e2222 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sun, 5 Jul 2026 00:06:51 +0800 Subject: [PATCH 10/14] gh-152298: Report missing lazy submodules --- Lib/test/test_lazy_import/__init__.py | 56 +++++++++++++++++++++++++++ Python/import.c | 11 +++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index c54c2655db29b96..7d02be7f2ec2664 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -457,6 +457,62 @@ def test_lazy_submodule_stored_in_parent_dict(self): self.assertIs(pkg.__dict__["bar"], sys.modules["test.test_lazy_import.data.pkg.bar"]) self.assertIn("BAR_MODULE_LOADED", out.getvalue()) + @support.requires_subprocess() + def test_lazy_submodule_missing_from_sys_modules_raises_key_error(self): + """A sys.modules race after import should surface the hard error.""" + code = textwrap.dedent(""" + import os + import sys + import tempfile + + class HidingModules(dict): + hide = False + hide_name = None + + def __getitem__(self, name): + if self.hide and name == self.hide_name: + raise KeyError(name) + return super().__getitem__(name) + + with tempfile.TemporaryDirectory() as tmpdir: + pkg_dir = os.path.join(tmpdir, "lazy_sysmodules_pkg") + os.mkdir(pkg_dir) + with open(os.path.join(pkg_dir, "__init__.py"), "w", encoding="utf-8") as f: + f.write("") + with open(os.path.join(pkg_dir, "sub.py"), "w", encoding="utf-8") as f: + f.write("VALUE = 42\\n") + + original_modules = sys.modules + modules = HidingModules(original_modules) + sys.modules = modules + sys.path.insert(0, tmpdir) + try: + lazy import lazy_sysmodules_pkg.sub + modules.hide_name = "lazy_sysmodules_pkg.sub" + modules.hide = True + + try: + lazy_sysmodules_pkg.sub + except KeyError as exc: + assert exc.args == ("lazy_sysmodules_pkg.sub",), exc + else: + raise AssertionError("KeyError was not raised") + finally: + sys.path.remove(tmpdir) + sys.modules = original_modules + for name in ("lazy_sysmodules_pkg", "lazy_sysmodules_pkg.sub"): + sys.modules.pop(name, None) + + print("OK") + """) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True + ) + self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") + self.assertIn("OK", result.stdout) + def test_lazy_import_pkg_cross_import(self): """Cross-imports within package should preserve lazy imports.""" import test.test_lazy_import.data.pkg.c diff --git a/Python/import.c b/Python/import.c index ddf703344c07d88..607fbee39c9f29f 100644 --- a/Python/import.c +++ b/Python/import.c @@ -4458,7 +4458,8 @@ _PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name, { *result = NULL; - PyInterpreterState *interp = _PyInterpreterState_GET(); + PyThreadState *tstate = _PyThreadState_GET(); + PyInterpreterState *interp = tstate->interp; PyObject *lazy_pending = LAZY_PENDING_SUBMODULES(interp); if (lazy_pending == NULL) { return 0; @@ -4499,11 +4500,17 @@ _PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name, Py_DECREF(mod); PyObject *submod = PyImport_GetModule(full_name); - Py_DECREF(full_name); if (submod == NULL) { + if (!_PyErr_Occurred(tstate)) { + _PyErr_Format(tstate, PyExc_KeyError, + "%R not in sys.modules as expected", + full_name); + } + Py_DECREF(full_name); Py_DECREF(pending_set); return -1; } + Py_DECREF(full_name); if (PyDict_SetItem(mod_dict, attr_name, submod) < 0) { Py_DECREF(pending_set); From 738c23abcc23ab2f49d951da80c483db3160878b Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sun, 5 Jul 2026 00:14:49 +0800 Subject: [PATCH 11/14] gh-152298: Preserve lazy getattr fallback --- Lib/test/test_lazy_import/__init__.py | 24 ++++++++++-------- Python/import.c | 36 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 7d02be7f2ec2664..d72964ace48881f 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -787,25 +787,27 @@ def test_reification_retries_on_failure(self): """ code = textwrap.dedent(""" import sys - import types lazy import test.test_lazy_import.data.broken_module # First access - should fail try: x = test.test_lazy_import.data.broken_module - except AttributeError: - pass + except ValueError as exc: + assert str(exc) == "This module always fails to import", exc + else: + raise AssertionError("ValueError was not raised") + + assert "test.test_lazy_import.data.broken_module" not in sys.modules - # The lazy object should still be a lazy proxy (not reified) - g = globals() - lazy_obj = g['test'] - # The root 'test' binding should still allow retry # Second access - should also fail (retry the import) try: x = test.test_lazy_import.data.broken_module - except AttributeError: + except ValueError as exc: + assert str(exc) == "This module always fails to import", exc print("OK - retry worked") + else: + raise AssertionError("ValueError was not raised") """) result = subprocess.run( [sys.executable, "-c", code], @@ -823,9 +825,11 @@ def test_error_during_module_execution_propagates(self): try: _ = test.test_lazy_import.data.broken_module - print("FAIL - should have raised") - except AttributeError: + except ValueError as exc: + assert str(exc) == "This module always fails to import", exc print("OK") + else: + raise AssertionError("ValueError was not raised") """) result = subprocess.run( [sys.executable, "-c", code], diff --git a/Python/import.c b/Python/import.c index 607fbee39c9f29f..0f914f917339cc1 100644 --- a/Python/import.c +++ b/Python/import.c @@ -4452,6 +4452,37 @@ register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name, return res; } +static int +is_module_not_found_for_name(PyThreadState *tstate, PyObject *name) +{ + // Return true only for "name itself was not found", consuming the current + // exception so callers can continue normal attribute fallback. + if (!_PyErr_ExceptionMatches(tstate, PyExc_ModuleNotFoundError)) { + return 0; + } + + PyObject *exc = _PyErr_GetRaisedException(tstate); + PyObject *missing_name = PyObject_GetAttr(exc, &_Py_ID(name)); + if (missing_name == NULL) { + PyErr_Clear(); + _PyErr_SetRaisedException(tstate, exc); + return 0; + } + + int is_same = PyObject_RichCompareBool(missing_name, name, Py_EQ); + Py_DECREF(missing_name); + if (is_same <= 0) { + if (is_same < 0) { + PyErr_Clear(); + } + _PyErr_SetRaisedException(tstate, exc); + return 0; + } + + Py_DECREF(exc); + return 1; +} + int _PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name, PyObject *mod_dict, PyObject **result) @@ -4493,6 +4524,11 @@ _PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name, PyObject *mod = PyImport_ImportModuleLevelObject( full_name, NULL, NULL, NULL, 0); if (mod == NULL) { + if (is_module_not_found_for_name(tstate, full_name)) { + Py_DECREF(pending_set); + Py_DECREF(full_name); + return 0; + } Py_DECREF(pending_set); Py_DECREF(full_name); return -1; From f29bc0abeaeb7d6044371e07007f21aa0d76ea68 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sun, 5 Jul 2026 00:28:10 +0800 Subject: [PATCH 12/14] gh-152298: Keep lazy race regression module-level --- Lib/test/test_lazy_import/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index d72964ace48881f..01df0e8a58dec86 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -486,8 +486,8 @@ def __getitem__(self, name): modules = HidingModules(original_modules) sys.modules = modules sys.path.insert(0, tmpdir) + lazy import lazy_sysmodules_pkg.sub try: - lazy import lazy_sysmodules_pkg.sub modules.hide_name = "lazy_sysmodules_pkg.sub" modules.hide = True From bcb1aac3346383b6b097ce0ee5a2e9cf81ba38fd Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sun, 5 Jul 2026 00:36:42 +0800 Subject: [PATCH 13/14] gh-152298: Assert lazy sys.modules race message --- Lib/test/test_lazy_import/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 01df0e8a58dec86..7b5721d2957bff7 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -494,7 +494,9 @@ def __getitem__(self, name): try: lazy_sysmodules_pkg.sub except KeyError as exc: - assert exc.args == ("lazy_sysmodules_pkg.sub",), exc + assert exc.args == ( + "'lazy_sysmodules_pkg.sub' not in sys.modules as expected", + ), exc else: raise AssertionError("KeyError was not raised") finally: From b40853152e8c46d15b10efbe922793239da26566 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sun, 5 Jul 2026 09:16:34 +0800 Subject: [PATCH 14/14] gh-152298: Rerun CI