Skip to content

Commit 632d95b

Browse files
committed
gh-152298: Fix LazyImportType resolve reification
1 parent 6713576 commit 632d95b

10 files changed

Lines changed: 606 additions & 23 deletions

File tree

Include/internal/pycore_lazyimportobject.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ typedef struct {
1919
PyObject *lz_builtins;
2020
PyObject *lz_from;
2121
PyObject *lz_attr;
22+
PyObject *lz_owner_globals;
23+
PyObject *lz_owner_key;
24+
PyObject *lz_resolved;
2225
// Frame information for the original import location.
2326
PyCodeObject *lz_code; // Code object where the lazy import was created.
2427
int lz_instr_offset; // Instruction offset where the lazy import was created.
@@ -28,6 +31,21 @@ typedef struct {
2831
PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import);
2932
PyAPI_FUNC(PyObject *) _PyLazyImport_New(
3033
struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from, PyObject *attr);
34+
PyAPI_FUNC(void) _PyLazyImport_BindGlobal(
35+
PyThreadState *tstate,
36+
PyObject *lazy_import,
37+
PyObject *globals,
38+
PyObject *name);
39+
PyAPI_FUNC(int) _PyLazyImport_CommitIfCurrent(
40+
PyThreadState *tstate,
41+
PyObject *lazy_import,
42+
PyObject *globals,
43+
PyObject *name,
44+
PyObject *resolved);
45+
PyAPI_FUNC(int) _PyLazyImport_CommitStoredIfCurrent(
46+
PyThreadState *tstate,
47+
PyObject *lazy_import,
48+
PyObject *resolved);
3149

3250
#ifdef __cplusplus
3351
}

Lib/test/test_lazy_import/__init__.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,273 @@ def test_lazy_import_type_attributes_accessible(self):
277277
proc = assert_python_ok("-c", code)
278278
self.assertIn(b"<built-in method resolve of lazy_import object at", proc.out)
279279

280+
@support.requires_subprocess()
281+
def test_resolve_replaces_original_global(self):
282+
code = textwrap.dedent("""
283+
import builtins
284+
import types
285+
286+
real_import = builtins.__import__
287+
calls = []
288+
289+
lazy import target_module as target
290+
291+
def custom_import(name, globals=None, locals=None, fromlist=None,
292+
level=0):
293+
if name == "target_module":
294+
index = len(calls) + 1
295+
calls.append((index, name, globals.get("__name__"), fromlist))
296+
module = types.ModuleType(name)
297+
module.VALUE = f"value-{index}"
298+
return module
299+
return real_import(name, globals, locals, fromlist, level)
300+
301+
builtins.__import__ = custom_import
302+
try:
303+
def resolve_from_dict():
304+
lazy_obj = globals()["target"]
305+
resolved = lazy_obj.resolve()
306+
assert resolved.VALUE == "value-1"
307+
assert globals()["target"] is resolved
308+
assert target.VALUE == "value-1"
309+
310+
resolve_from_dict()
311+
assert len(calls) == 1, calls
312+
finally:
313+
builtins.__import__ = real_import
314+
""")
315+
assert_python_ok("-c", code)
316+
317+
@support.requires_subprocess()
318+
def test_resolve_is_cached_on_stale_proxy(self):
319+
code = textwrap.dedent("""
320+
import builtins
321+
import types
322+
323+
real_import = builtins.__import__
324+
calls = []
325+
326+
lazy import target_module as target
327+
328+
def custom_import(name, globals=None, locals=None, fromlist=None,
329+
level=0):
330+
if name == "target_module":
331+
calls.append(name)
332+
module = types.ModuleType(name)
333+
module.VALUE = len(calls)
334+
return module
335+
return real_import(name, globals, locals, fromlist, level)
336+
337+
builtins.__import__ = custom_import
338+
try:
339+
def resolve_twice():
340+
lazy_obj = globals()["target"]
341+
first = lazy_obj.resolve()
342+
second = lazy_obj.resolve()
343+
assert first is second
344+
assert globals()["target"] is first
345+
346+
resolve_twice()
347+
assert len(calls) == 1, calls
348+
finally:
349+
builtins.__import__ = real_import
350+
""")
351+
assert_python_ok("-c", code)
352+
353+
@support.requires_subprocess()
354+
def test_resolve_after_automatic_reification_uses_cache(self):
355+
code = textwrap.dedent("""
356+
import builtins
357+
import types
358+
359+
real_import = builtins.__import__
360+
calls = []
361+
362+
lazy import target_module as target
363+
holder = [globals()["target"]]
364+
365+
def custom_import(name, globals=None, locals=None, fromlist=None,
366+
level=0):
367+
if name == "target_module":
368+
calls.append(name)
369+
module = types.ModuleType(name)
370+
module.VALUE = f"value-{len(calls)}"
371+
return module
372+
return real_import(name, globals, locals, fromlist, level)
373+
374+
builtins.__import__ = custom_import
375+
try:
376+
first = target.VALUE
377+
second = holder[0].resolve()
378+
assert second.VALUE == first == "value-1"
379+
assert globals()["target"] is second
380+
assert len(calls) == 1, calls
381+
finally:
382+
builtins.__import__ = real_import
383+
""")
384+
assert_python_ok("-c", code)
385+
386+
@support.requires_subprocess()
387+
def test_resolve_uses_actual_alias_binding(self):
388+
code = textwrap.dedent("""
389+
import builtins
390+
import types
391+
392+
real_import = builtins.__import__
393+
calls = []
394+
395+
lazy import target_module as target
396+
lazy from target_module import VALUE as alias
397+
398+
def custom_import(name, globals=None, locals=None, fromlist=None,
399+
level=0):
400+
if name == "target_module":
401+
index = len(calls) + 1
402+
calls.append((name, fromlist))
403+
module = types.ModuleType(name)
404+
module.VALUE = f"value-{index}"
405+
return module
406+
return real_import(name, globals, locals, fromlist, level)
407+
408+
builtins.__import__ = custom_import
409+
try:
410+
def resolve_aliases():
411+
target_lazy = globals()["target"]
412+
target_resolved = target_lazy.resolve()
413+
assert globals()["target"] is target_resolved
414+
assert globals().get("target_module") is not target_resolved
415+
416+
alias_lazy = globals()["alias"]
417+
alias_resolved = alias_lazy.resolve()
418+
assert globals()["alias"] is alias_resolved
419+
assert alias_resolved == "value-2"
420+
421+
resolve_aliases()
422+
assert len(calls) == 2, calls
423+
finally:
424+
builtins.__import__ = real_import
425+
""")
426+
assert_python_ok("-c", code)
427+
428+
@support.requires_subprocess()
429+
def test_resolve_does_not_clobber_rebound_or_deleted_global(self):
430+
code = textwrap.dedent("""
431+
import builtins
432+
import types
433+
434+
real_import = builtins.__import__
435+
calls = []
436+
437+
lazy import target_module as rebound
438+
lazy import target_module as deleted
439+
440+
def custom_import(name, globals=None, locals=None, fromlist=None,
441+
level=0):
442+
if name == "target_module":
443+
calls.append(name)
444+
module = types.ModuleType(name)
445+
module.VALUE = len(calls)
446+
return module
447+
return real_import(name, globals, locals, fromlist, level)
448+
449+
builtins.__import__ = custom_import
450+
try:
451+
def resolve_after_changes():
452+
rebound_lazy = globals()["rebound"]
453+
deleted_lazy = globals()["deleted"]
454+
sentinel = object()
455+
globals()["rebound"] = sentinel
456+
del globals()["deleted"]
457+
458+
rebound_resolved = rebound_lazy.resolve()
459+
deleted_resolved = deleted_lazy.resolve()
460+
461+
assert globals()["rebound"] is sentinel
462+
assert "deleted" not in globals()
463+
assert rebound_resolved.VALUE == 1
464+
assert deleted_resolved.VALUE == 2
465+
466+
resolve_after_changes()
467+
assert len(calls) == 2, calls
468+
finally:
469+
builtins.__import__ = real_import
470+
""")
471+
assert_python_ok("-c", code)
472+
473+
@support.requires_subprocess()
474+
def test_resolve_failure_preserves_lazy_global_for_retry(self):
475+
code = textwrap.dedent("""
476+
import builtins
477+
import types
478+
479+
real_import = builtins.__import__
480+
attempts = 0
481+
482+
lazy import target_module as target
483+
484+
def custom_import(name, globals=None, locals=None, fromlist=None,
485+
level=0):
486+
global attempts
487+
if name == "target_module":
488+
attempts += 1
489+
if attempts == 1:
490+
raise ImportError("not yet")
491+
module = types.ModuleType(name)
492+
module.VALUE = "ok"
493+
return module
494+
return real_import(name, globals, locals, fromlist, level)
495+
496+
builtins.__import__ = custom_import
497+
try:
498+
def retry_resolve():
499+
lazy_obj = globals()["target"]
500+
try:
501+
lazy_obj.resolve()
502+
except ImportError:
503+
pass
504+
else:
505+
raise AssertionError("first resolve should fail")
506+
assert globals()["target"] is lazy_obj
507+
508+
resolved = lazy_obj.resolve()
509+
assert globals()["target"] is resolved
510+
assert resolved.VALUE == "ok"
511+
512+
retry_resolve()
513+
assert attempts == 2, attempts
514+
finally:
515+
builtins.__import__ = real_import
516+
""")
517+
assert_python_ok("-c", code)
518+
519+
@support.requires_subprocess()
520+
def test_automatic_reification_does_not_clobber_changed_global(self):
521+
code = textwrap.dedent("""
522+
import builtins
523+
import types
524+
525+
real_import = builtins.__import__
526+
527+
lazy import target_module as target
528+
529+
def custom_import(name, globals=None, locals=None, fromlist=None,
530+
level=0):
531+
if name == "target_module":
532+
globals["target"] = "user value"
533+
module = types.ModuleType(name)
534+
module.VALUE = "resolved"
535+
return module
536+
return real_import(name, globals, locals, fromlist, level)
537+
538+
builtins.__import__ = custom_import
539+
try:
540+
assert target.VALUE == "resolved"
541+
assert globals()["target"] == "user value"
542+
finally:
543+
builtins.__import__ = real_import
544+
""")
545+
assert_python_ok("-c", code)
546+
280547

281548
class SyntaxRestrictionTests(LazyImportTestCase):
282549
"""Tests for syntax restrictions on lazy imports."""
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Make :meth:`types.LazyImportType.resolve` cache successful resolutions and
2+
replace the original module global only when it still references the same lazy
3+
import proxy.

0 commit comments

Comments
 (0)