Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Comment thread
brittanyrey marked this conversation as resolved.
Outdated
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