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
Prev Previous commit
Next Next commit
Add tests
Avoid the datetime module.
  • Loading branch information
vstinner committed Jan 17, 2025
commit 33b8507a941caff39ce329a33fdea0c019c8c9ca
157 changes: 136 additions & 21 deletions Lib/test/test_capi/test_import.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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):
Expand Down Expand Up @@ -36,7 +38,7 @@ def check_import_loaded_module(self, import_module):
def check_import_fresh_module(self, import_module):
old_modules = dict(sys.modules)
try:
for name in ('colorsys', 'datetime', 'math'):
for name in ('colorsys', 'math'):
with self.subTest(name=name):
sys.modules.pop(name, None)
module = import_module(name)
Expand All @@ -57,32 +59,43 @@ def test_getmodule(self):
self.assertIsNone(_testlimitedcapi.PyImport_GetModule(''))
self.assertIsNone(_testlimitedcapi.PyImport_GetModule(object()))

def check_addmodule(self, add_module):
def check_addmodule(self, add_module, accept_nonstr=False):
# create a new module
name = 'nonexistent'
self.assertNotIn(name, sys.modules)
try:
module = add_module(name)
self.assertIsInstance(module, types.ModuleType)
self.assertIs(module, sys.modules[name])
finally:
sys.modules.pop(name, None)
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)
self.check_addmodule(_testlimitedcapi.PyImport_AddModuleObject,
accept_nonstr=True)

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)
Expand All @@ -106,19 +119,121 @@ def test_importmodulenoblock(self):
with check_warnings(('', DeprecationWarning)):
self.check_import_func(_testlimitedcapi.PyImport_ImportModuleNoBlock)

# TODO: test PyImport_ExecCodeModule()
# TODO: test PyImport_ExecCodeModuleEx()
# TODO: test PyImport_ExecCodeModuleWithPathnames()
# TODO: test PyImport_ExecCodeModuleObject()
# TODO: test PyImport_ImportModuleLevel()
# TODO: test PyImport_ImportModuleLevelObject()
# TODO: test PyImport_ImportModuleEx()
def check_frozen_import(self, import_frozen_module):
# Importing a frozen module executes its code, so starts 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()
pathname = os.path.abspath('pathname')

def execute_code(name, code):
return _testlimitedcapi.PyImport_ExecCodeModuleEx(name, code,
pathname)
self.check_executecodemodule(execute_code, 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_ImportFrozenModuleObject()
# TODO: test PyImport_ImportFrozenModule()
# TODO: test PyImport_AppendInittab()
# TODO: test PyImport_ExtendInittab()
# PyImport_AppendInittab() is tested by test_embed


if __name__ == "__main__":
Expand Down
147 changes: 146 additions & 1 deletion Modules/_testlimitedcapi/import.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Need limited C API version 3.7 for PyImport_GetModule()
// Need limited C API version 3.13 for PyImport_AddModuleRef()
#include "pyconfig.h" // Py_GIL_DISABLED
#if !defined(Py_GIL_DISABLED) && !defined(Py_LIMITED_API)
# define Py_LIMITED_API 0x030d0000
Expand Down Expand Up @@ -118,6 +118,142 @@ pyimport_importmodulenoblock(PyObject *Py_UNUSED(module), PyObject *args)
}


/* Test PyImport_ImportModuleEx() */
static PyObject *
pyimport_importmoduleex(PyObject *Py_UNUSED(module), PyObject *args)
{
const char *name;
PyObject *globals, *locals, *fromlist;
if (!PyArg_ParseTuple(args, "sOOO",
&name, &globals, &locals, &fromlist)) {
vstinner marked this conversation as resolved.
Show resolved Hide resolved
return NULL;
}

return PyImport_ImportModuleEx(name, globals, locals, fromlist);
}


/* Test PyImport_ImportModuleLevel() */
static PyObject *
pyimport_importmodulelevel(PyObject *Py_UNUSED(module), PyObject *args)
{
const char *name;
PyObject *globals, *locals, *fromlist;
int level;
if (!PyArg_ParseTuple(args, "sOOOi",
&name, &globals, &locals, &fromlist, &level)) {
return NULL;
}

return PyImport_ImportModuleLevel(name, globals, locals, fromlist, level);
}


/* Test PyImport_ImportModuleLevelObject() */
static PyObject *
pyimport_importmodulelevelobject(PyObject *Py_UNUSED(module), PyObject *args)
{
PyObject *name, *globals, *locals, *fromlist;
int level;
if (!PyArg_ParseTuple(args, "OOOOi",
&name, &globals, &locals, &fromlist, &level)) {
return NULL;
}

return PyImport_ImportModuleLevelObject(name, globals, locals, fromlist, level);
}


/* Test PyImport_ImportFrozenModule() */
static PyObject *
pyimport_importfrozenmodule(PyObject *Py_UNUSED(module), PyObject *args)
{
const char *name;
if (!PyArg_ParseTuple(args, "s", &name)) {
return NULL;
}

int res = PyImport_ImportFrozenModule(name);
if (res < 0) {
return NULL;
}
return PyLong_FromLong(res);
}
vstinner marked this conversation as resolved.
Show resolved Hide resolved


/* Test PyImport_ImportFrozenModuleObject() */
static PyObject *
pyimport_importfrozenmoduleobject(PyObject *Py_UNUSED(module), PyObject *name)
{
int res = PyImport_ImportFrozenModuleObject(name);
if (res < 0) {
return NULL;
}
return PyLong_FromLong(res);
}


/* Test PyImport_ExecCodeModule() */
static PyObject *
pyimport_executecodemodule(PyObject *Py_UNUSED(module), PyObject *args)
{
const char *name;
PyObject *code;
if (!PyArg_ParseTuple(args, "sO", &name, &code)) {
return NULL;
}

return PyImport_ExecCodeModule(name, code);
}


/* Test PyImport_ExecCodeModuleEx() */
static PyObject *
pyimport_executecodemoduleex(PyObject *Py_UNUSED(module), PyObject *args)
{
const char *name;
PyObject *code;
const char *pathname;
if (!PyArg_ParseTuple(args, "sOs", &name, &code, &pathname)) {
return NULL;
}

return PyImport_ExecCodeModuleEx(name, code, pathname);
}


/* Test PyImport_ExecCodeModuleWithPathnames() */
static PyObject *
pyimport_executecodemodulewithpathnames(PyObject *Py_UNUSED(module), PyObject *args)
{
const char *name;
PyObject *code;
const char *pathname;
const char *cpathname;
if (!PyArg_ParseTuple(args, "sOzz", &name, &code, &pathname, &cpathname)) {
return NULL;
vstinner marked this conversation as resolved.
Show resolved Hide resolved
}

return PyImport_ExecCodeModuleWithPathnames(name, code,
pathname, cpathname);
}


/* Test PyImport_ExecCodeModuleObject() */
static PyObject *
pyimport_executecodemoduleobject(PyObject *Py_UNUSED(module), PyObject *args)
{
PyObject *name, *code, *pathname, *cpathname;
if (!PyArg_ParseTuple(args, "OOOO", &name, &code, &pathname, &cpathname)) {
return NULL;
}
NULLABLE(pathname);
NULLABLE(cpathname);

return PyImport_ExecCodeModuleObject(name, code, pathname, cpathname);
}


static PyMethodDef test_methods[] = {
{"PyImport_GetMagicNumber", pyimport_getmagicnumber, METH_NOARGS},
{"PyImport_GetMagicTag", pyimport_getmagictag, METH_NOARGS},
Expand All @@ -129,6 +265,15 @@ static PyMethodDef test_methods[] = {
{"PyImport_Import", pyimport_import, METH_O},
{"PyImport_ImportModule", pyimport_importmodule, METH_VARARGS},
{"PyImport_ImportModuleNoBlock", pyimport_importmodulenoblock, METH_VARARGS},
{"PyImport_ImportModuleEx", pyimport_importmoduleex, METH_VARARGS},
{"PyImport_ImportModuleLevel", pyimport_importmodulelevel, METH_VARARGS},
{"PyImport_ImportModuleLevelObject", pyimport_importmodulelevelobject, METH_VARARGS},
{"PyImport_ImportFrozenModule", pyimport_importfrozenmodule, METH_VARARGS},
{"PyImport_ImportFrozenModuleObject", pyimport_importfrozenmoduleobject, METH_O},
{"PyImport_ExecCodeModule", pyimport_executecodemodule, METH_VARARGS},
{"PyImport_ExecCodeModuleEx", pyimport_executecodemoduleex, METH_VARARGS},
{"PyImport_ExecCodeModuleWithPathnames", pyimport_executecodemodulewithpathnames, METH_VARARGS},
{"PyImport_ExecCodeModuleObject", pyimport_executecodemoduleobject, METH_VARARGS},
{NULL},
};

Expand Down
Loading