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
6 changes: 6 additions & 0 deletions Doc/library/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@ Standard names are defined for the following types:

Reify the lazy import and return the "real" object being imported.

A lazy import is reified at most once. Repeated calls, and any later
access to a name still bound to the proxy, return the object from the
first reification, even if the module has since been removed from
:data:`sys.modules`. A reification that raises is not remembered, so
the next access retries the import.


.. class:: GetSetDescriptorType

Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_lazyimportobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ typedef struct {
PyObject *lz_builtins;
PyObject *lz_from;
PyObject *lz_attr;
PyObject *lz_resolved; // Result of the first reification, or NULL.
// 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.
Expand Down
92 changes: 92 additions & 0 deletions Lib/test/test_lazy_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,98 @@ def test_lazy_import_type_attribute_error_message(self):
""")
assert_python_ok("-c", code)

# Setup for the two tests below. A repeat import is visible both in
# ``calls`` and as a different module object. They read the proxy out of
# the module dict because a plain global load would reify it first.
_COUNTING_IMPORT = """
import builtins
import types

real_import = builtins.__import__
calls = []
fail_import = False

lazy import target_module as target

def counting_import(name, *args, **kwargs):
if name != "target_module":
return real_import(name, *args, **kwargs)
calls.append(name)
if fail_import:
raise ImportError("no target_module for you")
return types.ModuleType(name)

builtins.__import__ = counting_import
"""

def _assert_counting_import_ok(self, body):
"""Run *body* in a subprocess with the counting __import__ installed."""
code = textwrap.dedent(self._COUNTING_IMPORT) + textwrap.dedent(body)
return assert_python_ok("-c", code)

@support.requires_subprocess()
def test_proxy_reifies_once_whichever_path_reaches_it(self):
"""Every reification of one proxy yields the first imported object."""
self._assert_counting_import_ok("""
def main():
proxy = globals()["target"]
resolved = proxy.resolve()
assert proxy.resolve() is resolved
# The global is still bound to the proxy, so loading it reifies.
assert target is resolved, (target, resolved)
# So does loading a copy of the proxy from another namespace.
namespace = {"__builtins__": builtins, "alias": proxy}
exec("alias", namespace)
assert namespace["alias"] is resolved
assert calls == ["target_module"], calls

main()
""")

@support.requires_subprocess()
def test_failed_resolve_is_not_cached(self):
"""A failed reification is retried rather than remembered."""
self._assert_counting_import_ok("""
def main():
global fail_import
fail_import = True
proxy = globals()["target"]
try:
proxy.resolve()
except ImportError:
pass
else:
assert False, 'ImportError is not raised'
fail_import = False
resolved = proxy.resolve()
assert proxy.resolve() is resolved
assert calls == ["target_module"] * 2, calls

main()
""")

@support.requires_subprocess()
def test_from_import_proxy_remembers_the_attribute(self):
"""A `lazy from` proxy binds the attribute, not the module."""
code = textwrap.dedent("""
import sys

lazy from test.test_lazy_import.data.basic2 import f

def main():
proxy = globals()["f"]
resolved = proxy.resolve()
module = sys.modules["test.test_lazy_import.data.basic2"]
assert resolved is module.f, (resolved, module.f)
# Rebinding on the source module does not retarget the proxy,
# just as it does not for an eager ``from ... import``.
module.f = lambda: None
assert proxy.resolve() is resolved

main()
""")
assert_python_ok("-c", code)


class SyntaxRestrictionTests(LazyImportTestCase):
"""Tests for syntax restrictions on lazy imports."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Reify a lazy import at most once. Previously
:meth:`!types.LazyImportType.resolve` followed by a normal access to the
still-lazy global imported twice and bound two different objects.
3 changes: 3 additions & 0 deletions Objects/lazyimportobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ _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_resolved = NULL;

// Capture frame information for the original import location.
m->lz_code = NULL;
Expand All @@ -58,6 +59,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_resolved);
Py_VISIT(m->lz_code);
return 0;
}
Expand All @@ -69,6 +71,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_resolved);
Py_CLEAR(m->lz_code);
return 0;
}
Expand Down
11 changes: 11 additions & 0 deletions Python/import.c
Original file line number Diff line number Diff line change
Expand Up @@ -3913,6 +3913,13 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
// Acquire the global import lock to serialize reification
_PyImport_AcquireLock(interp);

// Reify only once. lz_resolved is read and written under the import lock.
if (lz->lz_resolved != NULL) {
PyObject *resolved = Py_NewRef(lz->lz_resolved);
_PyImport_ReleaseLock(interp);
return resolved;
}

// 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.
Expand Down Expand Up @@ -4000,6 +4007,10 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)

assert(!PyLazyImport_CheckExact(obj));

// Reentrancy on the same proxy is rejected above as a cycle.
assert(lz->lz_resolved == NULL);
Py_XSETREF(lz->lz_resolved, Py_NewRef(obj));

goto ok;

error:
Expand Down
Loading