Skip to content

Commit 8341ca0

Browse files
committed
Chain lazy IMPORT_FROM placeholders instead of flagging dotted imports
Each deferred IMPORT_FROM off a placeholder without a fromlist now keeps the previous placeholder in lz_from and the attribute name in lz_attr. Reification walks back to the placeholder IMPORT_NAME left, runs that import, and replays the lookups in order with _PyEval_ImportFrom, which is what the eager bytecode does. This drops the lz_dotted_as flag and also follows a custom __lazy_import__ that returns a placeholder for a different module name.
1 parent a8ebc87 commit 8341ca0

5 files changed

Lines changed: 113 additions & 55 deletions

File tree

‎Include/internal/pycore_lazyimportobject.h‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ 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_dotted_as; // `import a.b as c`, which binds `a.b` not `a`.
2625
} PyLazyImportObject;
2726

2827

2928
PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import);
3029
PyAPI_FUNC(PyObject *) _PyLazyImport_New(
31-
struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from,
32-
PyObject *attr, int dotted_as);
30+
struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from, PyObject *attr);
3331

3432
#ifdef __cplusplus
3533
}

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2234,6 +2234,36 @@ def test_lazy_import_as_wins_over_variable(self):
22342234
""")
22352235
assert_python_ok("-c", code)
22362236

2237+
def test_dotted_as_of_loaded_module(self):
2238+
"""A dotted lazy import as binds the module, not a same-named attribute."""
2239+
# importlib.metadata is already loaded and has a `metadata` attribute.
2240+
code = textwrap.dedent("""
2241+
import importlib.metadata
2242+
import importlib.metadata as eager
2243+
2244+
lazy import importlib.metadata as lazily
2245+
2246+
assert lazily is eager, lazily
2247+
""")
2248+
assert_python_ok("-c", code)
2249+
2250+
def test_dotted_as_replays_lookups_on_custom_placeholder(self):
2251+
"""A dotted lazy import as looks up its names on what the hook returned."""
2252+
code = textwrap.dedent("""
2253+
import builtins
2254+
import xml.dom
2255+
2256+
# In a list, so the hook reading it does not resolve it.
2257+
placeholder = [__lazy_import__("xml")]
2258+
default = builtins.__lazy_import__
2259+
builtins.__lazy_import__ = lambda *args: placeholder[0]
2260+
lazy import fake.dom as dom
2261+
builtins.__lazy_import__ = default
2262+
2263+
assert dom is xml.dom, dom
2264+
""")
2265+
assert_python_ok("-c", code)
2266+
22372267

22382268
class DeletedModuleReimportTests(unittest.TestCase):
22392269
"""Tests for reimporting after module deletion from sys.modules."""

‎Objects/lazyimportobject.c‎

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111
#define PyLazyImportObject_CAST(op) ((PyLazyImportObject *)(op))
1212

1313
PyObject *
14-
_PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins,
15-
PyObject *name, PyObject *fromlist, int dotted_as)
14+
_PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins, PyObject *name, PyObject *fromlist)
1615
{
1716
PyLazyImportObject *m;
18-
if (!name || !PyUnicode_Check(name)) {
19-
PyErr_SetString(PyExc_TypeError, "expected str for name");
17+
if (!name || !(PyUnicode_Check(name) || PyLazyImport_CheckExact(name))) {
18+
PyErr_SetString(PyExc_TypeError, "expected str or lazy_import for name");
2019
return NULL;
2120
}
2221
if (fromlist == Py_None || fromlist == NULL) {
@@ -34,7 +33,6 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins,
3433
m->lz_builtins = Py_XNewRef(builtins);
3534
m->lz_from = Py_NewRef(name);
3635
m->lz_attr = Py_XNewRef(fromlist);
37-
m->lz_dotted_as = dotted_as;
3836

3937
// Capture frame information for the original import location.
4038
m->lz_code = NULL;
@@ -106,9 +104,43 @@ lazy_import_getattro(PyObject *op, PyObject *name)
106104
return value;
107105
}
108106

107+
static PyObject *lazy_import_name(PyLazyImportObject *m);
108+
109+
// The dotted name of the object that resolving the placeholder returns.
110+
static PyObject *
111+
lazy_import_path(PyLazyImportObject *m)
112+
{
113+
if (PyLazyImport_CheckExact(m->lz_from)) {
114+
PyObject *base = lazy_import_path((PyLazyImportObject *)m->lz_from);
115+
if (base == NULL) {
116+
return NULL;
117+
}
118+
PyObject *res = PyUnicode_FromFormat("%U.%U", base, m->lz_attr);
119+
Py_DECREF(base);
120+
return res;
121+
}
122+
if (m->lz_attr != NULL) {
123+
return lazy_import_name(m);
124+
}
125+
// __import__("a.b") returns the top-level package `a`.
126+
Py_ssize_t dot = PyUnicode_FindChar(
127+
m->lz_from, '.', 0, PyUnicode_GET_LENGTH(m->lz_from), 1
128+
);
129+
if (dot == -2) {
130+
return NULL;
131+
}
132+
if (dot < 0) {
133+
return Py_NewRef(m->lz_from);
134+
}
135+
return PyUnicode_Substring(m->lz_from, 0, dot);
136+
}
137+
109138
static PyObject *
110139
lazy_import_name(PyLazyImportObject *m)
111140
{
141+
if (PyLazyImport_CheckExact(m->lz_from)) {
142+
return lazy_import_path(m);
143+
}
112144
if (m->lz_attr != NULL) {
113145
if (PyUnicode_Check(m->lz_attr)) {
114146
return PyUnicode_FromFormat("%U.%U", m->lz_from, m->lz_attr);

‎Python/ceval.c‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3334,12 +3334,10 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje
33343334
PyObject *ret;
33353335
PyLazyImportObject *d = (PyLazyImportObject *)v;
33363336

3337-
if (d->lz_attr == NULL) {
3338-
// `import a.b as x` binds the module `a.b`, not an attribute of `a`.
3339-
if (d->lz_dotted_as) {
3340-
return Py_NewRef(v);
3341-
}
3342-
return _PyLazyImport_New(frame, d->lz_builtins, d->lz_from, NULL, 1);
3337+
if (d->lz_attr == NULL || PyUnicode_Check(d->lz_attr)) {
3338+
// `import a.b.c as d`: record the lookup, to replay once the
3339+
// import has run.
3340+
return _PyLazyImport_New(frame, d->lz_builtins, v, name);
33433341
}
33443342

33453343
PyObject *mod = PyImport_GetModule(d->lz_from);
@@ -3362,8 +3360,7 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje
33623360
Py_DECREF(mod);
33633361
}
33643362

3365-
assert(!PyUnicode_Check(d->lz_attr)); // a fromlist
3366-
return _PyLazyImport_New(frame, d->lz_builtins, d->lz_from, name, 0);
3363+
return _PyLazyImport_New(frame, d->lz_builtins, d->lz_from, name);
33673364
}
33683365

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

‎Python/import.c‎

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

3901-
// Take `a.b.c` off the `a` that importing "a.b.c" returns, as IMPORT_FROM does.
3901+
// Look up, in order, the attributes recorded from the root placeholder to lz
3902+
// on the module the root's import returned.
39023903
static PyObject *
3903-
import_from_dotted_name(PyThreadState *tstate, PyObject *mod, PyObject *name)
3904-
{
3905-
PyObject *parts = PyUnicode_Split(name, _Py_LATIN1_CHR('.'), -1);
3906-
if (parts == NULL) {
3907-
return NULL;
3908-
}
3909-
PyObject *obj = Py_NewRef(mod);
3910-
for (Py_ssize_t i = 1; i < PyList_GET_SIZE(parts); i++) {
3911-
Py_SETREF(obj, _PyEval_ImportFrom(tstate, obj,
3912-
PyList_GET_ITEM(parts, i)));
3913-
if (obj == NULL) {
3914-
break;
3904+
lazy_import_replay_from(PyThreadState *tstate, PyObject *mod,
3905+
PyLazyImportObject *lz)
3906+
{
3907+
PyObject *from;
3908+
if (PyLazyImport_CheckExact(lz->lz_from)) {
3909+
from = lazy_import_replay_from(
3910+
tstate, mod, (PyLazyImportObject *)lz->lz_from);
3911+
if (from == NULL) {
3912+
return NULL;
39153913
}
39163914
}
3917-
Py_DECREF(parts);
3915+
else if (lz->lz_attr != NULL && PyUnicode_Check(lz->lz_attr)) {
3916+
from = Py_NewRef(mod);
3917+
}
3918+
else {
3919+
return Py_NewRef(mod);
3920+
}
3921+
PyObject *obj = _PyEval_ImportFrom(tstate, from, lz->lz_attr);
3922+
Py_DECREF(from);
39183923
return obj;
39193924
}
39203925

@@ -3930,6 +3935,12 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
39303935
PyLazyImportObject *lz = (PyLazyImportObject *)lazy_import;
39313936
PyInterpreterState *interp = tstate->interp;
39323937

3938+
// Walk back to the placeholder IMPORT_NAME left.
3939+
PyLazyImportObject *root = lz;
3940+
while (PyLazyImport_CheckExact(root->lz_from)) {
3941+
root = (PyLazyImportObject *)root->lz_from;
3942+
}
3943+
39333944
// Acquire the global import lock to serialize reification
39343945
_PyImport_AcquireLock(interp);
39353946

@@ -3966,7 +3977,7 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
39663977
return NULL;
39673978
}
39683979
PyErr_SetImportErrorSubclass(PyExc_ImportCycleError, errmsg,
3969-
lz->lz_from, NULL);
3980+
root->lz_from, NULL);
39703981
Py_DECREF(errmsg);
39713982
Py_DECREF(name);
39723983
_PyImport_ReleaseLock(interp);
@@ -3976,24 +3987,24 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
39763987
goto error;
39773988
}
39783989

3979-
if (lz->lz_attr != NULL) {
3980-
if (PyUnicode_Check(lz->lz_attr)) {
3990+
if (root->lz_attr != NULL) {
3991+
if (PyUnicode_Check(root->lz_attr)) {
39813992
fromlist = PyTuple_New(1);
39823993
if (fromlist == NULL) {
39833994
goto error;
39843995
}
3985-
Py_INCREF(lz->lz_attr);
3986-
PyTuple_SET_ITEM(fromlist, 0, lz->lz_attr);
3996+
Py_INCREF(root->lz_attr);
3997+
PyTuple_SET_ITEM(fromlist, 0, root->lz_attr);
39873998
}
39883999
else {
3989-
Py_INCREF(lz->lz_attr);
3990-
fromlist = lz->lz_attr;
4000+
Py_INCREF(root->lz_attr);
4001+
fromlist = root->lz_attr;
39914002
}
39924003
}
39934004

39944005
PyObject *globals = PyEval_GetGlobals();
39954006

3996-
if (PyMapping_GetOptionalItem(lz->lz_builtins, &_Py_ID(__import__),
4007+
if (PyMapping_GetOptionalItem(root->lz_builtins, &_Py_ID(__import__),
39974008
&import_func) < 0) {
39984009
goto error;
39994010
}
@@ -4003,27 +4014,17 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
40034014
}
40044015
obj = _PyEval_ImportNameWithImport(
40054016
tstate, import_func, globals, globals,
4006-
lz->lz_from, fromlist, _PyLong_GetZero()
4017+
root->lz_from, fromlist, _PyLong_GetZero()
40074018
);
40084019
if (obj == NULL) {
40094020
goto error;
40104021
}
40114022

4012-
if (lz->lz_dotted_as) {
4013-
PyObject *top = obj;
4014-
obj = import_from_dotted_name(tstate, top, lz->lz_from);
4015-
Py_DECREF(top);
4016-
if (obj == NULL) {
4017-
goto error;
4018-
}
4019-
}
4020-
else if (lz->lz_attr != NULL && PyUnicode_Check(lz->lz_attr)) {
4021-
PyObject *from = obj;
4022-
obj = _PyEval_ImportFrom(tstate, from, lz->lz_attr);
4023-
Py_DECREF(from);
4024-
if (obj == NULL) {
4025-
goto error;
4026-
}
4023+
PyObject *from = obj;
4024+
obj = lazy_import_replay_from(tstate, from, lz);
4025+
Py_DECREF(from);
4026+
if (obj == NULL) {
4027+
goto error;
40274028
}
40284029

40294030
assert(!PyLazyImport_CheckExact(obj));
@@ -4628,7 +4629,7 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
46284629
else {
46294630
Py_XINCREF(fromlist);
46304631
}
4631-
PyObject *res = _PyLazyImport_New(frame, builtins, abs_name, fromlist, 0);
4632+
PyObject *res = _PyLazyImport_New(frame, builtins, abs_name, fromlist);
46324633
if (res == NULL) {
46334634
Py_XDECREF(fromlist);
46344635
Py_DECREF(abs_name);

0 commit comments

Comments
 (0)