Skip to content

Commit 096d009

Browse files
gh-101758: Add a Test For Single-Phase Init Module Variants (gh-101891)
The new test exercises the most important variants for single-phase init extension modules. We also add some explanation about those variants to import.c. #101758
1 parent 81e3aa8 commit 096d009

File tree

3 files changed

+660
-38
lines changed

3 files changed

+660
-38
lines changed

Lib/test/test_imp.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)