@@ -470,6 +470,272 @@ 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 previous , fromlist in ((None , ()), ((), 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.minidom', fromlist={ previous !r} )\\ n"
563+ "xml = __lazy_import__('xml.dom.pulldom', fromlist={ fromlist !r} )",
564+ namespace)
565+ exec("assert xml.dom.__name__ == 'xml.dom'", namespace)
566+ assert calls == [('xml', { fromlist !r} ),
567+ ('xml.dom', { fromlist !r} )], calls
568+ """ )
569+
570+ def test_cached_child_with_import_hook (self ):
571+ for access in ('xml.dom' , 'getattr(xml, "dom", None)' , 'helper(xml)' ):
572+ with self .subTest (access = access ):
573+ self .check (f"""
574+ import builtins, sys, xml.dom
575+ del xml.dom
576+ original = builtins.__import__
577+ calls = []
578+ denied = True
579+ def hook(name, *args):
580+ calls.append(name)
581+ if denied and name == 'xml.dom':
582+ raise PermissionError('denied child')
583+ return original(name, *args)
584+ def helper(package):
585+ return package.dom
586+ namespace = {{'helper': helper, '__builtins__': dict(
587+ vars(builtins), __import__=hook)}}
588+ exec('lazy import xml.dom\\ nlazy import xml.missing', namespace)
589+ try:
590+ exec({ access !r} , namespace)
591+ except PermissionError:
592+ pass
593+ else:
594+ raise AssertionError('import hook was bypassed')
595+ assert calls == ['xml', 'xml.dom'], calls
596+ denied = False
597+ exec('result = ' + { access !r} , namespace)
598+ assert namespace['result'] is sys.modules['xml.dom']
599+ assert calls == ['xml', 'xml.dom', 'xml.dom'], calls
600+ """ )
601+
602+ def test_failed_hook_preserves_namespace_declarations (self ):
603+ self .check ("""
604+ import builtins, sys
605+ original = builtins.__import__
606+ calls = []
607+ def second_import(name, *args, **kwargs):
608+ if name == 'xml.dom':
609+ calls.append('second')
610+ raise RuntimeError('second')
611+ return original(name, *args, **kwargs)
612+ second = {'__builtins__': dict(vars(builtins), __import__=second_import)}
613+ def first_import(name, *args, **kwargs):
614+ module = original(name, *args, **kwargs)
615+ if name == 'xml.dom':
616+ calls.append('first')
617+ del module.dom
618+ del sys.modules[name]
619+ exec('lazy import xml.dom', second)
620+ raise RuntimeError('first')
621+ return module
622+ first = {'__builtins__': dict(vars(builtins), __import__=first_import)}
623+ exec('lazy import xml.dom', first)
624+ for namespace, message in ((first, 'first'), (first, 'first'),
625+ (second, 'second')):
626+ try:
627+ exec('xml.dom', namespace)
628+ except RuntimeError as exc:
629+ assert str(exc) == message, exc
630+ else:
631+ raise AssertionError('hook did not run')
632+ assert calls == ['first', 'first', 'second'], calls
633+ """ )
634+
635+ def test_siblings_after_builtins_replacement (self ):
636+ self .check ("""
637+ import builtins, types, xml.dom
638+ calls = []
639+ def hook(name, *args):
640+ calls.append(name)
641+ assert name == 'xml.dom', name
642+ return types.SimpleNamespace(dom=42)
643+ def helper(package):
644+ return package.dom
645+ namespace = {'__builtins__': dict(vars(builtins), __import__=hook),
646+ 'helper': helper}
647+ exec('lazy import xml.dom', namespace)
648+ namespace['__builtins__'] = vars(builtins)
649+ exec('lazy import xml.missing', namespace)
650+ del xml.dom
651+ exec('result = helper(xml)', namespace)
652+ assert namespace['result'] == 42
653+ assert calls == ['xml.dom'], calls
654+ assert 'dom' not in vars(xml)
655+ """ )
656+
657+ def test_live_namespace_keeps_overwritten_sibling_source (self ):
658+ for statement in ('lazy import {name}' ,
659+ 'live_owners = __lazy_import__({name!r})' ,
660+ "dict.__setitem__(globals(), 'live_owners', "
661+ "__lazy_import__({name!r}))" ):
662+ code = '\n ' .join (statement .format (name = 'live_owners.' + child )
663+ for child in ('child' , 'missing' ))
664+ with self .subTest (statement = statement ):
665+ self .check (f"""
666+ import builtins, gc, sys, types
667+ root = types.ModuleType('live_owners')
668+ root.__path__ = []
669+ sys.modules['live_owners'] = root
670+ def first_import(name, *args):
671+ raise AssertionError('used the first namespace hook')
672+ first = {{'__builtins__': dict(
673+ vars(builtins), __import__=first_import)}}
674+ calls = []
675+ def second_import(name, *args):
676+ calls.append(name)
677+ if name == 'live_owners':
678+ return root
679+ assert name == 'live_owners.child', name
680+ return types.SimpleNamespace(child=42)
681+ second = {{'__builtins__': dict(
682+ vars(builtins), __import__=second_import)}}
683+ exec('lazy import live_owners.child', first)
684+ exec({ code !r} , second)
685+ gc.collect()
686+ exec('root = live_owners', second)
687+ gc.collect()
688+ exec('result = live_owners.child', second)
689+ assert second['result'] == 42
690+ assert calls == ['live_owners', 'live_owners.child'], calls
691+ """ )
692+
693+ def test_discarded_namespaces_do_not_retain_all_import_hooks (self ):
694+ self .check ("""
695+ import builtins, gc, weakref
696+ class Payload:
697+ pass
698+ references = []
699+ for _ in range(32):
700+ payload = Payload()
701+ references.append(weakref.ref(payload))
702+ namespace = {'__builtins__': dict(vars(builtins), payload=payload)}
703+ exec('lazy import dormant_owners.child', namespace)
704+ del namespace, payload
705+ gc.collect()
706+ # Keep one fallback for accesses outside the declaring namespace.
707+ assert sum(ref() is not None for ref in references) == 1
708+ """ )
709+
710+ def test_cached_declaration_does_not_run_spec_callbacks (self ):
711+ self .check ("""
712+ import xml.dom
713+ class Spec:
714+ @property
715+ def _initializing(self):
716+ raise AssertionError('spec read during declaration')
717+ xml.__spec__ = Spec()
718+ lazy import xml.dom
719+ """ )
720+
721+ def test_hook_creates_aliased_package (self ):
722+ self .check ("""
723+ import builtins, sys, types
724+ root = types.ModuleType('alias')
725+ root.__path__ = []
726+ calls = []
727+ def hook(name, *args):
728+ calls.append(name)
729+ sys.modules['pkg'] = root
730+ if name == 'pkg.child':
731+ root.child = 42
732+ return root
733+ namespace = {'__builtins__': dict(vars(builtins), __import__=hook)}
734+ exec('lazy import pkg.child\\ nresult = pkg.child', namespace)
735+ assert namespace['result'] == 42
736+ assert calls == ['pkg', 'pkg.child'], calls
737+ """ )
738+
473739 def test_star_import (self ):
474740 self .check ("""
475741 lazy import urllib.nonexistent
@@ -2708,9 +2974,8 @@ def test_empty_fromlist_preserved_for_custom_import(self):
27082974 calls = []
27092975
27102976 def import_hook(name, globals, locals, fromlist, level):
2711- assert name == "xml.dom", name
27122977 assert fromlist == (), fromlist
2713- calls.append(fromlist)
2978+ calls.append((name, fromlist) )
27142979 return module
27152980
27162981 builtins.__import__ = import_hook
@@ -2721,7 +2986,7 @@ def import_hook(name, globals, locals, fromlist, level):
27212986 builtins.__import__ = default_import
27222987 builtins.__lazy_import__ = default_lazy_import
27232988
2724- assert calls == [() , ()], calls
2989+ assert calls == [('xml', ()) , ('xml.dom', ()), ('xml.dom', () )], calls
27252990 """ )
27262991 assert_python_ok ("-c" , code )
27272992
0 commit comments