Skip to content

Commit 83de72e

Browse files
[3.12] gh-128911: Add tests on the PyImport C API (GH-128915) (GH-128960) (#128989)
* Add Modules/_testlimitedcapi/import.c * Add Lib/test/test_capi/test_import.py * Remove _testcapi.check_pyimport_addmodule(): tests already covered by newly added tests. (cherry picked from commit 34ded1a) Co-authored-by: Victor Stinner <[email protected]> Co-authored-by: Serhiy Storchaka <[email protected]> (cherry picked from commit d95ba9f)
1 parent 6d638c2 commit 83de72e

File tree

7 files changed

+609
-1
lines changed

7 files changed

+609
-1
lines changed

Lib/test/test_capi/test_import.py

+313
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
import importlib.util
2+
import os.path
3+
import sys
4+
import types
5+
import unittest
6+
from test.support import os_helper
7+
from test.support import import_helper
8+
from test.support.warnings_helper import check_warnings
9+
10+
_testcapi = import_helper.import_module('_testcapi')
11+
NULL = None
12+
13+
14+
class ImportTests(unittest.TestCase):
15+
def test_getmagicnumber(self):
16+
# Test PyImport_GetMagicNumber()
17+
magic = _testcapi.PyImport_GetMagicNumber()
18+
self.assertEqual(magic,
19+
int.from_bytes(importlib.util.MAGIC_NUMBER, 'little'))
20+
21+
def test_getmagictag(self):
22+
# Test PyImport_GetMagicTag()
23+
tag = _testcapi.PyImport_GetMagicTag()
24+
self.assertEqual(tag, sys.implementation.cache_tag)
25+
26+
def test_getmoduledict(self):
27+
# Test PyImport_GetModuleDict()
28+
modules = _testcapi.PyImport_GetModuleDict()
29+
self.assertIs(modules, sys.modules)
30+
31+
def check_import_loaded_module(self, import_module):
32+
for name in ('os', 'sys', 'test', 'unittest'):
33+
with self.subTest(name=name):
34+
self.assertIn(name, sys.modules)
35+
old_module = sys.modules[name]
36+
module = import_module(name)
37+
self.assertIsInstance(module, types.ModuleType)
38+
self.assertIs(module, old_module)
39+
40+
def check_import_fresh_module(self, import_module):
41+
old_modules = dict(sys.modules)
42+
try:
43+
for name in ('colorsys', 'math'):
44+
with self.subTest(name=name):
45+
sys.modules.pop(name, None)
46+
module = import_module(name)
47+
self.assertIsInstance(module, types.ModuleType)
48+
self.assertIs(module, sys.modules[name])
49+
self.assertEqual(module.__name__, name)
50+
finally:
51+
sys.modules.clear()
52+
sys.modules.update(old_modules)
53+
54+
def test_getmodule(self):
55+
# Test PyImport_GetModule()
56+
getmodule = _testcapi.PyImport_GetModule
57+
self.check_import_loaded_module(getmodule)
58+
59+
nonexistent = 'nonexistent'
60+
self.assertNotIn(nonexistent, sys.modules)
61+
self.assertIs(getmodule(nonexistent), KeyError)
62+
self.assertIs(getmodule(''), KeyError)
63+
self.assertIs(getmodule(object()), KeyError)
64+
65+
self.assertRaises(TypeError, getmodule, []) # unhashable
66+
# CRASHES getmodule(NULL)
67+
68+
def check_addmodule(self, add_module, accept_nonstr=False):
69+
# create a new module
70+
names = ['nonexistent']
71+
if accept_nonstr:
72+
names.append(b'\xff') # non-UTF-8
73+
for name in names:
74+
with self.subTest(name=name):
75+
self.assertNotIn(name, sys.modules)
76+
try:
77+
module = add_module(name)
78+
self.assertIsInstance(module, types.ModuleType)
79+
self.assertEqual(module.__name__, name)
80+
self.assertIs(module, sys.modules[name])
81+
finally:
82+
sys.modules.pop(name, None)
83+
84+
# get an existing module
85+
self.check_import_loaded_module(add_module)
86+
87+
def test_addmoduleobject(self):
88+
# Test PyImport_AddModuleObject()
89+
addmoduleobject = _testcapi.PyImport_AddModuleObject
90+
self.check_addmodule(addmoduleobject, accept_nonstr=True)
91+
92+
self.assertRaises(TypeError, addmoduleobject, []) # unhashable
93+
# CRASHES addmoduleobject(NULL)
94+
95+
def test_addmodule(self):
96+
# Test PyImport_AddModule()
97+
addmodule = _testcapi.PyImport_AddModule
98+
self.check_addmodule(addmodule)
99+
100+
self.assertRaises(UnicodeDecodeError, addmodule, b'\xff')
101+
# CRASHES addmodule(NULL)
102+
103+
def check_import_func(self, import_module):
104+
self.check_import_loaded_module(import_module)
105+
self.check_import_fresh_module(import_module)
106+
self.assertRaises(ModuleNotFoundError, import_module, 'nonexistent')
107+
self.assertRaises(ValueError, import_module, '')
108+
109+
def test_import(self):
110+
# Test PyImport_Import()
111+
import_ = _testcapi.PyImport_Import
112+
self.check_import_func(import_)
113+
114+
self.assertRaises(TypeError, import_, b'os')
115+
self.assertRaises(SystemError, import_, NULL)
116+
117+
def test_importmodule(self):
118+
# Test PyImport_ImportModule()
119+
importmodule = _testcapi.PyImport_ImportModule
120+
self.check_import_func(importmodule)
121+
122+
self.assertRaises(UnicodeDecodeError, importmodule, b'\xff')
123+
# CRASHES importmodule(NULL)
124+
125+
def test_importmodulenoblock(self):
126+
# Test deprecated PyImport_ImportModuleNoBlock()
127+
importmodulenoblock = _testcapi.PyImport_ImportModuleNoBlock
128+
self.check_import_func(importmodulenoblock)
129+
130+
self.assertRaises(UnicodeDecodeError, importmodulenoblock, b'\xff')
131+
# CRASHES importmodulenoblock(NULL)
132+
133+
def check_frozen_import(self, import_frozen_module):
134+
# Importing a frozen module executes its code, so start by unloading
135+
# the module to execute the code in a new (temporary) module.
136+
old_zipimport = sys.modules.pop('zipimport')
137+
try:
138+
self.assertEqual(import_frozen_module('zipimport'), 1)
139+
140+
# import zipimport again
141+
self.assertEqual(import_frozen_module('zipimport'), 1)
142+
finally:
143+
sys.modules['zipimport'] = old_zipimport
144+
145+
# not a frozen module
146+
self.assertEqual(import_frozen_module('sys'), 0)
147+
self.assertEqual(import_frozen_module('nonexistent'), 0)
148+
self.assertEqual(import_frozen_module(''), 0)
149+
150+
def test_importfrozenmodule(self):
151+
# Test PyImport_ImportFrozenModule()
152+
importfrozenmodule = _testcapi.PyImport_ImportFrozenModule
153+
self.check_frozen_import(importfrozenmodule)
154+
155+
self.assertRaises(UnicodeDecodeError, importfrozenmodule, b'\xff')
156+
# CRASHES importfrozenmodule(NULL)
157+
158+
def test_importfrozenmoduleobject(self):
159+
# Test PyImport_ImportFrozenModuleObject()
160+
importfrozenmoduleobject = _testcapi.PyImport_ImportFrozenModuleObject
161+
self.check_frozen_import(importfrozenmoduleobject)
162+
self.assertEqual(importfrozenmoduleobject(b'zipimport'), 0)
163+
self.assertEqual(importfrozenmoduleobject(NULL), 0)
164+
165+
def test_importmoduleex(self):
166+
# Test PyImport_ImportModuleEx()
167+
importmoduleex = _testcapi.PyImport_ImportModuleEx
168+
self.check_import_func(lambda name: importmoduleex(name, NULL, NULL, NULL))
169+
170+
self.assertRaises(ModuleNotFoundError, importmoduleex, 'nonexistent', NULL, NULL, NULL)
171+
self.assertRaises(ValueError, importmoduleex, '', NULL, NULL, NULL)
172+
self.assertRaises(UnicodeDecodeError, importmoduleex, b'\xff', NULL, NULL, NULL)
173+
# CRASHES importmoduleex(NULL, NULL, NULL, NULL)
174+
175+
def check_importmodulelevel(self, importmodulelevel):
176+
self.check_import_func(lambda name: importmodulelevel(name, NULL, NULL, NULL, 0))
177+
178+
self.assertRaises(ModuleNotFoundError, importmodulelevel, 'nonexistent', NULL, NULL, NULL, 0)
179+
self.assertRaises(ValueError, importmodulelevel, '', NULL, NULL, NULL, 0)
180+
181+
if __package__:
182+
self.assertIs(importmodulelevel('test_import', globals(), NULL, NULL, 1),
183+
sys.modules['test.test_capi.test_import'])
184+
self.assertIs(importmodulelevel('test_capi', globals(), NULL, NULL, 2),
185+
sys.modules['test.test_capi'])
186+
self.assertRaises(ValueError, importmodulelevel, 'os', NULL, NULL, NULL, -1)
187+
with self.assertWarns(ImportWarning):
188+
self.assertRaises(KeyError, importmodulelevel, 'test_import', {}, NULL, NULL, 1)
189+
self.assertRaises(TypeError, importmodulelevel, 'test_import', [], NULL, NULL, 1)
190+
191+
def test_importmodulelevel(self):
192+
# Test PyImport_ImportModuleLevel()
193+
importmodulelevel = _testcapi.PyImport_ImportModuleLevel
194+
self.check_importmodulelevel(importmodulelevel)
195+
196+
self.assertRaises(UnicodeDecodeError, importmodulelevel, b'\xff', NULL, NULL, NULL, 0)
197+
# CRASHES importmodulelevel(NULL, NULL, NULL, NULL, 0)
198+
199+
def test_importmodulelevelobject(self):
200+
# Test PyImport_ImportModuleLevelObject()
201+
importmodulelevel = _testcapi.PyImport_ImportModuleLevelObject
202+
self.check_importmodulelevel(importmodulelevel)
203+
204+
self.assertRaises(TypeError, importmodulelevel, b'os', NULL, NULL, NULL, 0)
205+
self.assertRaises(ValueError, importmodulelevel, NULL, NULL, NULL, NULL, 0)
206+
207+
def check_executecodemodule(self, execute_code, *args):
208+
name = 'test_import_executecode'
209+
try:
210+
# Create a temporary module where the code will be executed
211+
self.assertNotIn(name, sys.modules)
212+
module = _testcapi.PyImport_AddModule(name)
213+
self.assertFalse(hasattr(module, 'attr'))
214+
215+
# Execute the code
216+
code = compile('attr = 1', '<test>', 'exec')
217+
module2 = execute_code(name, code, *args)
218+
self.assertIs(module2, module)
219+
220+
# Check the function side effects
221+
self.assertEqual(module.attr, 1)
222+
finally:
223+
sys.modules.pop(name, None)
224+
return module.__spec__.origin
225+
226+
def test_executecodemodule(self):
227+
# Test PyImport_ExecCodeModule()
228+
execcodemodule = _testcapi.PyImport_ExecCodeModule
229+
self.check_executecodemodule(execcodemodule)
230+
231+
code = compile('attr = 1', '<test>', 'exec')
232+
self.assertRaises(UnicodeDecodeError, execcodemodule, b'\xff', code)
233+
# CRASHES execcodemodule(NULL, code)
234+
# CRASHES execcodemodule(name, NULL)
235+
236+
def test_executecodemoduleex(self):
237+
# Test PyImport_ExecCodeModuleEx()
238+
execcodemoduleex = _testcapi.PyImport_ExecCodeModuleEx
239+
240+
# Test NULL path (it should not crash)
241+
self.check_executecodemodule(execcodemoduleex, NULL)
242+
243+
# Test non-NULL path
244+
pathname = b'pathname'
245+
origin = self.check_executecodemodule(execcodemoduleex, pathname)
246+
self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
247+
248+
pathname = os_helper.TESTFN_UNDECODABLE
249+
if pathname:
250+
origin = self.check_executecodemodule(execcodemoduleex, pathname)
251+
self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
252+
253+
code = compile('attr = 1', '<test>', 'exec')
254+
self.assertRaises(UnicodeDecodeError, execcodemoduleex, b'\xff', code, NULL)
255+
# CRASHES execcodemoduleex(NULL, code, NULL)
256+
# CRASHES execcodemoduleex(name, NULL, NULL)
257+
258+
def check_executecode_pathnames(self, execute_code_func, object=False):
259+
# Test non-NULL pathname and NULL cpathname
260+
261+
# Test NULL paths (it should not crash)
262+
self.check_executecodemodule(execute_code_func, NULL, NULL)
263+
264+
pathname = 'pathname'
265+
origin = self.check_executecodemodule(execute_code_func, pathname, NULL)
266+
self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
267+
origin = self.check_executecodemodule(execute_code_func, NULL, pathname)
268+
if not object:
269+
self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
270+
271+
pathname = os_helper.TESTFN_UNDECODABLE
272+
if pathname:
273+
if object:
274+
pathname = os.fsdecode(pathname)
275+
origin = self.check_executecodemodule(execute_code_func, pathname, NULL)
276+
self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
277+
self.check_executecodemodule(execute_code_func, NULL, pathname)
278+
279+
# Test NULL pathname and non-NULL cpathname
280+
pyc_filename = importlib.util.cache_from_source(__file__)
281+
py_filename = importlib.util.source_from_cache(pyc_filename)
282+
origin = self.check_executecodemodule(execute_code_func, NULL, pyc_filename)
283+
if not object:
284+
self.assertEqual(origin, py_filename)
285+
286+
def test_executecodemodulewithpathnames(self):
287+
# Test PyImport_ExecCodeModuleWithPathnames()
288+
execute_code_func = _testcapi.PyImport_ExecCodeModuleWithPathnames
289+
self.check_executecode_pathnames(execute_code_func)
290+
291+
code = compile('attr = 1', '<test>', 'exec')
292+
self.assertRaises(UnicodeDecodeError, execute_code_func, b'\xff', code, NULL, NULL)
293+
# CRASHES execute_code_func(NULL, code, NULL, NULL)
294+
# CRASHES execute_code_func(name, NULL, NULL, NULL)
295+
296+
def test_executecodemoduleobject(self):
297+
# Test PyImport_ExecCodeModuleObject()
298+
execute_code_func = _testcapi.PyImport_ExecCodeModuleObject
299+
self.check_executecode_pathnames(execute_code_func, object=True)
300+
301+
code = compile('attr = 1', '<test>', 'exec')
302+
self.assertRaises(TypeError, execute_code_func, [], code, NULL, NULL)
303+
# CRASHES execute_code_func(NULL, code, NULL, NULL)
304+
# CRASHES execute_code_func(name, NULL, NULL, NULL)
305+
306+
# TODO: test PyImport_GetImporter()
307+
# TODO: test PyImport_ReloadModule()
308+
# TODO: test PyImport_ExtendInittab()
309+
# PyImport_AppendInittab() is tested by test_embed
310+
311+
312+
if __name__ == "__main__":
313+
unittest.main()

Modules/Setup.stdlib.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@
168168
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
169169
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
170170
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c
171-
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/bytearray.c _testcapi/bytes.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/pytime.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyos.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/heaptype_relative.c _testcapi/gc.c _testcapi/sys.c
171+
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/bytearray.c _testcapi/bytes.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/pytime.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyos.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/heaptype_relative.c _testcapi/gc.c _testcapi/sys.c _testcapi/import.c
172172
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
173173

174174
# Some testing modules MUST be built as shared libraries.

0 commit comments

Comments
 (0)