Skip to content

Commit d95ba9f

Browse files
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]>
1 parent b5558cd commit d95ba9f

File tree

9 files changed

+640
-72
lines changed

9 files changed

+640
-72
lines changed

Lib/test/test_capi/test_import.py

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

Lib/test/test_import/__init__.py

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

33133313

3314-
@cpython_only
3315-
class CAPITests(unittest.TestCase):
3316-
def test_pyimport_addmodule(self):
3317-
# gh-105922: Test PyImport_AddModuleRef(), PyImport_AddModule()
3318-
# and PyImport_AddModuleObject()
3319-
_testcapi = import_module("_testcapi")
3320-
for name in (
3321-
'sys', # frozen module
3322-
'test', # package
3323-
__name__, # package.module
3324-
):
3325-
_testcapi.check_pyimport_addmodule(name)
3326-
3327-
def test_pyimport_addmodule_create(self):
3328-
# gh-105922: Test PyImport_AddModuleRef(), create a new module
3329-
_testcapi = import_module("_testcapi")
3330-
name = 'dontexist'
3331-
self.assertNotIn(name, sys.modules)
3332-
self.addCleanup(unload, name)
3333-
3334-
mod = _testcapi.check_pyimport_addmodule(name)
3335-
self.assertIs(mod, sys.modules[name])
3336-
3337-
33383314
@cpython_only
33393315
class TestMagicNumber(unittest.TestCase):
33403316
def test_magic_number_endianness(self):

Modules/Setup.stdlib.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@
163163
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
164164
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
165165
@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 _testcapi/config.c
166-
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.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 _testlimitedcapi/version.c
166+
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.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 _testlimitedcapi/version.c
167167
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
168168
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
169169

0 commit comments

Comments
 (0)