Skip to content

Commit 39b3d6d

Browse files
committed
Import the module a lazy import a.b as c names
`import a.b as c` compiles to `IMPORT_NAME a.b` followed by `IMPORT_FROM b`. Lazily, IMPORT_NAME leaves a placeholder holding "a.b", and IMPORT_FROM rewrote it into the placeholder `lazy from a import b` produces. Reification then imported `a` alone and read `b` off it, so the module `a.b` was never imported under its own name: an attribute of the package shadowing it answered instead, and `math.pi`, which no module backs, bound the float where the eager statement raises ModuleNotFoundError. Mark the dotted import on the placeholder and keep the whole name on it. Reification imports that name and then walks its components with IMPORT_FROM, which is what the eager statement does. The test pinning `lazy import math.pi as pi` as working is inverted, since the eager statement raises.
1 parent 0e1ae61 commit 39b3d6d

6 files changed

Lines changed: 81 additions & 34 deletions

File tree

‎Include/internal/pycore_lazyimportobject.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@ typedef struct {
2222
// Frame information for the original import location.
2323
PyCodeObject *lz_code; // Code object where the lazy import was created.
2424
int lz_instr_offset; // Instruction offset where the lazy import was created.
25+
int lz_submodule; // True for `import a.b as c`, which binds `a.b`
26+
// where `import a.b` binds `a`.
2527
} PyLazyImportObject;
2628

2729

2830
PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import);
2931
PyAPI_FUNC(PyObject *) _PyLazyImport_New(
30-
struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from, PyObject *attr);
32+
struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from,
33+
PyObject *attr, int submodule);
3134

3235
#ifdef __cplusplus
3336
}

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -724,10 +724,18 @@ def test_non_package_lazily_imported(self):
724724
assert_python_ok("-c", code)
725725

726726
def test_non_package_lazily_imported_as(self):
727-
"""Doing a dotted lazy import as still works"""
727+
"""A dotted lazy import as raises when the name is not a module."""
728+
# gh-157757: the eager statement raises ModuleNotFoundError, so the
729+
# lazy one raises it at first use rather than binding math.pi.
728730
code = textwrap.dedent("""
729731
lazy import math.pi as pi
730-
pi
732+
733+
try:
734+
pi
735+
except ModuleNotFoundError:
736+
pass
737+
else:
738+
raise AssertionError("ModuleNotFoundError was not raised")
731739
""")
732740
assert_python_ok("-c", code)
733741

@@ -2209,6 +2217,25 @@ def test_import_after_variable_wins(self):
22092217
]
22102218
self.assertIs(module_same_name_var_order2.bar, bar_mod)
22112219

2220+
def test_lazy_import_as_wins_over_variable(self):
2221+
"""A dotted lazy import as imports the submodule the variable hides."""
2222+
# gh-157757: the eager statement imports pkg.b, which rebinds pkg.b to
2223+
# the module, so the lazy one must import it too rather than read the
2224+
# variable off pkg.
2225+
code = textwrap.dedent("""
2226+
import sys
2227+
import test.test_lazy_import.data.pkg as pkg
2228+
pkg.b = "hides the b submodule"
2229+
2230+
lazy import test.test_lazy_import.data.pkg.b as b
2231+
lazy import test.test_lazy_import.data.metasyntactic.foo.bar as bar
2232+
2233+
assert b is sys.modules["test.test_lazy_import.data.pkg.b"], b
2234+
assert bar is sys.modules[
2235+
"test.test_lazy_import.data.metasyntactic.foo.bar"], bar
2236+
""")
2237+
assert_python_ok("-c", code)
2238+
22122239

22132240
class DeletedModuleReimportTests(unittest.TestCase):
22142241
"""Tests for reimporting after module deletion from sys.modules."""
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a lazy ``import a.b as c`` reading ``b`` off ``a`` instead of importing
2+
the module ``a.b``. It now binds the submodule, and raises
3+
:exc:`ModuleNotFoundError` when no module backs the name.

‎Objects/lazyimportobject.c‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
#define PyLazyImportObject_CAST(op) ((PyLazyImportObject *)(op))
1212

1313
PyObject *
14-
_PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins, PyObject *name, PyObject *fromlist)
14+
_PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins,
15+
PyObject *name, PyObject *fromlist, int submodule)
1516
{
1617
PyLazyImportObject *m;
1718
if (!name || !PyUnicode_Check(name)) {
@@ -33,6 +34,7 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins, PyObject *name
3334
m->lz_builtins = Py_XNewRef(builtins);
3435
m->lz_from = Py_NewRef(name);
3536
m->lz_attr = Py_XNewRef(fromlist);
37+
m->lz_submodule = submodule;
3638

3739
// Capture frame information for the original import location.
3840
m->lz_code = NULL;

‎Python/ceval.c‎

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3333,6 +3333,15 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje
33333333
assert(PyUnicode_Check(name));
33343334
PyObject *ret;
33353335
PyLazyImportObject *d = (PyLazyImportObject *)v;
3336+
3337+
if (d->lz_attr == NULL) {
3338+
// `import a.b as x` binds `a.b`, not an attribute of `a` (gh-157757).
3339+
if (d->lz_submodule) {
3340+
return Py_NewRef(v); // a later component of the same name
3341+
}
3342+
return _PyLazyImport_New(frame, d->lz_builtins, d->lz_from, NULL, 1);
3343+
}
3344+
33363345
PyObject *mod = PyImport_GetModule(d->lz_from);
33373346
if (mod != NULL) {
33383347
// Check if the module already has the attribute, if so, resolve it
@@ -3353,34 +3362,8 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje
33533362
Py_DECREF(mod);
33543363
}
33553364

3356-
if (d->lz_attr != NULL) {
3357-
if (PyUnicode_Check(d->lz_attr)) {
3358-
PyObject *from = PyUnicode_FromFormat(
3359-
"%U.%U", d->lz_from, d->lz_attr);
3360-
if (from == NULL) {
3361-
return NULL;
3362-
}
3363-
ret = _PyLazyImport_New(frame, d->lz_builtins, from, name);
3364-
Py_DECREF(from);
3365-
return ret;
3366-
}
3367-
}
3368-
else {
3369-
Py_ssize_t dot = PyUnicode_FindChar(
3370-
d->lz_from, '.', 0, PyUnicode_GET_LENGTH(d->lz_from), 1
3371-
);
3372-
if (dot >= 0) {
3373-
PyObject *from = PyUnicode_Substring(d->lz_from, 0, dot);
3374-
if (from == NULL) {
3375-
return NULL;
3376-
}
3377-
ret = _PyLazyImport_New(frame, d->lz_builtins, from, name);
3378-
Py_DECREF(from);
3379-
return ret;
3380-
}
3381-
}
3382-
ret = _PyLazyImport_New(frame, d->lz_builtins, d->lz_from, name);
3383-
return ret;
3365+
assert(!PyUnicode_Check(d->lz_attr)); // a fromlist, not a taken name
3366+
return _PyLazyImport_New(frame, d->lz_builtins, d->lz_from, name, 0);
33843367
}
33853368

33863369
#define CANNOT_CATCH_MSG "catching classes that do not inherit from "\

‎Python/import.c‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3898,6 +3898,27 @@ _PyImport_ResolveName(PyThreadState *tstate, PyObject *name,
38983898
return resolve_name(tstate, name, globals, level);
38993899
}
39003900

3901+
// Repeat IMPORT_FROM over the components of `name` after the first, taking
3902+
// `a.b.c` off the `a` that importing "a.b.c" returns.
3903+
static PyObject *
3904+
import_from_dotted_name(PyThreadState *tstate, PyObject *mod, PyObject *name)
3905+
{
3906+
PyObject *parts = PyUnicode_Split(name, _Py_LATIN1_CHR('.'), -1);
3907+
if (parts == NULL) {
3908+
return NULL;
3909+
}
3910+
PyObject *obj = Py_NewRef(mod);
3911+
for (Py_ssize_t i = 1; i < PyList_GET_SIZE(parts); i++) {
3912+
Py_SETREF(obj, _PyEval_ImportFrom(tstate, obj,
3913+
PyList_GET_ITEM(parts, i)));
3914+
if (obj == NULL) {
3915+
break;
3916+
}
3917+
}
3918+
Py_DECREF(parts);
3919+
return obj;
3920+
}
3921+
39013922
PyObject *
39023923
_PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
39033924
{
@@ -3989,7 +4010,15 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
39894010
goto error;
39904011
}
39914012

3992-
if (lz->lz_attr != NULL && PyUnicode_Check(lz->lz_attr)) {
4013+
if (lz->lz_submodule) {
4014+
PyObject *top = obj;
4015+
obj = import_from_dotted_name(tstate, top, lz->lz_from);
4016+
Py_DECREF(top);
4017+
if (obj == NULL) {
4018+
goto error;
4019+
}
4020+
}
4021+
else if (lz->lz_attr != NULL && PyUnicode_Check(lz->lz_attr)) {
39934022
PyObject *from = obj;
39944023
obj = _PyEval_ImportFrom(tstate, from, lz->lz_attr);
39954024
Py_DECREF(from);
@@ -4600,7 +4629,7 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
46004629
else {
46014630
Py_XINCREF(fromlist);
46024631
}
4603-
PyObject *res = _PyLazyImport_New(frame, builtins, abs_name, fromlist);
4632+
PyObject *res = _PyLazyImport_New(frame, builtins, abs_name, fromlist, 0);
46044633
if (res == NULL) {
46054634
Py_XDECREF(fromlist);
46064635
Py_DECREF(abs_name);

0 commit comments

Comments
 (0)