Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-128911: Add tests on the PyImport C API #128915

Merged
merged 10 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions Lib/test/test_capi/test_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import importlib.util
import os.path
import sys
import types
import unittest
from test.support import import_helper
from test.support.warnings_helper import check_warnings

_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
NULL = None


class ImportTests(unittest.TestCase):
def test_getmagicnumber(self):
# Test PyImport_GetMagicNumber()
magic = _testlimitedcapi.PyImport_GetMagicNumber()
self.assertEqual(magic,
int.from_bytes(importlib.util.MAGIC_NUMBER, 'little'))

def test_getmagictag(self):
# Test PyImport_GetMagicTag()
tag = _testlimitedcapi.PyImport_GetMagicTag()
self.assertEqual(tag, sys.implementation.cache_tag)

def test_getmoduledict(self):
# Test PyImport_GetModuleDict()
modules = _testlimitedcapi.PyImport_GetModuleDict()
self.assertIs(modules, sys.modules)

def check_import_loaded_module(self, import_module):
for name in ('os', 'sys', 'test', 'unittest'):
with self.subTest(name=name):
self.assertIn(name, sys.modules)
module = import_module(name)
self.assertIsInstance(module, types.ModuleType)
self.assertIs(module, sys.modules[name])

def check_import_fresh_module(self, import_module):
old_modules = dict(sys.modules)
try:
for name in ('colorsys', 'math'):
with self.subTest(name=name):
sys.modules.pop(name, None)
module = import_module(name)
self.assertIsInstance(module, types.ModuleType)
self.assertIs(module, sys.modules[name])
self.assertEqual(module.__name__, name)
finally:
sys.modules.clear()
sys.modules.update(old_modules)

def test_getmodule(self):
# Test PyImport_GetModule()
self.check_import_loaded_module(_testlimitedcapi.PyImport_GetModule)

nonexistent = 'nonexistent'
self.assertNotIn(nonexistent, sys.modules)
for name in (nonexistent, '', object()):
with self.subTest(name=name):
with self.assertRaises(KeyError):
_testlimitedcapi.PyImport_GetModule(name)

# CRASHES PyImport_GetModule(NULL)

def check_addmodule(self, add_module, accept_nonstr=False):
# create a new module
names = ['nonexistent']
if accept_nonstr:
# PyImport_AddModuleObject() accepts non-string names
names.append(object())
for name in names:
with self.subTest(name=name):
self.assertNotIn(name, sys.modules)
try:
module = add_module(name)
self.assertIsInstance(module, types.ModuleType)
self.assertEqual(module.__name__, name)
self.assertIs(module, sys.modules[name])
finally:
sys.modules.pop(name, None)

# get an existing module
self.check_import_loaded_module(add_module)

def test_addmoduleobject(self):
# Test PyImport_AddModuleObject()
self.check_addmodule(_testlimitedcapi.PyImport_AddModuleObject,
accept_nonstr=True)

# CRASHES PyImport_AddModuleObject(NULL)

def test_addmodule(self):
# Test PyImport_AddModule()
self.check_addmodule(_testlimitedcapi.PyImport_AddModule)

# CRASHES PyImport_AddModule(NULL)

def test_addmoduleref(self):
# Test PyImport_AddModuleRef()
self.check_addmodule(_testlimitedcapi.PyImport_AddModuleRef)

# CRASHES PyImport_AddModuleRef(NULL)

def check_import_func(self, import_module):
self.check_import_loaded_module(import_module)
self.check_import_fresh_module(import_module)

# Invalid module name types
with self.assertRaises(TypeError):
import_module(123)
with self.assertRaises(TypeError):
import_module(object())

def test_import(self):
# Test PyImport_Import()
self.check_import_func(_testlimitedcapi.PyImport_Import)

with self.assertRaises(SystemError):
_testlimitedcapi.PyImport_Import(NULL)

def test_importmodule(self):
# Test PyImport_ImportModule()
self.check_import_func(_testlimitedcapi.PyImport_ImportModule)

# CRASHES PyImport_ImportModule(NULL)

def test_importmodulenoblock(self):
# Test deprecated PyImport_ImportModuleNoBlock()
with check_warnings(('', DeprecationWarning)):
self.check_import_func(_testlimitedcapi.PyImport_ImportModuleNoBlock)

# CRASHES PyImport_ImportModuleNoBlock(NULL)

def check_frozen_import(self, import_frozen_module):
# Importing a frozen module executes its code, so start by unloading
# the module to execute the code in a new (temporary) module.
old_zipimport = sys.modules.pop('zipimport')
try:
self.assertEqual(import_frozen_module('zipimport'), 1)
finally:
sys.modules['zipimport'] = old_zipimport

# not a frozen module
self.assertEqual(import_frozen_module('sys'), 0)

def test_importfrozenmodule(self):
# Test PyImport_ImportFrozenModule()
self.check_frozen_import(_testlimitedcapi.PyImport_ImportFrozenModule)

# CRASHES PyImport_ImportFrozenModule(NULL)

def test_importfrozenmoduleobject(self):
# Test PyImport_ImportFrozenModuleObject()
PyImport_ImportFrozenModuleObject = _testlimitedcapi.PyImport_ImportFrozenModuleObject
self.check_frozen_import(PyImport_ImportFrozenModuleObject)

# Bad name is treated as "not found"
self.assertEqual(PyImport_ImportFrozenModuleObject(None), 0)

def test_importmoduleex(self):
# Test PyImport_ImportModuleEx()
def import_module(name):
return _testlimitedcapi.PyImport_ImportModuleEx(
name, globals(), {}, [])

self.check_import_func(import_module)

def test_importmodulelevel(self):
# Test PyImport_ImportModuleLevel()
def import_module(name):
return _testlimitedcapi.PyImport_ImportModuleLevel(
name, globals(), {}, [], 0)

self.check_import_func(import_module)

def test_importmodulelevelobject(self):
# Test PyImport_ImportModuleLevelObject()
def import_module(name):
return _testlimitedcapi.PyImport_ImportModuleLevelObject(
name, globals(), {}, [], 0)

self.check_import_func(import_module)

def check_executecodemodule(self, execute_code, pathname=None):
name = 'test_import_executecode'
try:
# Create a temporary module where the code will be executed
self.assertNotIn(name, sys.modules)
module = _testlimitedcapi.PyImport_AddModuleRef(name)
self.assertNotHasAttr(module, 'attr')

# Execute the code
if pathname is not None:
code_filename = pathname
else:
code_filename = '<string>'
code = compile('attr = 1', code_filename, 'exec')
module2 = execute_code(name, code)
self.assertIs(module2, module)

# Check the function side effects
self.assertEqual(module.attr, 1)
if pathname is not None:
self.assertEqual(module.__spec__.origin, pathname)
finally:
sys.modules.pop(name, None)

def test_executecodemodule(self):
# Test PyImport_ExecCodeModule()
self.check_executecodemodule(_testlimitedcapi.PyImport_ExecCodeModule)

def test_executecodemoduleex(self):
# Test PyImport_ExecCodeModuleEx()

# Test NULL path (it should not crash)
def execute_code1(name, code):
return _testlimitedcapi.PyImport_ExecCodeModuleEx(name, code,
NULL)
self.check_executecodemodule(execute_code1)

# Test non-NULL path
pathname = os.path.abspath('pathname')
def execute_code2(name, code):
return _testlimitedcapi.PyImport_ExecCodeModuleEx(name, code,
pathname)
self.check_executecodemodule(execute_code2, pathname)

def check_executecode_pathnames(self, execute_code_func):
# Test non-NULL pathname and NULL cpathname
pathname = os.path.abspath('pathname')

def execute_code1(name, code):
return execute_code_func(name, code, pathname, NULL)
self.check_executecodemodule(execute_code1, pathname)

# Test NULL pathname and non-NULL cpathname
pyc_filename = importlib.util.cache_from_source(__file__)
py_filename = importlib.util.source_from_cache(pyc_filename)

def execute_code2(name, code):
return execute_code_func(name, code, NULL, pyc_filename)
self.check_executecodemodule(execute_code2, py_filename)

def test_executecodemodulewithpathnames(self):
# Test PyImport_ExecCodeModuleWithPathnames()
self.check_executecode_pathnames(_testlimitedcapi.PyImport_ExecCodeModuleWithPathnames)

def test_executecodemoduleobject(self):
# Test PyImport_ExecCodeModuleObject()
self.check_executecode_pathnames(_testlimitedcapi.PyImport_ExecCodeModuleObject)

# TODO: test PyImport_GetImporter()
# TODO: test PyImport_ReloadModule()
# TODO: test PyImport_ExtendInittab()
# PyImport_AppendInittab() is tested by test_embed


if __name__ == "__main__":
unittest.main()
24 changes: 0 additions & 24 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3311,30 +3311,6 @@ def test_basic_multiple_interpreters_reset_each(self):
# * module's global state was initialized, not reset


@cpython_only
class CAPITests(unittest.TestCase):
def test_pyimport_addmodule(self):
# gh-105922: Test PyImport_AddModuleRef(), PyImport_AddModule()
# and PyImport_AddModuleObject()
_testcapi = import_module("_testcapi")
for name in (
'sys', # frozen module
'test', # package
__name__, # package.module
):
_testcapi.check_pyimport_addmodule(name)

def test_pyimport_addmodule_create(self):
# gh-105922: Test PyImport_AddModuleRef(), create a new module
_testcapi = import_module("_testcapi")
name = 'dontexist'
self.assertNotIn(name, sys.modules)
self.addCleanup(unload, name)

mod = _testcapi.check_pyimport_addmodule(name)
self.assertIs(mod, sys.modules[name])


@cpython_only
class TestMagicNumber(unittest.TestCase):
def test_magic_number_endianness(self):
Expand Down
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
@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
@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
@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
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c

Expand Down
47 changes: 0 additions & 47 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3059,52 +3059,6 @@ function_set_closure(PyObject *self, PyObject *args)
Py_RETURN_NONE;
}

static PyObject *
check_pyimport_addmodule(PyObject *self, PyObject *args)
{
const char *name;
if (!PyArg_ParseTuple(args, "s", &name)) {
return NULL;
}

// test PyImport_AddModuleRef()
PyObject *module = PyImport_AddModuleRef(name);
if (module == NULL) {
return NULL;
}
assert(PyModule_Check(module));
// module is a strong reference

// test PyImport_AddModule()
PyObject *module2 = PyImport_AddModule(name);
if (module2 == NULL) {
goto error;
}
assert(PyModule_Check(module2));
assert(module2 == module);
// module2 is a borrowed ref

// test PyImport_AddModuleObject()
PyObject *name_obj = PyUnicode_FromString(name);
if (name_obj == NULL) {
goto error;
}
PyObject *module3 = PyImport_AddModuleObject(name_obj);
Py_DECREF(name_obj);
if (module3 == NULL) {
goto error;
}
assert(PyModule_Check(module3));
assert(module3 == module);
// module3 is a borrowed ref

return module;

error:
Py_DECREF(module);
return NULL;
}


static PyObject *
test_weakref_capi(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
Expand Down Expand Up @@ -3570,7 +3524,6 @@ static PyMethodDef TestMethods[] = {
{"function_set_kw_defaults", function_set_kw_defaults, METH_VARARGS, NULL},
{"function_get_closure", function_get_closure, METH_O, NULL},
{"function_set_closure", function_set_closure, METH_VARARGS, NULL},
{"check_pyimport_addmodule", check_pyimport_addmodule, METH_VARARGS},
{"test_weakref_capi", test_weakref_capi, METH_NOARGS},
{"function_set_warning", function_set_warning, METH_NOARGS},
{"test_critical_sections", test_critical_sections, METH_NOARGS},
Expand Down
3 changes: 3 additions & 0 deletions Modules/_testlimitedcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ PyInit__testlimitedcapi(void)
if (_PyTestLimitedCAPI_Init_HeaptypeRelative(mod) < 0) {
return NULL;
}
if (_PyTestLimitedCAPI_Init_Import(mod) < 0) {
return NULL;
}
if (_PyTestLimitedCAPI_Init_List(mod) < 0) {
return NULL;
}
Expand Down
Loading
Loading