Skip to content
Merged
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
151 changes: 149 additions & 2 deletions Lib/test/test_lazy_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,10 +732,17 @@ def test_non_package_lazily_imported(self):
assert_python_ok("-c", code)

def test_non_package_lazily_imported_as(self):
"""Doing a dotted lazy import as still works"""
"""A dotted lazy import as raises when the name is not a module."""
# gh-157757: the eager statement raises, so the lazy one raises too.
code = textwrap.dedent("""
lazy import math.pi as pi
pi

try:
pi
except ModuleNotFoundError:
pass
else:
raise AssertionError("ModuleNotFoundError was not raised")
""")
assert_python_ok("-c", code)

Expand Down Expand Up @@ -1178,6 +1185,22 @@ def test_accessing_one_name_leaves_others_as_proxies(self):
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
self.assertIn("OK", result.stdout)

def test_accessing_one_name_imports_only_its_submodule(self):
"""Accessing one name should not import the other names' submodules."""
code = textwrap.dedent("""
import sys

lazy from test.test_lazy_import.data.pkg import b, bar, broken

# Importing bar prints, and importing broken raises.
b.foo()

assert "test.test_lazy_import.data.pkg.bar" not in sys.modules
assert "test.test_lazy_import.data.pkg.broken" not in sys.modules
""")
rc, out, err = assert_python_ok("-c", code)
self.assertEqual(out, b"")

def test_all_names_reified_after_all_accessed(self):
"""All names should be reified after each is accessed."""
code = textwrap.dedent("""
Expand Down Expand Up @@ -2144,5 +2167,129 @@ def test_dunder_lazy_import_without_frame(self):
)


class DottedLazyImportTests(unittest.TestCase):

def test_lazy_import_as_wins_over_variable(self):
"""A dotted lazy import as imports the submodule the variable hides."""
# gh-157757: importing pkg.b rebinds pkg.b from the variable to the
# module, eagerly and lazily alike.
code = textwrap.dedent("""
import sys
import test.test_lazy_import.data.pkg as pkg
pkg.b = "hides the b submodule"

lazy import test.test_lazy_import.data.pkg.b as b
lazy import xml.dom.minidom as minidom

assert b is sys.modules["test.test_lazy_import.data.pkg.b"], b
assert minidom is sys.modules["xml.dom.minidom"], minidom
""")
assert_python_ok("-c", code)


def test_dotted_as_of_loaded_module(self):
"""A dotted lazy import as binds the module, not a same-named attribute."""
# importlib.metadata is already loaded and has a `metadata` attribute.
code = textwrap.dedent("""
import importlib.metadata
import importlib.metadata as eager

lazy import importlib.metadata as lazily

assert lazily is eager, lazily
""")
assert_python_ok("-c", code)


def test_dotted_as_replays_lookups_on_custom_placeholder(self):
"""A dotted lazy import as looks up its names on what the hook returned."""
code = textwrap.dedent("""
import builtins
import xml.dom

# In a list, so the hook reading it does not resolve it.
placeholder = [__lazy_import__("xml")]
default = builtins.__lazy_import__
builtins.__lazy_import__ = lambda *args: placeholder[0]
lazy import fake.dom as dom
builtins.__lazy_import__ = default

assert dom is xml.dom, dom
""")
assert_python_ok("-c", code)


def test_empty_fromlist_placeholder_matches_no_fromlist(self):
"""An empty fromlist behaves like None."""
code = textwrap.dedent("""
expected = "<lazy_import 'xml.dom'>"
# In lists, so reading them does not resolve them.
for fromlist in (None, ()):
same = [__lazy_import__("xml.dom", fromlist=fromlist)]
assert repr(same[0]) == expected, (fromlist, repr(same[0]))
bare = [__lazy_import__("xml.dom")]
assert repr(bare[0]) == expected, repr(bare[0])
""")
assert_python_ok("-c", code)


def test_empty_fromlist_preserved_for_custom_import(self):
code = textwrap.dedent("""
import builtins
import types

value = object()
module = types.SimpleNamespace(dom=value)
placeholder = [__lazy_import__("xml.dom", fromlist=())]
default_import = builtins.__import__
default_lazy_import = builtins.__lazy_import__
calls = []

def import_hook(name, globals, locals, fromlist, level):
assert name == "xml.dom", name
assert fromlist == (), fromlist
calls.append(fromlist)
return module

builtins.__import__ = import_hook
assert placeholder[0].resolve() is module
builtins.__lazy_import__ = lambda *args: placeholder[0]
lazy import fake.dom as dom
assert dom is value
builtins.__import__ = default_import
builtins.__lazy_import__ = default_lazy_import

assert calls == [(), ()], calls
""")
assert_python_ok("-c", code)


def test_dotted_as_replays_lookups_on_dotted_placeholder(self):
"""A dotted lazy import as replays its names on the hook's package."""
# importlib.metadata has a `metadata` attribute of its own, which the
# placeholder for importlib must not answer with.
for target in ("xml.dom", "importlib.metadata"):
with self.subTest(target=target):
leaf = target.rpartition(".")[2]
code = textwrap.dedent(f"""
import builtins
import sys
import {target}

# In a list, so the hook reading it does not resolve it.
placeholder = [__lazy_import__("{target}", fromlist=())]
default = builtins.__lazy_import__
builtins.__lazy_import__ = lambda *args: placeholder[0]
lazy import fake.{leaf} as {leaf}
builtins.__lazy_import__ = default

name = repr(globals()["{leaf}"])
assert name == "<lazy_import '{target}'>", name
assert {leaf} is sys.modules["{target}"], {leaf}
""")
assert_python_ok("-c", code)



if __name__ == '__main__':
unittest.main()
2 changes: 2 additions & 0 deletions Lib/test/test_lazy_import/data/pkg/broken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Submodule that raises an error during import
raise ValueError("This module always fails to import")
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix a lazy ``import a.b as c`` reading ``b`` off ``a`` instead of importing
the module ``a.b``. It now binds the submodule, and raises
:exc:`ModuleNotFoundError` when no module backs the name.
47 changes: 38 additions & 9 deletions Objects/lazyimportobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ PyObject *
_PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins, PyObject *name, PyObject *fromlist)
{
PyLazyImportObject *m;
if (!name || !PyUnicode_Check(name)) {
PyErr_SetString(PyExc_TypeError, "expected str for name");
if (!name || !(PyUnicode_Check(name) || PyLazyImport_CheckExact(name))) {
PyErr_SetString(PyExc_TypeError, "expected str or lazy_import for name");
return NULL;
}
if (fromlist == Py_None || fromlist == NULL) {
Expand Down Expand Up @@ -104,16 +104,45 @@ lazy_import_getattro(PyObject *op, PyObject *name)
return value;
}

// The dotted name of the object that resolving the placeholder returns.
static PyObject *
lazy_import_name(PyLazyImportObject *m)
lazy_import_path(PyLazyImportObject *m)
{
if (m->lz_attr != NULL) {
if (PyUnicode_Check(m->lz_attr)) {
return PyUnicode_FromFormat("%U.%U", m->lz_from, m->lz_attr);
}
else {
return PyUnicode_FromFormat("%U...", m->lz_from);
if (PyLazyImport_CheckExact(m->lz_from)) {
PyObject *base = lazy_import_path((PyLazyImportObject *)m->lz_from);
if (base == NULL) {
return NULL;
}
PyObject *res = PyUnicode_FromFormat("%U.%U", base, m->lz_attr);
Py_DECREF(base);
return res;
}
if (m->lz_attr != NULL &&
(!PyTuple_Check(m->lz_attr) || PyTuple_GET_SIZE(m->lz_attr) > 0)) {
return Py_NewRef(m->lz_from);
}
// __import__("a.b") returns the top-level package `a`.
Py_ssize_t dot = PyUnicode_FindChar(
m->lz_from, '.', 0, PyUnicode_GET_LENGTH(m->lz_from), 1
);
if (dot == -2) {
return NULL;
}
if (dot < 0) {
return Py_NewRef(m->lz_from);
}
return PyUnicode_Substring(m->lz_from, 0, dot);
}

static PyObject *
lazy_import_name(PyLazyImportObject *m)
{
if (PyLazyImport_CheckExact(m->lz_from)) {
return lazy_import_path(m);
}
if (m->lz_attr != NULL &&
(!PyTuple_Check(m->lz_attr) || PyTuple_GET_SIZE(m->lz_attr) > 0)) {
return PyUnicode_FromFormat("%U...", m->lz_from);
}
return Py_NewRef(m->lz_from);
}
Expand Down
37 changes: 8 additions & 29 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -3310,7 +3310,13 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje
assert(PyUnicode_Check(name));
PyObject *ret;
PyLazyImportObject *d = (PyLazyImportObject *)v;
PyObject *mod = PyImport_GetModule(d->lz_from);
PyObject *mod = NULL;
// Only `from a import b` can take b off an already imported a;
// `import a.b as c` has to import a.b first.
if (d->lz_attr != NULL && PyTuple_Check(d->lz_attr) &&
PyTuple_GET_SIZE(d->lz_attr) > 0) {
mod = PyImport_GetModule(d->lz_from);
}
if (mod != NULL) {
// Check if the module already has the attribute, if so, resolve it
// eagerly.
Expand All @@ -3330,34 +3336,7 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje
Py_DECREF(mod);
}

if (d->lz_attr != NULL) {
if (PyUnicode_Check(d->lz_attr)) {
PyObject *from = PyUnicode_FromFormat(
"%U.%U", d->lz_from, d->lz_attr);
if (from == NULL) {
return NULL;
}
ret = _PyLazyImport_New(frame, d->lz_builtins, from, name);
Py_DECREF(from);
return ret;
}
}
else {
Py_ssize_t dot = PyUnicode_FindChar(
d->lz_from, '.', 0, PyUnicode_GET_LENGTH(d->lz_from), 1
);
if (dot >= 0) {
PyObject *from = PyUnicode_Substring(d->lz_from, 0, dot);
if (from == NULL) {
return NULL;
}
ret = _PyLazyImport_New(frame, d->lz_builtins, from, name);
Py_DECREF(from);
return ret;
}
}
ret = _PyLazyImport_New(frame, d->lz_builtins, d->lz_from, name);
return ret;
return _PyLazyImport_New(frame, d->lz_builtins, v, name);
}

#define CANNOT_CATCH_MSG "catching classes that do not inherit from "\
Expand Down
Loading
Loading