Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add/adapt tests for mandatory Py_mod_abi slot
  • Loading branch information
encukou committed Mar 31, 2026
commit 6f9b080fb840f6dabaa9bbdd70e3ea64f5747379
18 changes: 16 additions & 2 deletions Lib/test/test_capi/test_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ def def_and_token(mod):
)

class TestModFromSlotsAndSpec(unittest.TestCase):
@requires_gil_enabled("empty slots re-enable GIL")
def test_empty(self):
mod = _testcapi.module_from_slots_empty(FakeSpec())
with self.assertRaises(SystemError):
_testcapi.module_from_slots_empty(FakeSpec())

@requires_gil_enabled("minimal slots re-enable GIL")
def test_minimal(self):
mod = _testcapi.module_from_slots_minimal(FakeSpec())
self.assertIsInstance(mod, types.ModuleType)
self.assertEqual(def_and_token(mod), (0, 0))
self.assertEqual(mod.__name__, 'testmod')
Expand Down Expand Up @@ -159,6 +163,16 @@ def test_null_def_slot(self):
self.assertIn(name, str(cm.exception))
self.assertIn("NULL", str(cm.exception))

def test_bad_abiinfo(self):
"""Slots that incompatible ABI is rejected"""
with self.assertRaises(ImportError) as cm:
_testcapi.module_from_bad_abiinfo(FakeSpec())

def test_multiple_abiinfo(self):
"""Slots that Py_mod_abiinfo can be repeated"""
mod = _testcapi.module_from_multiple_abiinfo(FakeSpec())
self.assertEqual(mod.__name__, 'testmod')

def test_def_multiple_exec(self):
"""PyModule_Exec runs all exec slots of PyModuleDef-defined module"""
mod = _testcapi.module_from_def_multiple_exec(FakeSpec())
Expand Down
16 changes: 12 additions & 4 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3499,12 +3499,20 @@ class Sub(tp):
pass
self.assertEqual(_testcapi.pytype_getmodulebytoken(Sub, token), module)

@requires_gil_enabled("empty slots re-enable GIL")
def test_from_modexport_empty_slots(self):
# Module to test that Py_mod_abi is mandatory for PyModExport
modname = '_test_from_modexport_empty_slots'
filename = _testmultiphase.__file__
with self.assertRaises(SystemError):
import_extension_from_file(
modname, filename, put_in_sys_modules=False)

@requires_gil_enabled("this module re-enables GIL")
def test_from_modexport_minimal_slots(self):
# Module to test that:
# - no slots are mandatory for PyModExport
# - no slots except Py_mod_abi is mandatory for PyModExport
# - the slots array is used as the default token
modname = '_test_from_modexport_empty_slots'
modname = '_test_from_modexport_minimal_slots'
filename = _testmultiphase.__file__
module = import_extension_from_file(
modname, filename, put_in_sys_modules=False)
Expand All @@ -3516,7 +3524,7 @@ def test_from_modexport_empty_slots(self):
smoke_mod = import_extension_from_file(
'_test_from_modexport_smoke', filename, put_in_sys_modules=False)
self.assertEqual(_testcapi.pymodule_get_token(module),
smoke_mod.get_modexport_empty_slots())
smoke_mod.get_modexport_minimal_slots())

@cpython_only
class TestMagicNumber(unittest.TestCase):
Expand Down
65 changes: 65 additions & 0 deletions Modules/_testcapi/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* Lib/test/test_capi/test_module.py
*/

PyABIInfo_VAR(abi_info);

static PyObject *
module_from_slots_empty(PyObject *self, PyObject *spec)
{
Expand All @@ -17,6 +19,16 @@ module_from_slots_empty(PyObject *self, PyObject *spec)
return PyModule_FromSlotsAndSpec(slots, spec);
}

static PyObject *
module_from_slots_minimal(PyObject *self, PyObject *spec)
{
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{0},
};
return PyModule_FromSlotsAndSpec(slots, spec);
}

static PyObject *
module_from_slots_null(PyObject *self, PyObject *spec)
{
Expand All @@ -27,6 +39,7 @@ static PyObject *
module_from_slots_name(PyObject *self, PyObject *spec)
{
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_name, "currently ignored..."},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
Expand All @@ -39,6 +52,7 @@ static PyObject *
module_from_slots_doc(PyObject *self, PyObject *spec)
{
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_doc, "the docstring"},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
Expand All @@ -51,6 +65,7 @@ static PyObject *
module_from_slots_size(PyObject *self, PyObject *spec)
{
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_state_size, (void*)123},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
Expand Down Expand Up @@ -78,6 +93,7 @@ static PyObject *
module_from_slots_methods(PyObject *self, PyObject *spec)
{
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_methods, a_methoddef_array},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
Expand All @@ -96,6 +112,7 @@ static PyObject *
module_from_slots_gc(PyObject *self, PyObject *spec)
{
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_state_traverse, noop_traverse},
{Py_mod_state_clear, noop_clear},
{Py_mod_state_free, noop_free},
Expand Down Expand Up @@ -128,6 +145,7 @@ static PyObject *
module_from_slots_token(PyObject *self, PyObject *spec)
{
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_token, (void*)&test_token},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
Expand Down Expand Up @@ -156,6 +174,7 @@ static PyObject *
module_from_slots_exec(PyObject *self, PyObject *spec)
{
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_exec, simple_exec},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
Expand Down Expand Up @@ -189,6 +208,7 @@ static PyObject *
module_from_slots_create(PyObject *self, PyObject *spec)
{
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_create, create_attr_from_spec},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
Expand Down Expand Up @@ -220,6 +240,7 @@ module_from_slots_repeat_slot(PyObject *self, PyObject *spec)
return NULL;
}
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{slot_id, "anything"},
{slot_id, "anything else"},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
Expand All @@ -238,6 +259,7 @@ module_from_slots_null_slot(PyObject *self, PyObject *spec)
}
PyModuleDef_Slot slots[] = {
{slot_id, NULL},
{Py_mod_abi, &abi_info},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
{0},
Expand All @@ -254,6 +276,7 @@ module_from_def_slot(PyObject *self, PyObject *spec)
}
PyModuleDef_Slot slots[] = {
{slot_id, "anything"},
{Py_mod_abi, &abi_info},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
{0},
Expand Down Expand Up @@ -285,6 +308,7 @@ static PyModuleDef parrot_def = {
.m_slots = NULL /* set below */,
};
static PyModuleDef_Slot parrot_slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_name, (void*)parrot_name},
{Py_mod_doc, (void*)parrot_doc},
{Py_mod_state_size, (void*)123},
Expand Down Expand Up @@ -314,6 +338,43 @@ module_from_def_slot_parrot(PyObject *self, PyObject *spec)
return module;
}

static PyObject *
module_from_bad_abiinfo(PyObject *self, PyObject *spec)
{
PyABIInfo bad_abi_info = {
1, 0,
.abi_version=0x02080000,
};
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_abi, &bad_abi_info},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
{0},
};
return PyModule_FromSlotsAndSpec(slots, spec);
}

static PyObject *
module_from_multiple_abiinfo(PyObject *self, PyObject *spec)
{
PyABIInfo extra_abi_info = {
1, 0,
.flags=PyABIInfo_STABLE | PyABIInfo_FREETHREADING_AGNOSTIC,
.abi_version=0x03040000,
};
PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_abi, &abi_info},
{Py_mod_abi, &extra_abi_info},
{Py_mod_abi, &extra_abi_info},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
{0},
};
return PyModule_FromSlotsAndSpec(slots, spec);
}

static int
another_exec(PyObject *module)
{
Expand Down Expand Up @@ -344,6 +405,7 @@ static PyObject *
module_from_def_multiple_exec(PyObject *self, PyObject *spec)
{
static PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_exec, simple_exec},
{Py_mod_exec, another_exec},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
Expand Down Expand Up @@ -399,6 +461,7 @@ pymodule_get_state_size(PyObject *self, PyObject *module)

static PyMethodDef test_methods[] = {
{"module_from_slots_empty", module_from_slots_empty, METH_O},
{"module_from_slots_minimal", module_from_slots_minimal, METH_O},
{"module_from_slots_null", module_from_slots_null, METH_O},
{"module_from_slots_name", module_from_slots_name, METH_O},
{"module_from_slots_doc", module_from_slots_doc, METH_O},
Expand All @@ -413,6 +476,8 @@ static PyMethodDef test_methods[] = {
{"module_from_def_multiple_exec", module_from_def_multiple_exec, METH_O},
{"module_from_def_slot", module_from_def_slot, METH_O},
{"module_from_def_slot_parrot", module_from_def_slot_parrot, METH_O},
{"module_from_bad_abiinfo", module_from_bad_abiinfo, METH_O},
{"module_from_multiple_abiinfo", module_from_multiple_abiinfo, METH_O},
{"pymodule_get_token", pymodule_get_token, METH_O},
{"pymodule_get_def", pymodule_get_def, METH_O},
{"pymodule_get_state_size", pymodule_get_state_size, METH_O},
Expand Down
27 changes: 23 additions & 4 deletions Modules/_testmultiphase.c
Original file line number Diff line number Diff line change
Expand Up @@ -1034,10 +1034,13 @@ PyInit__test_no_multiple_interpreter_slot(void)

/* PyModExport_* hooks */

PyABIInfo_VAR(abi_info);

PyMODEXPORT_FUNC
PyModExport__test_from_modexport(void)
{
static PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_name, "_test_from_modexport"},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
Expand All @@ -1050,6 +1053,7 @@ PyMODEXPORT_FUNC
PyModExport__test_from_modexport_gil_used(void)
{
static PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_name, "_test_from_modexport_gil_used"},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_USED},
Expand Down Expand Up @@ -1100,6 +1104,7 @@ PyMODEXPORT_FUNC
PyModExport__test_from_modexport_create_nonmodule(void)
{
static PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_name, "_test_from_modexport_create_nonmodule"},
{Py_mod_create, modexport_create_string},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
Expand All @@ -1113,6 +1118,7 @@ PyMODEXPORT_FUNC
PyModExport__test_from_modexport_create_nonmodule_gil_used(void)
{
static PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_name, "_test_from_modexport_create_nonmodule"},
{Py_mod_create, modexport_create_string},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
Expand All @@ -1132,6 +1138,18 @@ PyModExport__test_from_modexport_empty_slots(void)
return modexport_empty_slots;
}


static PyModuleDef_Slot modexport_minimal_slots[] = {
{Py_mod_abi, &abi_info},
{0},
};

PyMODEXPORT_FUNC
PyModExport__test_from_modexport_minimal_slots(void)
{
return modexport_minimal_slots;
}

static int
modexport_smoke_exec(PyObject *mod)
{
Expand Down Expand Up @@ -1172,13 +1190,13 @@ modexport_smoke_get_test_token(PyObject *mod, PyObject *arg)
}

static PyObject *
modexport_get_empty_slots(PyObject *mod, PyObject *arg)
modexport_get_minimal_slots(PyObject *mod, PyObject *arg)
{
/* Get the address of modexport_empty_slots.
* This method would be in the `_test_from_modexport_empty_slots` module,
* This method would be in the `_test_from_modexport_minimal_slots` module,
* if it had a methods slot.
*/
return PyLong_FromVoidPtr(&modexport_empty_slots);
return PyLong_FromVoidPtr(&modexport_minimal_slots);
}

static void
Expand All @@ -1198,10 +1216,11 @@ PyModExport__test_from_modexport_smoke(void)
static PyMethodDef methods[] = {
{"get_state_int", modexport_smoke_get_state_int, METH_NOARGS},
{"get_test_token", modexport_smoke_get_test_token, METH_NOARGS},
{"get_modexport_empty_slots", modexport_get_empty_slots, METH_NOARGS},
{"get_modexport_minimal_slots", modexport_get_minimal_slots, METH_NOARGS},
{0},
};
static PyModuleDef_Slot slots[] = {
{Py_mod_abi, &abi_info},
{Py_mod_name, "_test_from_modexport_smoke"},
{Py_mod_doc, "the expected docstring"},
{Py_mod_exec, modexport_smoke_exec},
Expand Down