Skip to content

Commit a346705

Browse files
committed
gh-152298: Cover reentrant lazy owner finalization
1 parent a97cc10 commit a346705

4 files changed

Lines changed: 58 additions & 16 deletions

File tree

Doc/library/types.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -355,10 +355,10 @@ Standard names are defined for the following types:
355355
Resolve the lazy import and return the imported object. Successful
356356
resolutions are cached.
357357

358-
For module-level lazy imports, ``resolve()`` replaces the original global
359-
only while it still refers to this proxy. If that binding was deleted or
360-
rebound, it is left unchanged and later ``resolve()`` calls on the same
361-
proxy do not update it.
358+
For a module-level lazy import, ``resolve()`` also updates the original
359+
module global if that name still refers to this proxy. If the name was
360+
deleted or rebound, it is left unchanged; later ``resolve()`` calls return
361+
the cached object without updating that name.
362362

363363
.. versionadded:: 3.15
364364

Include/internal/pycore_lazyimportobject.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ typedef struct {
2121
PyObject *lz_attr;
2222
// Protected by the import lock. lz_owner_* records the first module
2323
// global that resolve() may replace. Pending owners are being stored;
24-
// finalized owners cannot be claimed again.
24+
// pending and finalized may overlap during reentrant store resolution.
2525
PyObject *lz_owner_globals;
2626
PyObject *lz_owner_key;
2727
PyObject *lz_resolved;

Lib/test/test_lazy_import/__init__.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -732,8 +732,8 @@ def custom_import(name, globals=None, locals=None, fromlist=None,
732732
return real_import(name, globals, locals, fromlist, level)
733733
734734
def check():
735-
proxy = globals()["primary"]
736-
globals()["secondary"] = proxy
735+
exec('secondary = globals()["primary"]', globals())
736+
proxy = globals()["secondary"]
737737
738738
resolved = proxy.resolve()
739739
assert globals()["primary"] is resolved
@@ -752,6 +752,54 @@ def check():
752752
""")
753753
assert_python_ok("-c", code)
754754

755+
@support.requires_subprocess()
756+
def test_reentrant_store_resolution_clears_pending_owner(self):
757+
code = textwrap.dedent("""
758+
import builtins
759+
import types
760+
761+
real_import = builtins.__import__
762+
calls = []
763+
errors = []
764+
proxies = []
765+
766+
def custom_import(name, globals=None, locals=None, fromlist=None,
767+
level=0):
768+
if name == "target_module":
769+
calls.append(name)
770+
resolved = types.ModuleType(name)
771+
resolved.VALUE = "resolved"
772+
return resolved
773+
return real_import(name, globals, locals, fromlist, level)
774+
775+
class Reenter:
776+
def __del__(self):
777+
try:
778+
proxies.append(globals()["target"])
779+
assert target.VALUE == "resolved"
780+
except BaseException as exc:
781+
errors.append((type(exc).__name__, str(exc)))
782+
783+
builtins.__import__ = custom_import
784+
target = Reenter()
785+
lazy import target_module as target
786+
787+
try:
788+
assert not errors, errors
789+
assert len(proxies) == 1, proxies
790+
resolved = globals()["target"]
791+
proxy = proxies[0]
792+
assert resolved.VALUE == "resolved"
793+
794+
globals()["target"] = proxy
795+
assert proxy.resolve() is resolved
796+
assert globals()["target"] is proxy
797+
assert len(calls) == 1, calls
798+
finally:
799+
builtins.__import__ = real_import
800+
""")
801+
assert_python_ok("-c", code)
802+
755803

756804
class SyntaxRestrictionTests(LazyImportTestCase):
757805
"""Tests for syntax restrictions on lazy imports."""

Objects/lazyimportobject.c

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,11 +201,8 @@ _PyLazyImport_FinishGlobalBinding(PyThreadState *tstate, PyObject *op,
201201
PyInterpreterState *interp = tstate->interp;
202202
_PyImport_AcquireLock(interp);
203203
if (m->lz_owner_pending && owner_matches(m, globals, name)) {
204-
if (stored) {
205-
m->lz_owner_pending = 0;
206-
}
207-
else {
208-
m->lz_owner_pending = 0;
204+
m->lz_owner_pending = 0;
205+
if (!stored) {
209206
Py_CLEAR(m->lz_owner_globals);
210207
Py_CLEAR(m->lz_owner_key);
211208
}
@@ -220,10 +217,7 @@ finalize_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m,
220217
int finalized = 0;
221218
PyInterpreterState *interp = tstate->interp;
222219
_PyImport_AcquireLock(interp);
223-
if (!m->lz_owner_pending &&
224-
!m->lz_owner_finalized &&
225-
owner_matches(m, globals, name))
226-
{
220+
if (!m->lz_owner_finalized && owner_matches(m, globals, name)) {
227221
m->lz_owner_finalized = 1;
228222
finalized = 1;
229223
}

0 commit comments

Comments
 (0)