mirror of
https://github.com/python/cpython.git
synced 2025-07-24 11:44:31 +00:00
PEP 489: Multi-phase extension module initialization
Known limitations of the current implementation: - documentation changes are incomplete - there's a reference leak I haven't tracked down yet The leak is most visible by running: ./python -m test -R3:3 test_importlib However, you can also see it by running: ./python -X showrefcount Importing the array or _testmultiphase modules, and then deleting them from both sys.modules and the local namespace shows significant increases in the total number of active references each cycle. By contrast, with _testcapi (which continues to use single-phase initialisation) the global refcounts stabilise after a couple of cycles.
This commit is contained in:
parent
ec219ba1c0
commit
d5cacbb1d9
34 changed files with 4462 additions and 3124 deletions
|
@ -7,6 +7,8 @@ import os.path
|
|||
import sys
|
||||
import types
|
||||
import unittest
|
||||
import importlib.util
|
||||
import importlib
|
||||
|
||||
|
||||
class LoaderTests(abc.LoaderTests):
|
||||
|
@ -80,6 +82,171 @@ class LoaderTests(abc.LoaderTests):
|
|||
Source_LoaderTests
|
||||
) = util.test_both(LoaderTests, machinery=machinery)
|
||||
|
||||
class MultiPhaseExtensionModuleTests(abc.LoaderTests):
|
||||
"""Test loading extension modules with multi-phase initialization (PEP 489)
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.name = '_testmultiphase'
|
||||
finder = self.machinery.FileFinder(None)
|
||||
self.spec = importlib.util.find_spec(self.name)
|
||||
assert self.spec
|
||||
self.loader = self.machinery.ExtensionFileLoader(
|
||||
self.name, self.spec.origin)
|
||||
|
||||
# No extension module as __init__ available for testing.
|
||||
test_package = None
|
||||
|
||||
# No extension module in a package available for testing.
|
||||
test_lacking_parent = None
|
||||
|
||||
# Handling failure on reload is the up to the module.
|
||||
test_state_after_failure = None
|
||||
|
||||
def test_module(self):
|
||||
'''Test loading an extension module'''
|
||||
with util.uncache(self.name):
|
||||
module = self.load_module()
|
||||
for attr, value in [('__name__', self.name),
|
||||
('__file__', self.spec.origin),
|
||||
('__package__', '')]:
|
||||
self.assertEqual(getattr(module, attr), value)
|
||||
with self.assertRaises(AttributeError):
|
||||
module.__path__
|
||||
self.assertIs(module, sys.modules[self.name])
|
||||
self.assertIsInstance(module.__loader__,
|
||||
self.machinery.ExtensionFileLoader)
|
||||
|
||||
def test_functionality(self):
|
||||
'''Test basic functionality of stuff defined in an extension module'''
|
||||
with util.uncache(self.name):
|
||||
module = self.load_module()
|
||||
self.assertIsInstance(module, types.ModuleType)
|
||||
ex = module.Example()
|
||||
self.assertEqual(ex.demo('abcd'), 'abcd')
|
||||
self.assertEqual(ex.demo(), None)
|
||||
with self.assertRaises(AttributeError):
|
||||
ex.abc
|
||||
ex.abc = 0
|
||||
self.assertEqual(ex.abc, 0)
|
||||
self.assertEqual(module.foo(9, 9), 18)
|
||||
self.assertIsInstance(module.Str(), str)
|
||||
self.assertEqual(module.Str(1) + '23', '123')
|
||||
with self.assertRaises(module.error):
|
||||
raise module.error()
|
||||
self.assertEqual(module.int_const, 1969)
|
||||
self.assertEqual(module.str_const, 'something different')
|
||||
|
||||
def test_reload(self):
|
||||
'''Test that reload didn't re-set the module's attributes'''
|
||||
with util.uncache(self.name):
|
||||
module = self.load_module()
|
||||
ex_class = module.Example
|
||||
importlib.reload(module)
|
||||
self.assertIs(ex_class, module.Example)
|
||||
|
||||
def test_try_registration(self):
|
||||
'''Assert that the PyState_{Find,Add,Remove}Module C API doesn't work'''
|
||||
module = self.load_module()
|
||||
with self.subTest('PyState_FindModule'):
|
||||
self.assertEqual(module.call_state_registration_func(0), None)
|
||||
with self.subTest('PyState_AddModule'):
|
||||
with self.assertRaises(SystemError):
|
||||
module.call_state_registration_func(1)
|
||||
with self.subTest('PyState_RemoveModule'):
|
||||
with self.assertRaises(SystemError):
|
||||
module.call_state_registration_func(2)
|
||||
|
||||
def load_module(self):
|
||||
'''Load the module from the test extension'''
|
||||
return self.loader.load_module(self.name)
|
||||
|
||||
def load_module_by_name(self, fullname):
|
||||
'''Load a module from the test extension by name'''
|
||||
origin = self.spec.origin
|
||||
loader = self.machinery.ExtensionFileLoader(fullname, origin)
|
||||
spec = importlib.util.spec_from_loader(fullname, loader)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
loader.exec_module(module)
|
||||
return module
|
||||
|
||||
def test_load_twice(self):
|
||||
'''Test that 2 loads result in 2 module objects'''
|
||||
module1 = self.load_module_by_name(self.name)
|
||||
module2 = self.load_module_by_name(self.name)
|
||||
self.assertIsNot(module1, module2)
|
||||
|
||||
def test_unloadable(self):
|
||||
'''Test nonexistent module'''
|
||||
name = 'asdfjkl;'
|
||||
with self.assertRaises(ImportError) as cm:
|
||||
self.load_module_by_name(name)
|
||||
self.assertEqual(cm.exception.name, name)
|
||||
|
||||
def test_unloadable_nonascii(self):
|
||||
'''Test behavior with nonexistent module with non-ASCII name'''
|
||||
name = 'fo\xf3'
|
||||
with self.assertRaises(ImportError) as cm:
|
||||
self.load_module_by_name(name)
|
||||
self.assertEqual(cm.exception.name, name)
|
||||
|
||||
def test_nonmodule(self):
|
||||
'''Test returning a non-module object from create works'''
|
||||
name = self.name + '_nonmodule'
|
||||
mod = self.load_module_by_name(name)
|
||||
self.assertNotEqual(type(mod), type(unittest))
|
||||
self.assertEqual(mod.three, 3)
|
||||
|
||||
def test_null_slots(self):
|
||||
'''Test that NULL slots aren't a problem'''
|
||||
name = self.name + '_null_slots'
|
||||
module = self.load_module_by_name(name)
|
||||
self.assertIsInstance(module, types.ModuleType)
|
||||
assert module.__name__ == name
|
||||
|
||||
def test_bad_modules(self):
|
||||
'''Test SystemError is raised for misbehaving extensions'''
|
||||
for name_base in [
|
||||
'bad_slot_large',
|
||||
'bad_slot_negative',
|
||||
'create_int_with_state',
|
||||
'negative_size',
|
||||
'export_null',
|
||||
'export_uninitialized',
|
||||
'export_raise',
|
||||
'export_unreported_exception',
|
||||
'create_null',
|
||||
'create_raise',
|
||||
'create_unreported_exception',
|
||||
'nonmodule_with_exec_slots',
|
||||
'exec_err',
|
||||
'exec_raise',
|
||||
'exec_unreported_exception',
|
||||
]:
|
||||
with self.subTest(name_base):
|
||||
name = self.name + '_' + name_base
|
||||
with self.assertRaises(SystemError):
|
||||
self.load_module_by_name(name)
|
||||
|
||||
def test_nonascii(self):
|
||||
'''Test that modules with non-ASCII names can be loaded'''
|
||||
# punycode behaves slightly differently in some-ASCII and no-ASCII
|
||||
# cases, so test both
|
||||
cases = [
|
||||
(self.name + '_zkou\u0161ka_na\u010dten\xed', 'Czech'),
|
||||
('\uff3f\u30a4\u30f3\u30dd\u30fc\u30c8\u30c6\u30b9\u30c8',
|
||||
'Japanese'),
|
||||
]
|
||||
for name, lang in cases:
|
||||
with self.subTest(name):
|
||||
module = self.load_module_by_name(name)
|
||||
self.assertEqual(module.__name__, name)
|
||||
self.assertEqual(module.__doc__, "Module named in %s" % lang)
|
||||
|
||||
|
||||
(Frozen_MultiPhaseExtensionModuleTests,
|
||||
Source_MultiPhaseExtensionModuleTests
|
||||
) = util.test_both(MultiPhaseExtensionModuleTests, machinery=machinery)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue