@@ -470,6 +470,263 @@ def test_siblings(self):
470470 assert { package } .{ second } is sys.modules['{ package } .{ second } ']
471471 """ )
472472
473+ def test_siblings_with_import_hook (self ):
474+ accesses = ('xml.dom' , 'getattr(xml, "dom")' ,
475+ 'getattr(xml, "dom", None)' , 'hasattr(xml, "dom")' ,
476+ 'helper(xml)' , 'xml' )
477+ for private in (False , True ):
478+ for sibling in ('xml.etree' , 'xml.missing' ):
479+ for reverse in (False , True ):
480+ statements = ['lazy import xml.dom' ,
481+ f'lazy import { sibling } ' ]
482+ if reverse :
483+ statements .reverse ()
484+ for access in accesses :
485+ code = '\n ' .join (statements + [f'result = { access } ' ])
486+ with self .subTest (private = private , sibling = sibling ,
487+ reverse = reverse , access = access ):
488+ self .check (f"""
489+ import builtins, sys
490+ original = builtins.__import__
491+ calls = []
492+ def hook(name, *args):
493+ if name.startswith('xml'):
494+ calls.append(name)
495+ return original(name, *args)
496+ def helper(package):
497+ return package.dom
498+ namespace = {{'helper': helper}}
499+ if { private } :
500+ namespace['__builtins__'] = dict(
501+ vars(builtins), __import__=hook)
502+ else:
503+ builtins.__import__ = hook
504+ exec({ code !r} , namespace)
505+ assert { sibling !r} not in calls, calls
506+ assert { sibling !r} not in sys.modules
507+ if { access !r} == 'xml':
508+ assert namespace['result'] is sys.modules['xml']
509+ assert 'xml.dom' not in sys.modules
510+ assert calls == ['xml'], calls
511+ else:
512+ result = namespace['result']
513+ if { access !r} == 'hasattr(xml, "dom")':
514+ assert result is True
515+ else:
516+ assert result is sys.modules['xml.dom']
517+ assert calls == ['xml', 'xml.dom'], calls
518+ """ )
519+
520+ def test_preloaded_sibling_with_import_hook (self ):
521+ for private in (False , True ):
522+ with self .subTest (private = private ):
523+ self .check (f"""
524+ import builtins, sys, xml.etree
525+ original = builtins.__import__
526+ calls = []
527+ def hook(name, *args):
528+ if name.startswith('xml'):
529+ calls.append(name)
530+ if name == 'xml.etree':
531+ raise AssertionError('requested an unused sibling')
532+ return original(name, *args)
533+ namespace = {{}}
534+ if { private } :
535+ namespace['__builtins__'] = dict(
536+ vars(builtins), __import__=hook)
537+ else:
538+ builtins.__import__ = hook
539+ exec('lazy import xml.dom\\ nlazy import xml.etree', namespace)
540+ exec('root = xml', namespace)
541+ assert namespace['root'] is sys.modules['xml']
542+ assert 'xml.dom' not in sys.modules
543+ assert calls == ['xml'], calls
544+ exec('result = xml.dom', namespace)
545+ assert namespace['result'] is sys.modules['xml.dom']
546+ assert calls == ['xml', 'xml.dom'], calls
547+ """ )
548+
549+ def test_pending_child_preserves_fromlist (self ):
550+ for fromlist in (None , ()):
551+ with self .subTest (fromlist = fromlist ):
552+ self .check (f"""
553+ import builtins
554+ original = builtins.__import__
555+ calls = []
556+ def hook(name, globals, locals, fromlist, level):
557+ calls.append((name, fromlist))
558+ assert fromlist == { fromlist !r}
559+ return original(name, globals, locals, fromlist, level)
560+ namespace = {{'__builtins__': dict(
561+ vars(builtins), __import__=hook)}}
562+ exec("xml = __lazy_import__('xml.dom', fromlist={ fromlist !r} )",
563+ namespace)
564+ exec("assert xml.dom.__name__ == 'xml.dom'", namespace)
565+ assert calls == [('xml', { fromlist !r} ),
566+ ('xml.dom', { fromlist !r} )], calls
567+ """ )
568+
569+ def test_cached_child_with_import_hook (self ):
570+ for access in ('xml.dom' , 'getattr(xml, "dom", None)' , 'helper(xml)' ):
571+ with self .subTest (access = access ):
572+ self .check (f"""
573+ import builtins, sys, xml.dom
574+ del xml.dom
575+ original = builtins.__import__
576+ calls = []
577+ denied = True
578+ def hook(name, *args):
579+ calls.append(name)
580+ if denied and name == 'xml.dom':
581+ raise PermissionError('denied child')
582+ return original(name, *args)
583+ def helper(package):
584+ return package.dom
585+ namespace = {{'helper': helper, '__builtins__': dict(
586+ vars(builtins), __import__=hook)}}
587+ exec('lazy import xml.dom\\ nlazy import xml.missing', namespace)
588+ try:
589+ exec({ access !r} , namespace)
590+ except PermissionError:
591+ pass
592+ else:
593+ raise AssertionError('import hook was bypassed')
594+ assert calls == ['xml', 'xml.dom'], calls
595+ denied = False
596+ exec('result = ' + { access !r} , namespace)
597+ assert namespace['result'] is sys.modules['xml.dom']
598+ assert calls == ['xml', 'xml.dom', 'xml.dom'], calls
599+ """ )
600+
601+ def test_failed_hook_preserves_namespace_declarations (self ):
602+ self .check ("""
603+ import builtins, sys
604+ original = builtins.__import__
605+ calls = []
606+ def second_import(name, *args, **kwargs):
607+ if name == 'xml.dom':
608+ calls.append('second')
609+ raise RuntimeError('second')
610+ return original(name, *args, **kwargs)
611+ second = {'__builtins__': dict(vars(builtins), __import__=second_import)}
612+ def first_import(name, *args, **kwargs):
613+ module = original(name, *args, **kwargs)
614+ if name == 'xml.dom':
615+ calls.append('first')
616+ del module.dom
617+ del sys.modules[name]
618+ exec('lazy import xml.dom', second)
619+ raise RuntimeError('first')
620+ return module
621+ first = {'__builtins__': dict(vars(builtins), __import__=first_import)}
622+ exec('lazy import xml.dom', first)
623+ for namespace, message in ((first, 'first'), (first, 'first'),
624+ (second, 'second')):
625+ try:
626+ exec('xml.dom', namespace)
627+ except RuntimeError as exc:
628+ assert str(exc) == message, exc
629+ else:
630+ raise AssertionError('hook did not run')
631+ assert calls == ['first', 'first', 'second'], calls
632+ """ )
633+
634+ def test_siblings_after_builtins_replacement (self ):
635+ self .check ("""
636+ import builtins, types, xml.dom
637+ calls = []
638+ def hook(name, *args):
639+ calls.append(name)
640+ assert name == 'xml.dom', name
641+ return types.SimpleNamespace(dom=42)
642+ def helper(package):
643+ return package.dom
644+ namespace = {'__builtins__': dict(vars(builtins), __import__=hook),
645+ 'helper': helper}
646+ exec('lazy import xml.dom', namespace)
647+ namespace['__builtins__'] = vars(builtins)
648+ exec('lazy import xml.missing', namespace)
649+ del xml.dom
650+ exec('result = helper(xml)', namespace)
651+ assert namespace['result'] == 42
652+ assert calls == ['xml.dom'], calls
653+ assert 'dom' not in vars(xml)
654+ """ )
655+
656+ def test_live_namespace_keeps_overwritten_sibling_source (self ):
657+ self .check ("""
658+ import builtins, gc, sys, types
659+ root = types.ModuleType('live_owners')
660+ root.__path__ = []
661+ sys.modules['live_owners'] = root
662+ def first_import(name, *args):
663+ raise AssertionError('used the first namespace hook')
664+ first = {'__builtins__': dict(vars(builtins), __import__=first_import)}
665+ calls = []
666+ def second_import(name, *args):
667+ calls.append(name)
668+ if name == 'live_owners':
669+ return root
670+ assert name == 'live_owners.child', name
671+ return types.SimpleNamespace(child=42)
672+ second = {'__builtins__': dict(vars(builtins), __import__=second_import)}
673+ exec('lazy import live_owners.child', first)
674+ exec('lazy import live_owners.child\\ nlazy import live_owners.missing',
675+ second)
676+ gc.collect()
677+ exec('root = live_owners', second)
678+ gc.collect()
679+ exec('result = live_owners.child', second)
680+ assert second['result'] == 42
681+ assert calls == ['live_owners', 'live_owners.child'], calls
682+ """ )
683+
684+ def test_discarded_namespaces_do_not_retain_all_import_hooks (self ):
685+ self .check ("""
686+ import builtins, gc, weakref
687+ class Payload:
688+ pass
689+ references = []
690+ for _ in range(32):
691+ payload = Payload()
692+ references.append(weakref.ref(payload))
693+ namespace = {'__builtins__': dict(vars(builtins), payload=payload)}
694+ exec('lazy import dormant_owners.child', namespace)
695+ del namespace, payload
696+ gc.collect()
697+ # Keep one fallback for accesses outside the declaring namespace.
698+ assert sum(ref() is not None for ref in references) == 1
699+ """ )
700+
701+ def test_cached_declaration_does_not_run_spec_callbacks (self ):
702+ self .check ("""
703+ import xml.dom
704+ class Spec:
705+ @property
706+ def _initializing(self):
707+ raise AssertionError('spec read during declaration')
708+ xml.__spec__ = Spec()
709+ lazy import xml.dom
710+ """ )
711+
712+ def test_hook_creates_aliased_package (self ):
713+ self .check ("""
714+ import builtins, sys, types
715+ root = types.ModuleType('alias')
716+ root.__path__ = []
717+ calls = []
718+ def hook(name, *args):
719+ calls.append(name)
720+ sys.modules['pkg'] = root
721+ if name == 'pkg.child':
722+ root.child = 42
723+ return root
724+ namespace = {'__builtins__': dict(vars(builtins), __import__=hook)}
725+ exec('lazy import pkg.child\\ nresult = pkg.child', namespace)
726+ assert namespace['result'] == 42
727+ assert calls == ['pkg', 'pkg.child'], calls
728+ """ )
729+
473730 def test_star_import (self ):
474731 self .check ("""
475732 lazy import urllib.nonexistent
@@ -2708,9 +2965,8 @@ def test_empty_fromlist_preserved_for_custom_import(self):
27082965 calls = []
27092966
27102967 def import_hook(name, globals, locals, fromlist, level):
2711- assert name == "xml.dom", name
27122968 assert fromlist == (), fromlist
2713- calls.append(fromlist)
2969+ calls.append((name, fromlist) )
27142970 return module
27152971
27162972 builtins.__import__ = import_hook
@@ -2721,7 +2977,7 @@ def import_hook(name, globals, locals, fromlist, level):
27212977 builtins.__import__ = default_import
27222978 builtins.__lazy_import__ = default_lazy_import
27232979
2724- assert calls == [() , ()], calls
2980+ assert calls == [('xml', ()) , ('xml.dom', ()), ('xml.dom', () )], calls
27252981 """ )
27262982 assert_python_ok ("-c" , code )
27272983
0 commit comments