Skip to content

Commit 8f00fc9

Browse files
committed
gh-152297: Reify dotted lazy imports with full name
1 parent 6713576 commit 8f00fc9

3 files changed

Lines changed: 103 additions & 30 deletions

File tree

Lib/test/test_lazy_import/__init__.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,103 @@ def test_from_import_with_imported_module_getattr(self):
167167
""")
168168
assert_python_ok("-c", code)
169169

170+
@support.requires_subprocess()
171+
def test_dotted_import_calls_full_name_import_hook(self):
172+
"""Dotted lazy imports should call __import__ with the full name."""
173+
code = textwrap.dedent(r"""
174+
import atexit
175+
import builtins
176+
import shutil
177+
import sys
178+
import tempfile
179+
import types
180+
from pathlib import Path
181+
182+
PKG = "hookpkg_gh_152297"
183+
SUBMOD = f"{PKG}.sub"
184+
185+
real_import = builtins.__import__
186+
full_name_calls = []
187+
calls = []
188+
189+
tmpdir = tempfile.mkdtemp()
190+
atexit.register(shutil.rmtree, tmpdir, ignore_errors=True)
191+
192+
package_dir = Path(tmpdir, PKG)
193+
package_dir.mkdir()
194+
(package_dir / "__init__.py").touch()
195+
(package_dir / "sub.py").write_text(
196+
"VALUE = 'real-submodule'\n", encoding="utf-8")
197+
198+
sys.path.insert(0, tmpdir)
199+
atexit.register(setattr, builtins, "__import__", real_import)
200+
201+
def custom_import(name, globals=None, locals=None, fromlist=(),
202+
level=0):
203+
caller = (
204+
globals.get("__name__")
205+
if isinstance(globals, dict) else None
206+
)
207+
call = (name, caller, tuple(fromlist or ()))
208+
calls.append(call)
209+
210+
if name == SUBMOD:
211+
full_name_calls.append(call)
212+
package = real_import(PKG, globals, locals, (), level)
213+
module = types.ModuleType(SUBMOD)
214+
module.VALUE = "hooked-submodule"
215+
package.sub = module
216+
sys.modules[SUBMOD] = module
217+
return package
218+
219+
return real_import(name, globals, locals, fromlist, level)
220+
221+
builtins.__import__ = custom_import
222+
223+
lazy import hookpkg_gh_152297.sub
224+
225+
assert not full_name_calls, calls
226+
assert hookpkg_gh_152297.sub.VALUE == "hooked-submodule", calls
227+
assert full_name_calls == [(SUBMOD, "__main__", ())], calls
228+
""")
229+
assert_python_ok("-c", code)
230+
231+
@support.requires_subprocess()
232+
def test_dotted_import_first_use_loads_full_target(self):
233+
"""First use of a dotted lazy import should load the submodule."""
234+
code = textwrap.dedent(r"""
235+
import atexit
236+
import shutil
237+
import sys
238+
import tempfile
239+
from pathlib import Path
240+
241+
PKG = "hookpkg_gh_152297_default"
242+
SUBMOD = f"{PKG}.sub"
243+
244+
tmpdir = tempfile.mkdtemp()
245+
atexit.register(shutil.rmtree, tmpdir, ignore_errors=True)
246+
247+
package_dir = Path(tmpdir, PKG)
248+
package_dir.mkdir()
249+
(package_dir / "__init__.py").touch()
250+
(package_dir / "sub.py").write_text(
251+
"VALUE = 42\n", encoding="utf-8")
252+
253+
sys.path.insert(0, tmpdir)
254+
255+
lazy import hookpkg_gh_152297_default.sub
256+
257+
assert SUBMOD not in sys.modules
258+
259+
_ = hookpkg_gh_152297_default
260+
261+
assert PKG in sys.modules
262+
assert SUBMOD in sys.modules
263+
assert hookpkg_gh_152297_default.sub.VALUE == 42
264+
""")
265+
assert_python_ok("-c", code)
266+
170267

171268
class GlobalLazyImportModeTests(LazyImportTestCase):
172269
"""Tests for sys.set_lazy_imports() global mode control."""
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix ``lazy import pkg.sub`` so reification calls ``__import__`` with
2+
``pkg.sub``, matching eager imports.

Python/import.c

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3940,19 +3940,6 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
39403940
goto error;
39413941
}
39423942

3943-
Py_ssize_t dot = -1;
3944-
int full = 0;
3945-
if (lz->lz_attr != NULL) {
3946-
full = 1;
3947-
}
3948-
if (!full) {
3949-
dot = PyUnicode_FindChar(lz->lz_from, '.', 0,
3950-
PyUnicode_GET_LENGTH(lz->lz_from), 1);
3951-
}
3952-
if (dot < 0) {
3953-
full = 1;
3954-
}
3955-
39563943
if (lz->lz_attr != NULL) {
39573944
if (PyUnicode_Check(lz->lz_attr)) {
39583945
fromlist = PyTuple_New(1);
@@ -3978,23 +3965,10 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
39783965
PyErr_SetString(PyExc_ImportError, "__import__ not found");
39793966
goto error;
39803967
}
3981-
if (full) {
3982-
obj = _PyEval_ImportNameWithImport(
3983-
tstate, import_func, globals, globals,
3984-
lz->lz_from, fromlist, _PyLong_GetZero()
3985-
);
3986-
}
3987-
else {
3988-
PyObject *name = PyUnicode_Substring(lz->lz_from, 0, dot);
3989-
if (name == NULL) {
3990-
goto error;
3991-
}
3992-
obj = _PyEval_ImportNameWithImport(
3993-
tstate, import_func, globals, globals,
3994-
name, fromlist, _PyLong_GetZero()
3995-
);
3996-
Py_DECREF(name);
3997-
}
3968+
obj = _PyEval_ImportNameWithImport(
3969+
tstate, import_func, globals, globals,
3970+
lz->lz_from, fromlist, _PyLong_GetZero()
3971+
);
39983972
if (obj == NULL) {
39993973
goto error;
40003974
}

0 commit comments

Comments
 (0)