Skip to content

Commit c10339c

Browse files
committed
Treat an empty fromlist on a lazy import placeholder as no fromlist
__import__("a.b", fromlist=()) returns the top-level package `a`, the same as fromlist=None, but the placeholder kept the empty tuple. Every consumer of lz_attr then read it as a real fromlist: reification narrowed it to the chained attribute and replayed the lookups on `a.b`, _PyEval_LazyImportFrom took the attribute off sys.modules["a.b"], and the repr named `a.b.attr`. _PyLazyImport_New already collapses None to NULL for exactly this reason, so collapse an empty tuple there too and every site follows.
1 parent f977cd3 commit c10339c

2 files changed

Lines changed: 42 additions & 0 deletions

File tree

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2280,6 +2280,44 @@ def test_dotted_as_replays_lookups_on_custom_placeholder(self):
22802280
""")
22812281
assert_python_ok("-c", code)
22822282

2283+
def test_empty_fromlist_placeholder_matches_no_fromlist(self):
2284+
"""An empty fromlist behaves like None."""
2285+
code = textwrap.dedent("""
2286+
expected = "<lazy_import 'xml.dom'>"
2287+
# In lists, so reading them does not resolve them.
2288+
for fromlist in (None, ()):
2289+
same = [__lazy_import__("xml.dom", fromlist=fromlist)]
2290+
assert repr(same[0]) == expected, (fromlist, repr(same[0]))
2291+
bare = [__lazy_import__("xml.dom")]
2292+
assert repr(bare[0]) == expected, repr(bare[0])
2293+
""")
2294+
assert_python_ok("-c", code)
2295+
2296+
def test_dotted_as_replays_lookups_on_dotted_placeholder(self):
2297+
"""A dotted lazy import as replays its names on the hook's package."""
2298+
# importlib.metadata has a `metadata` attribute of its own, which the
2299+
# placeholder for importlib must not answer with.
2300+
for target in ("xml.dom", "importlib.metadata"):
2301+
with self.subTest(target=target):
2302+
leaf = target.rpartition(".")[2]
2303+
code = textwrap.dedent(f"""
2304+
import builtins
2305+
import sys
2306+
import {target}
2307+
2308+
# In a list, so the hook reading it does not resolve it.
2309+
placeholder = [__lazy_import__("{target}", fromlist=())]
2310+
default = builtins.__lazy_import__
2311+
builtins.__lazy_import__ = lambda *args: placeholder[0]
2312+
lazy import fake.{leaf} as {leaf}
2313+
builtins.__lazy_import__ = default
2314+
2315+
name = repr(globals()["{leaf}"])
2316+
assert name == "<lazy_import '{target}'>", name
2317+
assert {leaf} is sys.modules["{target}"], {leaf}
2318+
""")
2319+
assert_python_ok("-c", code)
2320+
22832321

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

‎Objects/lazyimportobject.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins, PyObject *name
2626
"lazy_import: fromlist must be None, a string, or a tuple");
2727
return NULL;
2828
}
29+
else if (PyTuple_Check(fromlist) && PyTuple_GET_SIZE(fromlist) == 0) {
30+
// __import__("a.b", fromlist=()) returns `a`, as fromlist=None does.
31+
fromlist = NULL;
32+
}
2933
m = PyObject_GC_New(PyLazyImportObject, &PyLazyImport_Type);
3034
if (m == NULL) {
3135
return NULL;

0 commit comments

Comments
 (0)