@@ -844,32 +844,142 @@ def test_getmodule(self):
844844 sys .modules [__name__ ])
845845
846846 def test_getmodule_unregistered_exec_frame (self ):
847- def exec_namespace (namespace ):
847+ def exec_namespace (namespace , filename , expected ):
848848 exec (compile (textwrap .dedent ("""
849849 frame = inspect.currentframe()
850850 try:
851851 1 / 0
852852 except ZeroDivisionError as error:
853853 traceback = error.__traceback__
854- """ ), modfile , "exec" ), namespace )
855- self .assertIsNone (inspect .getmodule (namespace ["frame" ]))
856- self .assertIsNone (inspect .getmodule (namespace ["traceback" ]))
857-
858- # Missing and invalid module names identify no registered namespace.
859- exec_namespace ({"inspect" : inspect })
860- exec_namespace ({"inspect" : inspect , "__name__" : []})
861-
862- module_name = f"{ __name__ } .not_registered"
863- for module in (None , object (), types .ModuleType (module_name )):
864- with self .subTest (module = module ):
865- sys .modules [module_name ] = module
866- try :
867- exec_namespace ({
868- "inspect" : inspect ,
869- "__name__" : module_name ,
870- })
871- finally :
872- del sys .modules [module_name ]
854+ """ ), filename , "exec" ), namespace )
855+ self .assertIs (inspect .getmodule (namespace ["frame" ]), expected )
856+ self .assertIs (
857+ inspect .getmodule (namespace ["frame" ].f_code ), expected )
858+ self .assertIs (inspect .getmodule (namespace ["traceback" ]), expected )
859+
860+ with (unittest .mock .patch .object (inspect , "modulesbyfile" , {}),
861+ unittest .mock .patch .object (inspect , "_filesbymodname" , {}),
862+ unittest .mock .patch .object (
863+ inspect , "_modulesbyfile_snapshot" , None )):
864+ # Preserve filename-based resolution when the execution namespace
865+ # does not identify the module that supplied the source.
866+ exec_namespace ({"inspect" : inspect }, modfile , mod )
867+ exec_namespace ({"inspect" : inspect , "__name__" : []},
868+ modfile , mod )
869+
870+ module_name = f"{ __name__ } .not_registered"
871+ for module in (None , object (), types .ModuleType (module_name )):
872+ with self .subTest (module = module ):
873+ sys .modules [module_name ] = module
874+ try :
875+ exec_namespace ({
876+ "inspect" : inspect ,
877+ "__name__" : module_name ,
878+ }, modfile , mod )
879+ finally :
880+ del sys .modules [module_name ]
881+
882+ # A namespace and filename with no registered module still
883+ # resolves to None after the filename fallback.
884+ with temp_cwd () as cwd :
885+ filename = os .path .join (cwd , "not_registered.py" )
886+ exec_namespace ({"inspect" : inspect }, filename , None )
887+
888+ def test_getmodule_skips_unchanged_module_rescan (self ):
889+ with temp_cwd () as cwd :
890+ filename = os .path .join (cwd , "not_registered.py" )
891+ namespace = {"inspect" : inspect }
892+ exec (compile ("frame = inspect.currentframe()" , filename , "exec" ),
893+ namespace )
894+ frame = namespace ["frame" ]
895+ modules = sys .modules .copy ()
896+ module_name = f"{ __name__ } .late_registered"
897+ marker_name = f"{ __name__ } .scan_marker"
898+ marker = types .ModuleType (marker_name )
899+ marker .__file__ = os .path .join (cwd , "scan_marker.py" )
900+ with open (marker .__file__ , "w" ):
901+ pass
902+ modules [marker_name ] = marker
903+
904+ original_ismodule = inspect .ismodule
905+ scan_count = 0
906+
907+ def ismodule (object ):
908+ nonlocal scan_count
909+ if object is marker :
910+ scan_count += 1
911+ return original_ismodule (object )
912+
913+ with (unittest .mock .patch .object (sys , "modules" , modules ),
914+ unittest .mock .patch .object (inspect , "modulesbyfile" , {}),
915+ unittest .mock .patch .object (inspect , "_filesbymodname" , {}),
916+ unittest .mock .patch .object (inspect , "ismodule" , ismodule ),
917+ unittest .mock .patch .object (
918+ inspect , "_modulesbyfile_snapshot" , None )):
919+ self .assertIsNone (inspect .getmodule (frame , filename ))
920+ first_scan_count = scan_count
921+ self .assertGreater (first_scan_count , 0 )
922+ self .assertIsNone (inspect .getmodule (frame , filename ))
923+ self .assertEqual (scan_count , first_scan_count )
924+
925+ module = types .ModuleType (module_name )
926+ module .__file__ = filename
927+ modules [module_name ] = module
928+ self .assertIs (inspect .getmodule (frame .f_code ), module )
929+ self .assertGreater (scan_count , first_scan_count )
930+ second_scan_count = scan_count
931+
932+ # A replacement sys.modules object must invalidate the
933+ # snapshot even when it contains the same entries.
934+ replacement = modules .copy ()
935+ unknown = os .path .join (cwd , "unknown.py" )
936+ with unittest .mock .patch .object (sys , "modules" , replacement ):
937+ self .assertIsNone (inspect .getmodule (None , unknown ))
938+ self .assertGreater (scan_count , second_scan_count )
939+
940+ def test_getmodule_sys_modules_changes_during_key_snapshot (self ):
941+ class MutatingModules (dict ):
942+ mutate = True
943+
944+ def __iter__ (self ):
945+ iterator = super ().__iter__ ()
946+ yield next (iterator )
947+ if self .mutate :
948+ self .mutate = False
949+ self [f"{ __name__ } .added_during_iteration" ] = None
950+ yield from iterator
951+
952+ modules = MutatingModules (sys .modules )
953+ with (temp_cwd () as cwd ,
954+ unittest .mock .patch .object (sys , "modules" , modules ),
955+ unittest .mock .patch .object (inspect , "modulesbyfile" , {}),
956+ unittest .mock .patch .object (inspect , "_filesbymodname" , {}),
957+ unittest .mock .patch .object (
958+ inspect , "_modulesbyfile_snapshot" , None )):
959+ filename = os .path .join (cwd , "not_registered.py" )
960+ self .assertIsNone (inspect .getmodule (None , filename ))
961+
962+ def test_getmodule_snapshot_does_not_retain_modules (self ):
963+ with temp_cwd () as cwd :
964+ module_name = f"{ __name__ } .snapshot_module"
965+ module = types .ModuleType (module_name )
966+ module .__file__ = os .path .join (cwd , "snapshot_module.py" )
967+ with open (module .__file__ , "w" ):
968+ pass
969+ module_ref = weakref .ref (module )
970+ modules = sys .modules .copy ()
971+ modules [module_name ] = module
972+
973+ with (unittest .mock .patch .object (sys , "modules" , modules ),
974+ unittest .mock .patch .object (inspect , "modulesbyfile" , {}),
975+ unittest .mock .patch .object (inspect , "_filesbymodname" , {}),
976+ unittest .mock .patch .object (
977+ inspect , "_modulesbyfile_snapshot" , None )):
978+ self .assertIs (inspect .getmodule (None , module .__file__ ), module )
979+ del modules [module_name ]
980+ del module
981+ support .gc_collect ()
982+ self .assertIsNone (module_ref ())
873983
874984 def test_getmodule_registered_exec_frame (self ):
875985 def exec_module (module , filename ):
@@ -902,8 +1012,24 @@ def exec_module(module, filename):
9021012 self .assertIsNone (inspect .getmodule (module .frame ))
9031013 self .assertIsNone (inspect .getmodule (module .traceback ))
9041014
905- # Preserve the existing result for fileless modules while
906- # avoiding a scan of sys.modules.
1015+ # If another module supplied the source, fall back to the
1016+ # filename-based lookup instead of trusting frame globals.
1017+ source_name = f"{ module_name } .source"
1018+ source_module = types .ModuleType (source_name )
1019+ source_module .__file__ = filename + ".source"
1020+ with open (source_module .__file__ , "w" ):
1021+ pass
1022+ sys .modules [source_name ] = source_module
1023+ try :
1024+ exec_module (module , source_module .__file__ )
1025+ self .assertIs (inspect .getmodule (module .frame ),
1026+ source_module )
1027+ self .assertIs (inspect .getmodule (module .traceback ),
1028+ source_module )
1029+ finally :
1030+ del sys .modules [source_name ]
1031+
1032+ # Preserve the existing result for fileless modules.
9071033 del module .__file__
9081034 exec_module (module , "<fileless>" )
9091035 self .assertIsNone (inspect .getmodule (module .frame ))
0 commit comments