@@ -251,6 +251,205 @@ def test_issue16421_multiple_modules_in_one_dll(self):
251251 with self .assertRaises (ImportError ):
252252 imp .load_dynamic ('nonexistent' , pathname )
253253
254+ @requires_load_dynamic
255+ def test_singlephase_variants (self ):
256+ '''Exercise the most meaningful variants described in Python/import.c.'''
257+ self .maxDiff = None
258+
259+ basename = '_testsinglephase'
260+ fileobj , pathname , _ = imp .find_module (basename )
261+ fileobj .close ()
262+
263+ modules = {}
264+ def load (name ):
265+ assert name not in modules
266+ module = imp .load_dynamic (name , pathname )
267+ self .assertNotIn (module , modules .values ())
268+ modules [name ] = module
269+ return module
270+
271+ def re_load (name , module ):
272+ assert sys .modules [name ] is module
273+ before = type (module )(module .__name__ )
274+ before .__dict__ .update (vars (module ))
275+
276+ reloaded = imp .load_dynamic (name , pathname )
277+
278+ return before , reloaded
279+
280+ def check_common (name , module ):
281+ summed = module .sum (1 , 2 )
282+ lookedup = module .look_up_self ()
283+ initialized = module .initialized ()
284+ cached = sys .modules [name ]
285+
286+ # module.__name__ might not match, but the spec will.
287+ self .assertEqual (module .__spec__ .name , name )
288+ if initialized is not None :
289+ self .assertIsInstance (initialized , float )
290+ self .assertGreater (initialized , 0 )
291+ self .assertEqual (summed , 3 )
292+ self .assertTrue (issubclass (module .error , Exception ))
293+ self .assertEqual (module .int_const , 1969 )
294+ self .assertEqual (module .str_const , 'something different' )
295+ self .assertIs (cached , module )
296+
297+ return lookedup , initialized , cached
298+
299+ def check_direct (name , module , lookedup ):
300+ # The module has its own PyModuleDef, with a matching name.
301+ self .assertEqual (module .__name__ , name )
302+ self .assertIs (lookedup , module )
303+
304+ def check_indirect (name , module , lookedup , orig ):
305+ # The module re-uses another's PyModuleDef, with a different name.
306+ assert orig is not module
307+ assert orig .__name__ != name
308+ self .assertNotEqual (module .__name__ , name )
309+ self .assertIs (lookedup , module )
310+
311+ def check_basic (module , initialized ):
312+ init_count = module .initialized_count ()
313+
314+ self .assertIsNot (initialized , None )
315+ self .assertIsInstance (init_count , int )
316+ self .assertGreater (init_count , 0 )
317+
318+ return init_count
319+
320+ def check_common_reloaded (name , module , cached , before , reloaded ):
321+ recached = sys .modules [name ]
322+
323+ self .assertEqual (reloaded .__spec__ .name , name )
324+ self .assertEqual (reloaded .__name__ , before .__name__ )
325+ self .assertEqual (before .__dict__ , module .__dict__ )
326+ self .assertIs (recached , reloaded )
327+
328+ def check_basic_reloaded (module , lookedup , initialized , init_count ,
329+ before , reloaded ):
330+ relookedup = reloaded .look_up_self ()
331+ reinitialized = reloaded .initialized ()
332+ reinit_count = reloaded .initialized_count ()
333+
334+ self .assertIs (reloaded , module )
335+ self .assertIs (reloaded .__dict__ , module .__dict__ )
336+ # It only happens to be the same but that's good enough here.
337+ # We really just want to verify that the re-loaded attrs
338+ # didn't change.
339+ self .assertIs (relookedup , lookedup )
340+ self .assertEqual (reinitialized , initialized )
341+ self .assertEqual (reinit_count , init_count )
342+
343+ def check_with_reinit_reloaded (module , lookedup , initialized ,
344+ before , reloaded ):
345+ relookedup = reloaded .look_up_self ()
346+ reinitialized = reloaded .initialized ()
347+
348+ self .assertIsNot (reloaded , module )
349+ self .assertIsNot (reloaded , module )
350+ self .assertNotEqual (reloaded .__dict__ , module .__dict__ )
351+ self .assertIs (relookedup , reloaded )
352+ if initialized is None :
353+ self .assertIs (reinitialized , None )
354+ else :
355+ self .assertGreater (reinitialized , initialized )
356+
357+ # Check the "basic" module.
358+
359+ name = basename
360+ expected_init_count = 1
361+ with self .subTest (name ):
362+ mod = load (name )
363+ lookedup , initialized , cached = check_common (name , mod )
364+ check_direct (name , mod , lookedup )
365+ init_count = check_basic (mod , initialized )
366+ self .assertEqual (init_count , expected_init_count )
367+
368+ before , reloaded = re_load (name , mod )
369+ check_common_reloaded (name , mod , cached , before , reloaded )
370+ check_basic_reloaded (mod , lookedup , initialized , init_count ,
371+ before , reloaded )
372+ basic = mod
373+
374+ # Check its indirect variants.
375+
376+ name = f'{ basename } _basic_wrapper'
377+ expected_init_count += 1
378+ with self .subTest (name ):
379+ mod = load (name )
380+ lookedup , initialized , cached = check_common (name , mod )
381+ check_indirect (name , mod , lookedup , basic )
382+ init_count = check_basic (mod , initialized )
383+ self .assertEqual (init_count , expected_init_count )
384+
385+ before , reloaded = re_load (name , mod )
386+ check_common_reloaded (name , mod , cached , before , reloaded )
387+ check_basic_reloaded (mod , lookedup , initialized , init_count ,
388+ before , reloaded )
389+
390+ # Currently _PyState_AddModule() always replaces the cached module.
391+ self .assertIs (basic .look_up_self (), mod )
392+ self .assertEqual (basic .initialized_count (), expected_init_count )
393+
394+ # The cached module shouldn't be changed after this point.
395+ basic_lookedup = mod
396+
397+ # Check its direct variant.
398+
399+ name = f'{ basename } _basic_copy'
400+ expected_init_count += 1
401+ with self .subTest (name ):
402+ mod = load (name )
403+ lookedup , initialized , cached = check_common (name , mod )
404+ check_direct (name , mod , lookedup )
405+ init_count = check_basic (mod , initialized )
406+ self .assertEqual (init_count , expected_init_count )
407+
408+ before , reloaded = re_load (name , mod )
409+ check_common_reloaded (name , mod , cached , before , reloaded )
410+ check_basic_reloaded (mod , lookedup , initialized , init_count ,
411+ before , reloaded )
412+
413+ # This should change the cached module for _testsinglephase.
414+ self .assertIs (basic .look_up_self (), basic_lookedup )
415+ self .assertEqual (basic .initialized_count (), expected_init_count )
416+
417+ # Check the non-basic variant that has no state.
418+
419+ name = f'{ basename } _with_reinit'
420+ with self .subTest (name ):
421+ mod = load (name )
422+ lookedup , initialized , cached = check_common (name , mod )
423+ self .assertIs (initialized , None )
424+ check_direct (name , mod , lookedup )
425+
426+ before , reloaded = re_load (name , mod )
427+ check_common_reloaded (name , mod , cached , before , reloaded )
428+ check_with_reinit_reloaded (mod , lookedup , initialized ,
429+ before , reloaded )
430+
431+ # This should change the cached module for _testsinglephase.
432+ self .assertIs (basic .look_up_self (), basic_lookedup )
433+ self .assertEqual (basic .initialized_count (), expected_init_count )
434+
435+ # Check the basic variant that has state.
436+
437+ name = f'{ basename } _with_state'
438+ with self .subTest (name ):
439+ mod = load (name )
440+ lookedup , initialized , cached = check_common (name , mod )
441+ self .assertIsNot (initialized , None )
442+ check_direct (name , mod , lookedup )
443+
444+ before , reloaded = re_load (name , mod )
445+ check_common_reloaded (name , mod , cached , before , reloaded )
446+ check_with_reinit_reloaded (mod , lookedup , initialized ,
447+ before , reloaded )
448+
449+ # This should change the cached module for _testsinglephase.
450+ self .assertIs (basic .look_up_self (), basic_lookedup )
451+ self .assertEqual (basic .initialized_count (), expected_init_count )
452+
254453 @requires_load_dynamic
255454 def test_load_dynamic_ImportError_path (self ):
256455 # Issue #1559549 added `name` and `path` attributes to ImportError
0 commit comments