@@ -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
281548class SyntaxRestrictionTests (LazyImportTestCase ):
282549 """Tests for syntax restrictions on lazy imports."""
0 commit comments