Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions Lib/test/test_lazy_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2807,5 +2807,60 @@ def test_fails_without_lazy(self):
self.assertIn("ImportError", result.stderr)


@support.requires_subprocess()
class LazyImportFinalizationTests(unittest.TestCase):
"""Destructors that use lazy imports after finalization freed their state."""

def test_declare(self):
code = textwrap.dedent("""
import builtins, os

class Canary:
def __del__(self, write=os.write, exec=exec,
builtins={"__lazy_import__": builtins.__lazy_import__}):
exec("lazy import a.b", {"__builtins__": builtins})
write(1, b"ok")

# Only this placeholder keeps the canary alive.
exec("lazy import pkg.mod", {"__builtins__": {
"__lazy_import__": builtins.__lazy_import__, "c": Canary()}})
""")
self.assertEqual(assert_python_ok("-c", code).out, b"ok")

def test_resolve(self):
code = textwrap.dedent("""
import contextvars, json, os, types

def safe_import(name, *args, allowed={"json": json}):
return allowed[name]

# Not in sys.modules, with builtins that still work at shutdown.
ns = types.ModuleType("ns")
ns.json = __lazy_import__("json", {"__builtins__": {"__import__": safe_import}})

class Canary:
def __del__(self, write=os.write, type=type, ns=ns):
write(1, type(ns.json).__name__.encode())

contextvars.ContextVar("v").set(Canary())
""")
self.assertEqual(assert_python_ok("-c", code).out, b"module")

def test_set_lazy_attributes(self):
code = textwrap.dedent("""
import _imp, os, sys

class Canary:
def __del__(self, write=os.write,
set_lazy=_imp._set_lazy_attributes):
set_lazy(None, "mod")
write(1, b"ok")

# Freed while sys.lazy_modules is being cleared.
sys.lazy_modules.add(Canary())
""")
self.assertEqual(assert_python_ok("-c", code).out, b"ok")


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix a crash when a :ref:`lazy import <lazy-imports>` is declared or resolved
by a finalizer running during interpreter shutdown.
15 changes: 12 additions & 3 deletions Python/import.c
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ _PyImport_ClearLazyModules(PyInterpreterState *interp)
int
_PyImport_DiscardLazyModule(PyInterpreterState *interp, PyObject *name)
{
if (LAZY_MODULES(interp) == NULL) {
return 0;
}
return PySet_Discard(LAZY_MODULES(interp), name);
}

Expand Down Expand Up @@ -4179,7 +4182,10 @@ lazy_modules_add(PyThreadState *tstate, PyObject *name,
else {
Py_XDECREF(mod);
}
return loaded ? 0 : PySet_Add(LAZY_MODULES(tstate->interp), name);
if (loaded || LAZY_MODULES(tstate->interp) == NULL) {
return 0;
}
return PySet_Add(LAZY_MODULES(tstate->interp), name);
}

// Ensure a dict of pending submodule names exists for the parent.
Expand Down Expand Up @@ -4213,7 +4219,10 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name, PyObject *source)
{
PyDictObject *pending =
(PyDictObject *)LAZY_PENDING_SUBMODULES(tstate->interp);
assert(pending != NULL);
// Finalizers can still run after finalize_modules() cleared the dict.
if (pending == NULL) {
Comment thread
brittanyrey marked this conversation as resolved.
return 0;
}
Py_ssize_t end = PyUnicode_GET_LENGTH(name);
while (true) {
Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0, end, -1);
Expand Down Expand Up @@ -5575,7 +5584,7 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
/*[clinic end generated code: output=3369bb3242b1f043 input=900339e013ab2b82]*/
{
PyInterpreterState *interp = _PyInterpreterState_GET();
if (PySet_Discard(LAZY_MODULES(interp), name) < 0) {
if (_PyImport_DiscardLazyModule(interp, name) < 0) {
Comment thread
brittanyrey marked this conversation as resolved.
return NULL;
}
if (_PyImport_ClearLazySubmodule(_PyThreadState_GET(), name, 0) < 0) {
Expand Down
Loading