Skip to content

Commit 34ded1a

Browse files
[3.13] gh-128911: Add tests on the PyImport C API (#128915) (#128960)
gh-128911: Add tests on the PyImport C API (#128915) * 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. Co-authored-by: Serhiy Storchaka <[email protected]> (cherry picked from commit d95ba9f)
1 parent dc77f19 commit 34ded1a

File tree

9 files changed

+635
-72
lines changed

9 files changed

+635
-72
lines changed

Lib/test/test_capi/test_import.py

+322
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
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+
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
11+
NULL = None
12+
13+
14+
class ImportTests(unittest.TestCase):
15+
def test_getmagicnumber(self):
16+
# Test PyImport_GetMagicNumber()
17+
magic = _testlimitedcapi.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 = _testlimitedcapi.PyImport_GetMagicTag()
24+
self.assertEqual(tag, sys.implementation.cache_tag)
25+
26+
def test_getmoduledict(self):
27+
# Test PyImport_GetModuleDict()
28+
modules = _testlimitedcapi.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 = _testlimitedcapi.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 = _testlimitedcapi.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 = _testlimitedcapi.PyImport_AddModule
98+
self.check_addmodule(addmodule)
99+
100+
self.assertRaises(UnicodeDecodeError, addmodule, b'\xff')
101+
# CRASHES addmodule(NULL)
102+
103+
def test_addmoduleref(self):
104+
# Test PyImport_AddModuleRef()
105+
addmoduleref = _testlimitedcapi.PyImport_AddModuleRef
106+
self.check_addmodule(addmoduleref)
107+
108+
self.assertRaises(UnicodeDecodeError, addmoduleref, b'\xff')
109+
# CRASHES addmoduleref(NULL)
110+
111+
def check_import_func(self, import_module):
112+
self.check_import_loaded_module(import_module)
113+
self.check_import_fresh_module(import_module)
114+
self.assertRaises(ModuleNotFoundError, import_module, 'nonexistent')
115+
self.assertRaises(ValueError, import_module, '')
116+
117+
def test_import(self):
118+
# Test PyImport_Import()
119+
import_ = _testlimitedcapi.PyImport_Import
120+
self.check_import_func(import_)
121+
122+
self.assertRaises(TypeError, import_, b'os')
123+
self.assertRaises(SystemError, import_, NULL)
124+
125+
def test_importmodule(self):
126+
# Test PyImport_ImportModule()
127+
importmodule = _testlimitedcapi.PyImport_ImportModule
128+
self.check_import_func(importmodule)
129+
130+
self.assertRaises(UnicodeDecodeError, importmodule, b'\xff')
131+
# CRASHES importmodule(NULL)
132+
133+
def test_importmodulenoblock(self):
134+
# Test deprecated PyImport_ImportModuleNoBlock()
135+
importmodulenoblock = _testlimitedcapi.PyImport_ImportModuleNoBlock
136+
with check_warnings(('', DeprecationWarning)):
137+
self.check_import_func(importmodulenoblock)
138+
self.assertRaises(UnicodeDecodeError, importmodulenoblock, b'\xff')
139+
140+
# CRASHES importmodulenoblock(NULL)
141+
142+
def check_frozen_import(self, import_frozen_module):
143+
# Importing a frozen module executes its code, so start by unloading
144+
# the module to execute the code in a new (temporary) module.
145+
old_zipimport = sys.modules.pop('zipimport')
146+
try:
147+
self.assertEqual(import_frozen_module('zipimport'), 1)
148+
149+
# import zipimport again
150+
self.assertEqual(import_frozen_module('zipimport'), 1)
151+
finally:
152+
sys.modules['zipimport'] = old_zipimport
153+
154+
# not a frozen module
155+
self.assertEqual(import_frozen_module('sys'), 0)
156+
self.assertEqual(import_frozen_module('nonexistent'), 0)
157+
self.assertEqual(import_frozen_module(''), 0)
158+
159+
def test_importfrozenmodule(self):
160+
# Test PyImport_ImportFrozenModule()
161+
importfrozenmodule = _testlimitedcapi.PyImport_ImportFrozenModule
162+
self.check_frozen_import(importfrozenmodule)
163+
164+
self.assertRaises(UnicodeDecodeError, importfrozenmodule, b'\xff')
165+
# CRASHES importfrozenmodule(NULL)
166+
167+
def test_importfrozenmoduleobject(self):
168+
# Test PyImport_ImportFrozenModuleObject()
169+
importfrozenmoduleobject = _testlimitedcapi.PyImport_ImportFrozenModuleObject
170+
self.check_frozen_import(importfrozenmoduleobject)
171+
self.assertEqual(importfrozenmoduleobject(b'zipimport'), 0)
172+
self.assertEqual(importfrozenmoduleobject(NULL), 0)
173+
174+
def test_importmoduleex(self):
175+
# Test PyImport_ImportModuleEx()
176+
importmoduleex = _testlimitedcapi.PyImport_ImportModuleEx
177+
self.check_import_func(lambda name: importmoduleex(name, NULL, NULL, NULL))
178+
179+
self.assertRaises(ModuleNotFoundError, importmoduleex, 'nonexistent', NULL, NULL, NULL)
180+
self.assertRaises(ValueError, importmoduleex, '', NULL, NULL, NULL)
181+
self.assertRaises(UnicodeDecodeError, importmoduleex, b'\xff', NULL, NULL, NULL)
182+
# CRASHES importmoduleex(NULL, NULL, NULL, NULL)
183+
184+
def check_importmodulelevel(self, importmodulelevel):
185+
self.check_import_func(lambda name: importmodulelevel(name, NULL, NULL, NULL, 0))
186+
187+
self.assertRaises(ModuleNotFoundError, importmodulelevel, 'nonexistent', NULL, NULL, NULL, 0)
188+
self.assertRaises(ValueError, importmodulelevel, '', NULL, NULL, NULL, 0)
189+
190+
if __package__:
191+
self.assertIs(importmodulelevel('test_import', globals(), NULL, NULL, 1),
192+
sys.modules['test.test_capi.test_import'])
193+
self.assertIs(importmodulelevel('test_capi', globals(), NULL, NULL, 2),
194+
sys.modules['test.test_capi'])
195+
self.assertRaises(ValueError, importmodulelevel, 'os', NULL, NULL, NULL, -1)
196+
with self.assertWarns(ImportWarning):
197+
self.assertRaises(KeyError, importmodulelevel, 'test_import', {}, NULL, NULL, 1)
198+
self.assertRaises(TypeError, importmodulelevel, 'test_import', [], NULL, NULL, 1)
199+
200+
def test_importmodulelevel(self):
201+
# Test PyImport_ImportModuleLevel()
202+
importmodulelevel = _testlimitedcapi.PyImport_ImportModuleLevel
203+
self.check_importmodulelevel(importmodulelevel)
204+
205+
self.assertRaises(UnicodeDecodeError, importmodulelevel, b'\xff', NULL, NULL, NULL, 0)
206+
# CRASHES importmodulelevel(NULL, NULL, NULL, NULL, 0)
207+
208+
def test_importmodulelevelobject(self):
209+
# Test PyImport_ImportModuleLevelObject()
210+
importmodulelevel = _testlimitedcapi.PyImport_ImportModuleLevelObject
211+
self.check_importmodulelevel(importmodulelevel)
212+
213+
self.assertRaises(TypeError, importmodulelevel, b'os', NULL, NULL, NULL, 0)
214+
self.assertRaises(ValueError, importmodulelevel, NULL, NULL, NULL, NULL, 0)
215+
216+
def check_executecodemodule(self, execute_code, *args):
217+
name = 'test_import_executecode'
218+
try:
219+
# Create a temporary module where the code will be executed
220+
self.assertNotIn(name, sys.modules)
221+
module = _testlimitedcapi.PyImport_AddModuleRef(name)
222+
self.assertFalse(hasattr(module, 'attr'))
223+
224+
# Execute the code
225+
code = compile('attr = 1', '<test>', 'exec')
226+
module2 = execute_code(name, code, *args)
227+
self.assertIs(module2, module)
228+
229+
# Check the function side effects
230+
self.assertEqual(module.attr, 1)
231+
finally:
232+
sys.modules.pop(name, None)
233+
return module.__spec__.origin
234+
235+
def test_executecodemodule(self):
236+
# Test PyImport_ExecCodeModule()
237+
execcodemodule = _testlimitedcapi.PyImport_ExecCodeModule
238+
self.check_executecodemodule(execcodemodule)
239+
240+
code = compile('attr = 1', '<test>', 'exec')
241+
self.assertRaises(UnicodeDecodeError, execcodemodule, b'\xff', code)
242+
# CRASHES execcodemodule(NULL, code)
243+
# CRASHES execcodemodule(name, NULL)
244+
245+
def test_executecodemoduleex(self):
246+
# Test PyImport_ExecCodeModuleEx()
247+
execcodemoduleex = _testlimitedcapi.PyImport_ExecCodeModuleEx
248+
249+
# Test NULL path (it should not crash)
250+
self.check_executecodemodule(execcodemoduleex, NULL)
251+
252+
# Test non-NULL path
253+
pathname = b'pathname'
254+
origin = self.check_executecodemodule(execcodemoduleex, pathname)
255+
self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
256+
257+
pathname = os_helper.TESTFN_UNDECODABLE
258+
if pathname:
259+
origin = self.check_executecodemodule(execcodemoduleex, pathname)
260+
self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
261+
262+
code = compile('attr = 1', '<test>', 'exec')
263+
self.assertRaises(UnicodeDecodeError, execcodemoduleex, b'\xff', code, NULL)
264+
# CRASHES execcodemoduleex(NULL, code, NULL)
265+
# CRASHES execcodemoduleex(name, NULL, NULL)
266+
267+
def check_executecode_pathnames(self, execute_code_func, object=False):
268+
# Test non-NULL pathname and NULL cpathname
269+
270+
# Test NULL paths (it should not crash)
271+
self.check_executecodemodule(execute_code_func, NULL, NULL)
272+
273+
pathname = 'pathname'
274+
origin = self.check_executecodemodule(execute_code_func, pathname, NULL)
275+
self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
276+
origin = self.check_executecodemodule(execute_code_func, NULL, pathname)
277+
if not object:
278+
self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
279+
280+
pathname = os_helper.TESTFN_UNDECODABLE
281+
if pathname:
282+
if object:
283+
pathname = os.fsdecode(pathname)
284+
origin = self.check_executecodemodule(execute_code_func, pathname, NULL)
285+
self.assertEqual(origin, os.path.abspath(os.fsdecode(pathname)))
286+
self.check_executecodemodule(execute_code_func, NULL, pathname)
287+
288+
# Test NULL pathname and non-NULL cpathname
289+
pyc_filename = importlib.util.cache_from_source(__file__)
290+
py_filename = importlib.util.source_from_cache(pyc_filename)
291+
origin = self.check_executecodemodule(execute_code_func, NULL, pyc_filename)
292+
if not object:
293+
self.assertEqual(origin, py_filename)
294+
295+
def test_executecodemodulewithpathnames(self):
296+
# Test PyImport_ExecCodeModuleWithPathnames()
297+
execute_code_func = _testlimitedcapi.PyImport_ExecCodeModuleWithPathnames
298+
self.check_executecode_pathnames(execute_code_func)
299+
300+
code = compile('attr = 1', '<test>', 'exec')
301+
self.assertRaises(UnicodeDecodeError, execute_code_func, b'\xff', code, NULL, NULL)
302+
# CRASHES execute_code_func(NULL, code, NULL, NULL)
303+
# CRASHES execute_code_func(name, NULL, NULL, NULL)
304+
305+
def test_executecodemoduleobject(self):
306+
# Test PyImport_ExecCodeModuleObject()
307+
execute_code_func = _testlimitedcapi.PyImport_ExecCodeModuleObject
308+
self.check_executecode_pathnames(execute_code_func, object=True)
309+
310+
code = compile('attr = 1', '<test>', 'exec')
311+
self.assertRaises(TypeError, execute_code_func, [], code, NULL, NULL)
312+
# CRASHES execute_code_func(NULL, code, NULL, NULL)
313+
# CRASHES execute_code_func(name, NULL, NULL, NULL)
314+
315+
# TODO: test PyImport_GetImporter()
316+
# TODO: test PyImport_ReloadModule()
317+
# TODO: test PyImport_ExtendInittab()
318+
# PyImport_AppendInittab() is tested by test_embed
319+
320+
321+
if __name__ == "__main__":
322+
unittest.main()

Lib/test/test_import/__init__.py

-24
Original file line numberDiff line numberDiff line change
@@ -3330,30 +3330,6 @@ def test_basic_multiple_interpreters_reset_each(self):
33303330
# * module's global state was initialized, not reset
33313331

33323332

3333-
@cpython_only
3334-
class CAPITests(unittest.TestCase):
3335-
def test_pyimport_addmodule(self):
3336-
# gh-105922: Test PyImport_AddModuleRef(), PyImport_AddModule()
3337-
# and PyImport_AddModuleObject()
3338-
_testcapi = import_module("_testcapi")
3339-
for name in (
3340-
'sys', # frozen module
3341-
'test', # package
3342-
__name__, # package.module
3343-
):
3344-
_testcapi.check_pyimport_addmodule(name)
3345-
3346-
def test_pyimport_addmodule_create(self):
3347-
# gh-105922: Test PyImport_AddModuleRef(), create a new module
3348-
_testcapi = import_module("_testcapi")
3349-
name = 'dontexist'
3350-
self.assertNotIn(name, sys.modules)
3351-
self.addCleanup(unload, name)
3352-
3353-
mod = _testcapi.check_pyimport_addmodule(name)
3354-
self.assertIs(mod, sys.modules[name])
3355-
3356-
33573333
if __name__ == '__main__':
33583334
# Test needs to be a package, so we can do relative imports.
33593335
unittest.main()

Modules/Setup.stdlib.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
165165
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
166166
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.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/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c
167-
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c
167+
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c
168168
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
169169
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
170170

0 commit comments

Comments
 (0)