From 4ad95be01591832e4eb5c307599dd30a721e17a7 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 21 May 2025 15:11:12 +0200 Subject: [PATCH 01/81] Define PyMODEXPORT_FUNC --- Include/exports.h | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Include/exports.h b/Include/exports.h index 0c646d5beb6ad6..62feb09ed2b433 100644 --- a/Include/exports.h +++ b/Include/exports.h @@ -9,6 +9,7 @@ inside the Python core, they are private to the core. If in an extension module, it may be declared with external linkage depending on the platform. + PyMODEXPORT_FUNC: Like PyMODINIT_FUNC, but for a slots array As a number of platforms support/require "__declspec(dllimport/dllexport)", we support a HAVE_DECLSPEC_DLL macro to save duplication. @@ -62,9 +63,9 @@ /* module init functions inside the core need no external linkage */ /* except for Cygwin to handle embedding */ # if defined(__CYGWIN__) -# define PyMODINIT_FUNC Py_EXPORTED_SYMBOL PyObject* +# define _PyINIT_FUNC_DECLSPEC Py_EXPORTED_SYMBOL # else /* __CYGWIN__ */ -# define PyMODINIT_FUNC PyObject* +# define _PyINIT_FUNC_DECLSPEC # endif /* __CYGWIN__ */ # else /* Py_BUILD_CORE */ /* Building an extension module, or an embedded situation */ @@ -78,9 +79,9 @@ # define PyAPI_DATA(RTYPE) extern Py_IMPORTED_SYMBOL RTYPE /* module init functions outside the core must be exported */ # if defined(__cplusplus) -# define PyMODINIT_FUNC extern "C" Py_EXPORTED_SYMBOL PyObject* +# define _PyINIT_FUNC_DECLSPEC extern "C" Py_EXPORTED_SYMBOL # else /* __cplusplus */ -# define PyMODINIT_FUNC Py_EXPORTED_SYMBOL PyObject* +# define _PyINIT_FUNC_DECLSPEC Py_EXPORTED_SYMBOL # endif /* __cplusplus */ # endif /* Py_BUILD_CORE */ # endif /* HAVE_DECLSPEC_DLL */ @@ -93,13 +94,15 @@ #ifndef PyAPI_DATA # define PyAPI_DATA(RTYPE) extern Py_EXPORTED_SYMBOL RTYPE #endif -#ifndef PyMODINIT_FUNC +#ifndef _PyINIT_FUNC_DECLSPEC # if defined(__cplusplus) -# define PyMODINIT_FUNC extern "C" Py_EXPORTED_SYMBOL PyObject* +# define _PyINIT_FUNC_DECLSPEC extern "C" Py_EXPORTED_SYMBOL # else /* __cplusplus */ -# define PyMODINIT_FUNC Py_EXPORTED_SYMBOL PyObject* +# define _PyINIT_FUNC_DECLSPEC Py_EXPORTED_SYMBOL # endif /* __cplusplus */ #endif +#define PyMODINIT_FUNC _PyINIT_FUNC_DECLSPEC PyObject* +#define PyMODEXPORT_FUNC _PyINIT_FUNC_DECLSPEC PyModuleDef_Slot* #endif /* Py_EXPORTS_H */ From c53650d4bb2a4e4b9187e044bf0797faa52bd051 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 21 May 2025 15:13:48 +0200 Subject: [PATCH 02/81] Add new state to module object --- Include/internal/pycore_moduleobject.h | 6 ++++++ Objects/moduleobject.c | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index b170d7bce702c6..382d11c923481f 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -27,6 +27,12 @@ typedef struct { #ifdef Py_GIL_DISABLED void *md_gil; #endif + Py_ssize_t md_size; + traverseproc md_traverse; + inquiry md_clear; + freefunc md_free; + void *md_token; + int (*md_exec)(PyObject *); } PyModuleObject; static inline PyModuleDef* _PyModule_GetDef(PyObject *mod) { diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 0d45c1171688ab..f27f4f4fd92a5b 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -150,6 +150,12 @@ new_module_notrack(PyTypeObject *mt) m->md_state = NULL; m->md_weaklist = NULL; m->md_name = NULL; + m->md_size = 0; + m->md_traverse = NULL; + m->md_clear = NULL; + m->md_free = NULL; + m->md_exec = NULL; + m->md_token = NULL; m->md_dict = PyDict_New(); if (m->md_dict == NULL) { Py_DECREF(m); From 9564e201ac886ee7b9cab365be8e1339a70cdae2 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 21 May 2025 15:18:36 +0200 Subject: [PATCH 03/81] Make sure the module def matches the state --- Objects/moduleobject.c | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index f27f4f4fd92a5b..2cbfeb23fba44a 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -27,6 +27,18 @@ static PyMemberDef module_members[] = { {0} }; +static void +assert_def_missing_or_redundant(PyModuleObject *m) { + if (m->md_def) { +#define DO_ASSERT(F) assert (m->md_def->m_ ## F == m->md_ ## F); + DO_ASSERT(size); + DO_ASSERT(traverse); + DO_ASSERT(clear); + DO_ASSERT(free); +#undef DO_ASSERT + } +} + PyTypeObject PyModuleDef_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) @@ -270,6 +282,16 @@ PyModule_Create2(PyModuleDef* module, int module_api_version) return _PyModule_CreateInitialized(module, module_api_version); } +static void +module_set_def(PyModuleObject *md, PyModuleDef *def) +{ + md->md_def = def; + md->md_size = def->m_size; + md->md_traverse = def->m_traverse; + md->md_clear = def->m_clear; + md->md_free = def->m_free; +} + PyObject * _PyModule_CreateInitialized(PyModuleDef* module, int module_api_version) { @@ -316,7 +338,7 @@ _PyModule_CreateInitialized(PyModuleDef* module, int module_api_version) return NULL; } } - m->md_def = module; + module_set_def(m, module); #ifdef Py_GIL_DISABLED m->md_gil = Py_MOD_GIL_USED; #endif @@ -462,7 +484,7 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio if (PyModule_Check(m)) { ((PyModuleObject*)m)->md_state = NULL; - ((PyModuleObject*)m)->md_def = def; + module_set_def(((PyModuleObject*)m), def); #ifdef Py_GIL_DISABLED ((PyModuleObject*)m)->md_gil = gil_slot; #else @@ -894,6 +916,7 @@ module_dealloc(PyObject *self) } FT_CLEAR_WEAKREFS(self, m->md_weaklist); + assert_def_missing_or_redundant(m); /* bpo-39824: Don't call m_free() if m_size > 0 and md_state=NULL */ if (m->md_def && m->md_def->m_free && (m->md_def->m_size <= 0 || m->md_state != NULL)) @@ -1212,6 +1235,7 @@ module_traverse(PyObject *self, visitproc visit, void *arg) { PyModuleObject *m = _PyModule_CAST(self); + assert_def_missing_or_redundant(m); /* bpo-39824: Don't call m_traverse() if m_size > 0 and md_state=NULL */ if (m->md_def && m->md_def->m_traverse && (m->md_def->m_size <= 0 || m->md_state != NULL)) @@ -1230,6 +1254,7 @@ module_clear(PyObject *self) { PyModuleObject *m = _PyModule_CAST(self); + assert_def_missing_or_redundant(m); /* bpo-39824: Don't call m_clear() if m_size > 0 and md_state=NULL */ if (m->md_def && m->md_def->m_clear && (m->md_def->m_size <= 0 || m->md_state != NULL)) From 05a87c3558e43625da62e9bd3f433578c90a01d5 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 21 May 2025 15:25:58 +0200 Subject: [PATCH 04/81] Add simple test case --- Lib/test/test_capi/test_modexport.py | 29 ++++++++++++++++++++++++++++ Modules/_testmultiphase.c | 10 ++++++++++ 2 files changed, 39 insertions(+) create mode 100644 Lib/test/test_capi/test_modexport.py diff --git a/Lib/test/test_capi/test_modexport.py b/Lib/test/test_capi/test_modexport.py new file mode 100644 index 00000000000000..b89e1ab3109194 --- /dev/null +++ b/Lib/test/test_capi/test_modexport.py @@ -0,0 +1,29 @@ +import unittest +import importlib +import sys +from importlib.machinery import ExtensionFileLoader + +try: + import _testmultiphase +except ImportError: + _testmultiphase = None + +def import_extension_from_file(modname, filename, *, put_in_sys_modules=True): + loader = ExtensionFileLoader(modname, filename) + spec = importlib.util.spec_from_loader(modname, loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + if put_in_sys_modules: + sys.modules[modname] = module + return module + + +class TestModExport(unittest.TestCase): + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_modexport(self): + modname = '_test_modexport' + filename = _testmultiphase.__file__ + module = import_extension_from_file(modname, filename, + put_in_sys_modules=False) + + self.assertEqual(module.__name__, modname) diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index bfec0678e2c669..991cb7ca528f7d 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -993,3 +993,13 @@ PyInit__test_no_multiple_interpreter_slot(void) { return PyModuleDef_Init(&no_multiple_interpreter_slot_def); } + +PyMODEXPORT_FUNC +PyModExport__test_from_modexport(PyObject *spec) +{ + static PyModuleDef_Slot from_modexport_slots[] = { + {Py_mod_name, "_test_from_modexport"}, + {0}, + }; + return from_modexport_slots; +} From 57c4f1e9e8628a1ad209744fcf0e9cac5b640e70 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 21 May 2025 15:26:46 +0200 Subject: [PATCH 05/81] Add new slot definitions --- Include/moduleobject.h | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Include/moduleobject.h b/Include/moduleobject.h index e3afac0a343be1..c5b6d542e3514e 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -83,11 +83,19 @@ struct PyModuleDef_Slot { #endif #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) # define Py_mod_abi 5 +# define Py_mod_name 6 +# define Py_mod_doc 7 +# define Py_mod_size 8 +# define Py_mod_methods 9 +# define Py_mod_traverse 10 +# define Py_mod_clear 11 +# define Py_mod_free 12 +# define Py_mod_token 13 #endif #ifndef Py_LIMITED_API -#define _Py_mod_LAST_SLOT 5 +#define _Py_mod_LAST_SLOT 13 #endif #endif /* New in 3.5 */ From 956c59a23a1e9793c823c6d37675164a0ddf6637 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 23 May 2025 17:41:15 +0200 Subject: [PATCH 06/81] Modules are bigger sue me --- Lib/test/test_sys.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 1198c6d35113c8..b1454f3aa6b4bb 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1725,9 +1725,10 @@ def get_gen(): yield 1 check(int(PyLong_BASE**2), vsize('') + 3*self.longdigit) # module if support.Py_GIL_DISABLED: - check(unittest, size('PPPPPP')) + md_gil = 'P' else: - check(unittest, size('PPPPP')) + md_gil = '' + check(unittest, size('PPPPP' + md_gil + 'NPPPPP')) # None check(None, size('')) # NotImplementedType From 14a66d204d232b4a8a1f241fc5ffa6f4c07cef42 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 21 May 2025 15:36:48 +0200 Subject: [PATCH 07/81] Add a test for PyModule_FromSlotsAndSpec --- Modules/Setup.stdlib.in | 2 +- Modules/_testcapi/module.c | 30 ++++++++++++++++++++++++++++++ Modules/_testcapi/parts.h | 1 + Modules/_testcapimodule.c | 3 +++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 Modules/_testcapi/module.c diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 5365e68101cf4c..231d73e462d121 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -174,7 +174,7 @@ @MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c @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 _testinternalcapi/complex.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/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.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/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.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 _testlimitedcapi/file.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c @MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c new file mode 100644 index 00000000000000..a797894134bca2 --- /dev/null +++ b/Modules/_testcapi/module.c @@ -0,0 +1,30 @@ +#include "parts.h" +#include "util.h" + +// Test PyModule_* API + +static PyObject * +module_from_slots_and_spec(PyObject *self, PyObject *py_slots) +{ + assert(PyList_Check(py_slots)); + Py_ssize_t n_slots = PyList_GET_SIZE(py_slots); + PyModuleDef_Slot *slots = PyMem_Calloc(n_slots + 1, + sizeof(PyModuleDef_Slot)); + if (!slots) { + return PyErr_NoMemory(); + } + + return PyModule_FromSlotsAndSpec(slots, Py_None); +} + + +static PyMethodDef test_methods[] = { + {"module_from_slots_and_spec", module_from_slots_and_spec, METH_O}, + {NULL}, +}; + +int +_PyTestCapi_Init_Module(PyObject *m) +{ + return PyModule_AddFunctions(m, test_methods); +} diff --git a/Modules/_testcapi/parts.h b/Modules/_testcapi/parts.h index 32915d04bd3635..a7feca5bd96070 100644 --- a/Modules/_testcapi/parts.h +++ b/Modules/_testcapi/parts.h @@ -66,5 +66,6 @@ int _PyTestCapi_Init_Import(PyObject *mod); int _PyTestCapi_Init_Frame(PyObject *mod); int _PyTestCapi_Init_Type(PyObject *mod); int _PyTestCapi_Init_Function(PyObject *mod); +int _PyTestCapi_Init_Module(PyObject *mod); #endif // Py_TESTCAPI_PARTS_H diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 4e73be20e1b709..77c582b5cc45f8 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3477,6 +3477,9 @@ _testcapi_exec(PyObject *m) if (_PyTestCapi_Init_Function(m) < 0) { return -1; } + if (_PyTestCapi_Init_Module(m) < 0) { + return -1; + } return 0; } From 72a27f50a17d37e9a4bd18ae6ff67241579ad7a0 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 24 Oct 2025 13:07:54 +0200 Subject: [PATCH 08/81] Add PyModule_FromSlotsAndSpec (empty for now) --- Include/moduleobject.h | 4 ++++ Objects/moduleobject.c | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/Include/moduleobject.h b/Include/moduleobject.h index c5b6d542e3514e..5e16787dc5c0be 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -117,6 +117,10 @@ struct PyModuleDef_Slot { PyAPI_FUNC(int) PyUnstable_Module_SetGIL(PyObject *module, void *gil); #endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) +PyAPI_FUNC(PyObject *) PyModule_FromSlotsAndSpec(PyModuleDef_Slot *, + PyObject *spec); +#endif #ifndef _Py_OPAQUE_PYOBJECT struct PyModuleDef { diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 2cbfeb23fba44a..e985f57d0700c8 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -531,6 +531,14 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio return NULL; } +PyObject * +PyModule_FromSlotsAndSpec(PyModuleDef_Slot *slots, PyObject *spec) +{ + PyObject *result = NULL; + return result; +} + + #ifdef Py_GIL_DISABLED int PyUnstable_Module_SetGIL(PyObject *module, void *gil) From b54293bd96f608ef69bd73c74517ba96651659c6 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 21 May 2025 15:47:58 +0200 Subject: [PATCH 09/81] Add module test --- Lib/test/test_capi/test_module.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Lib/test/test_capi/test_module.py diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py new file mode 100644 index 00000000000000..cf67360b878012 --- /dev/null +++ b/Lib/test/test_capi/test_module.py @@ -0,0 +1,12 @@ +import unittest +import types +from test.support import import_helper + +# Skip this test if the _testcapi module isn't available. +_testcapi = import_helper.import_module('_testcapi') + + +class TestModExport(unittest.TestCase): + def test_modexport(self): + mod = _testcapi.module_from_slots_and_spec([]) + self.assertIsInstance(mod, types.ModuleType) From fcc68f8c1a8af917034d554cb4fd76a8c87045dd Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 21 May 2025 15:53:12 +0200 Subject: [PATCH 10/81] Fill in enough of a PyModuleDef to pass to common machinery --- Objects/moduleobject.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index e985f57d0700c8..2a8487bcc033bb 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -535,6 +535,24 @@ PyObject * PyModule_FromSlotsAndSpec(PyModuleDef_Slot *slots, PyObject *spec) { PyObject *result = NULL; + if (!slots) { + PyErr_BadArgument(); + } + PyObject *nameobj = PyObject_GetAttrString(spec, "name"); + if (nameobj == NULL) { + goto finally; + } + const char *name = PyUnicode_AsUTF8(nameobj); + if (name == NULL) { + goto finally; + } + + // Fill in enough of a PyModuleDef to pass to common machinery + PyModuleDef def_like = {.m_slots = slots}; + + result = PyModule_FromDefAndSpec2(&def_like, spec, 3); +finally: + Py_XDECREF(nameobj); return result; } From 3ab607ead42132186888753772a195e30c8c8313 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 21 May 2025 15:57:20 +0200 Subject: [PATCH 11/81] Pass in a spec --- Lib/test/test_capi/test_module.py | 10 +++++++--- Modules/_testcapi/module.c | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index cf67360b878012..b42d2f64576e82 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -6,7 +6,11 @@ _testcapi = import_helper.import_module('_testcapi') -class TestModExport(unittest.TestCase): - def test_modexport(self): - mod = _testcapi.module_from_slots_and_spec([]) +class TestModFromSlotsAndSpec(unittest.TestCase): + def test_empty(self): + spec_like = types.SimpleNamespace( + name='testmod', + ) + mod = _testcapi.module_from_slots_and_spec([], spec_like) self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(mod.__name__, 'testmod') diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index a797894134bca2..3aa9e2888a18c5 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -4,8 +4,15 @@ // Test PyModule_* API static PyObject * -module_from_slots_and_spec(PyObject *self, PyObject *py_slots) +module_from_slots_and_spec(PyObject *self, PyObject *args) { + PyObject *spec; + PyObject *py_slots; + if(PyArg_UnpackTuple(args, "module_from_slots_and_spec", 2, 2, + &py_slots, &spec) < 1) + { + return NULL; + } assert(PyList_Check(py_slots)); Py_ssize_t n_slots = PyList_GET_SIZE(py_slots); PyModuleDef_Slot *slots = PyMem_Calloc(n_slots + 1, @@ -14,12 +21,14 @@ module_from_slots_and_spec(PyObject *self, PyObject *py_slots) return PyErr_NoMemory(); } - return PyModule_FromSlotsAndSpec(slots, Py_None); + PyObject *result = PyModule_FromSlotsAndSpec(slots, spec); + PyMem_Free(slots); + return result; } static PyMethodDef test_methods[] = { - {"module_from_slots_and_spec", module_from_slots_and_spec, METH_O}, + {"module_from_slots_and_spec", module_from_slots_and_spec, METH_VARARGS}, {NULL}, }; From 35d7d512b313298dd5b55cb1aed46049c742282c Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 10:54:21 +0200 Subject: [PATCH 12/81] Rename field which can now be NULL --- Include/internal/pycore_moduleobject.h | 8 +++++--- Objects/moduleobject.c | 27 ++++++++++++-------------- Objects/typeobject.c | 4 ++-- Python/import.c | 20 ++++++++++--------- Python/importdl.c | 2 +- 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index 382d11c923481f..430eeced831244 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -19,7 +19,9 @@ extern int _PyModule_IsExtension(PyObject *obj); typedef struct { PyObject_HEAD PyObject *md_dict; - PyModuleDef *md_def; + // The PyModuleDef used to define the module, if any. + // (used to be `md_def` when all extension modules had one) + PyModuleDef *md_def_or_null; void *md_state; PyObject *md_weaklist; // for logging purposes after md_dict is cleared @@ -35,9 +37,9 @@ typedef struct { int (*md_exec)(PyObject *); } PyModuleObject; -static inline PyModuleDef* _PyModule_GetDef(PyObject *mod) { +static inline PyModuleDef* _PyModule_GetDefOrNull(PyObject *mod) { assert(PyModule_Check(mod)); - return ((PyModuleObject *)mod)->md_def; + return ((PyModuleObject *)mod)->md_def_or_null; } static inline void* _PyModule_GetState(PyObject* mod) { diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 2a8487bcc033bb..2bd1b4f1929c15 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -29,8 +29,8 @@ static PyMemberDef module_members[] = { static void assert_def_missing_or_redundant(PyModuleObject *m) { - if (m->md_def) { -#define DO_ASSERT(F) assert (m->md_def->m_ ## F == m->md_ ## F); + if (m->md_def_or_null) { +#define DO_ASSERT(F) assert (m->md_def_or_null->m_ ## F == m->md_ ## F); DO_ASSERT(size); DO_ASSERT(traverse); DO_ASSERT(clear); @@ -56,7 +56,7 @@ _PyModule_IsExtension(PyObject *obj) } PyModuleObject *module = (PyModuleObject*)obj; - PyModuleDef *def = module->md_def; + PyModuleDef *def = module->md_def_or_null; return (def != NULL && def->m_methods != NULL); } @@ -158,7 +158,7 @@ new_module_notrack(PyTypeObject *mt) m = (PyModuleObject *)_PyType_AllocNoTrack(mt, 0); if (m == NULL) return NULL; - m->md_def = NULL; + m->md_def_or_null = NULL; m->md_state = NULL; m->md_weaklist = NULL; m->md_name = NULL; @@ -285,7 +285,7 @@ PyModule_Create2(PyModuleDef* module, int module_api_version) static void module_set_def(PyModuleObject *md, PyModuleDef *def) { - md->md_def = def; + md->md_def_or_null = def; md->md_size = def->m_size; md->md_traverse = def->m_traverse; md->md_clear = def->m_clear; @@ -818,7 +818,7 @@ PyModule_GetDef(PyObject* m) PyErr_BadArgument(); return NULL; } - return _PyModule_GetDef(m); + return _PyModule_GetDefOrNull(m); } void* @@ -944,10 +944,9 @@ module_dealloc(PyObject *self) assert_def_missing_or_redundant(m); /* bpo-39824: Don't call m_free() if m_size > 0 and md_state=NULL */ - if (m->md_def && m->md_def->m_free - && (m->md_def->m_size <= 0 || m->md_state != NULL)) + if (m->md_free && (m->md_size <= 0 || m->md_state != NULL)) { - m->md_def->m_free(m); + m->md_free(m); } Py_XDECREF(m->md_dict); @@ -1263,10 +1262,9 @@ module_traverse(PyObject *self, visitproc visit, void *arg) assert_def_missing_or_redundant(m); /* bpo-39824: Don't call m_traverse() if m_size > 0 and md_state=NULL */ - if (m->md_def && m->md_def->m_traverse - && (m->md_def->m_size <= 0 || m->md_state != NULL)) + if (m->md_traverse && (m->md_size <= 0 || m->md_state != NULL)) { - int res = m->md_def->m_traverse((PyObject*)m, visit, arg); + int res = m->md_traverse((PyObject*)m, visit, arg); if (res) return res; } @@ -1282,10 +1280,9 @@ module_clear(PyObject *self) assert_def_missing_or_redundant(m); /* bpo-39824: Don't call m_clear() if m_size > 0 and md_state=NULL */ - if (m->md_def && m->md_def->m_clear - && (m->md_def->m_size <= 0 || m->md_state != NULL)) + if (m->md_clear && (m->md_size <= 0 || m->md_state != NULL)) { - int res = m->md_def->m_clear((PyObject*)m); + int res = m->md_clear((PyObject*)m); if (PyErr_Occurred()) { PyErr_FormatUnraisable("Exception ignored in m_clear of module%s%V", m->md_name ? " " : "", diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 6cc6d366a3598c..dd3f4f3d76d3f7 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -5780,7 +5780,7 @@ PyType_GetModuleByDef(PyTypeObject *type, PyModuleDef *def) else { PyHeapTypeObject *ht = (PyHeapTypeObject*)type; PyObject *module = ht->ht_module; - if (module && _PyModule_GetDef(module) == def) { + if (module && _PyModule_GetDefOrNull(module) == def) { return module; } } @@ -5808,7 +5808,7 @@ PyType_GetModuleByDef(PyTypeObject *type, PyModuleDef *def) PyHeapTypeObject *ht = (PyHeapTypeObject*)super; PyObject *module = ht->ht_module; - if (module && _PyModule_GetDef(module) == def) { + if (module && _PyModule_GetDefOrNull(module) == def) { res = module; break; } diff --git a/Python/import.c b/Python/import.c index d01c4d478283ff..f2c82315aa01b6 100644 --- a/Python/import.c +++ b/Python/import.c @@ -1801,7 +1801,7 @@ finish_singlephase_extension(PyThreadState *tstate, PyObject *mod, PyObject *name, PyObject *modules) { assert(mod != NULL && PyModule_Check(mod)); - assert(cached->def == _PyModule_GetDef(mod)); + assert(cached->def == _PyModule_GetDefOrNull(mod)); Py_ssize_t index = _get_cached_module_index(cached); if (_modules_by_index_set(tstate->interp, index, mod) < 0) { @@ -1879,8 +1879,8 @@ reload_singlephase_extension(PyThreadState *tstate, * due to violating interpreter isolation. * See the note in set_cached_m_dict(). * Until that is solved, we leave md_def set to NULL. */ - assert(_PyModule_GetDef(mod) == NULL - || _PyModule_GetDef(mod) == def); + assert(_PyModule_GetDefOrNull(mod) == NULL + || _PyModule_GetDefOrNull(mod) == def); } else { assert(cached->m_dict == NULL); @@ -2253,9 +2253,11 @@ _PyImport_FixupBuiltin(PyThreadState *tstate, PyObject *mod, const char *name, return -1; } - PyModuleDef *def = PyModule_GetDef(mod); + PyModuleDef *def = _PyModule_GetDefOrNull(mod); if (def == NULL) { - PyErr_BadInternalCall(); + if (!PyErr_Occurred()) { + PyErr_BadInternalCall(); + } goto finally; } @@ -2336,8 +2338,8 @@ create_builtin(PyThreadState *tstate, PyObject *name, PyObject *spec) assert(!_PyErr_Occurred(tstate)); assert(cached != NULL); /* The module might not have md_def set in certain reload cases. */ - assert(_PyModule_GetDef(mod) == NULL - || cached->def == _PyModule_GetDef(mod)); + assert(_PyModule_GetDefOrNull(mod) == NULL + || cached->def == _PyModule_GetDefOrNull(mod)); assert_singlephase(cached); goto finally; } @@ -4666,8 +4668,8 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file) assert(!_PyErr_Occurred(tstate)); assert(cached != NULL); /* The module might not have md_def set in certain reload cases. */ - assert(_PyModule_GetDef(mod) == NULL - || cached->def == _PyModule_GetDef(mod)); + assert(_PyModule_GetDefOrNull(mod) == NULL + || cached->def == _PyModule_GetDefOrNull(mod)); assert_singlephase(cached); goto finally; } diff --git a/Python/importdl.c b/Python/importdl.c index 802843fe7b9dce..b640f649aa1c65 100644 --- a/Python/importdl.c +++ b/Python/importdl.c @@ -496,7 +496,7 @@ _PyImport_RunModInitFunc(PyModInitFunction p0, goto error; } - res.def = _PyModule_GetDef(m); + res.def = _PyModule_GetDefOrNull(m); if (res.def == NULL) { PyErr_Clear(); _Py_ext_module_loader_result_set_error( From 6fbbc18881ea3539e6a2701bda0879e25d24b68c Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 11:28:13 +0200 Subject: [PATCH 13/81] Common implementation for PyModule_From{Def,Slots}AndSpec This uses a "def-like" structure: a PyModuleDef* that's not a valid Python object. --- Objects/moduleobject.c | 63 +++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 2bd1b4f1929c15..25a1b61e186f66 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -283,13 +283,15 @@ PyModule_Create2(PyModuleDef* module, int module_api_version) } static void -module_set_def(PyModuleObject *md, PyModuleDef *def) +module_copy_members_from_deflike( + PyModuleObject *md, + PyModuleDef *def_like /* not necessarily a valid Python object */) { - md->md_def_or_null = def; - md->md_size = def->m_size; - md->md_traverse = def->m_traverse; - md->md_clear = def->m_clear; - md->md_free = def->m_free; + /* def may not be a valid PyObject*, see */ + md->md_size = def_like->m_size; + md->md_traverse = def_like->m_traverse; + md->md_clear = def_like->m_clear; + md->md_free = def_like->m_free; } PyObject * @@ -338,7 +340,8 @@ _PyModule_CreateInitialized(PyModuleDef* module, int module_api_version) return NULL; } } - module_set_def(m, module); + m->md_def_or_null = module; + module_copy_members_from_deflike(m, module); #ifdef Py_GIL_DISABLED m->md_gil = Py_MOD_GIL_USED; #endif @@ -346,7 +349,11 @@ _PyModule_CreateInitialized(PyModuleDef* module, int module_api_version) } PyObject * -PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_version) +module_from_def_and_spec( + PyModuleDef* def_like, /* not necessarily a valid Python object */ + PyObject *spec, + int module_api_version, + PyModuleDef* original_def /* NULL if not defined by a def */) { PyModuleDef_Slot* cur_slot; PyObject *(*create)(PyObject *, PyModuleDef*) = NULL; @@ -361,8 +368,6 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio int ret; PyInterpreterState *interp = _PyInterpreterState_GET(); - PyModuleDef_Init(def); - nameobj = PyObject_GetAttrString(spec, "name"); if (nameobj == NULL) { return NULL; @@ -376,7 +381,7 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio goto error; } - if (def->m_size < 0) { + if (def_like->m_size < 0) { PyErr_Format( PyExc_SystemError, "module %s: m_size may not be negative for multi-phase initialization", @@ -384,7 +389,7 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio goto error; } - for (cur_slot = def->m_slots; cur_slot && cur_slot->slot; cur_slot++) { + for (cur_slot = def_like->m_slots; cur_slot && cur_slot->slot; cur_slot++) { switch (cur_slot->slot) { case Py_mod_create: if (create) { @@ -457,7 +462,7 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio } if (create) { - m = create(spec, def); + m = create(spec, def_like); if (m == NULL) { if (!PyErr_Occurred()) { PyErr_Format( @@ -483,15 +488,21 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio } if (PyModule_Check(m)) { - ((PyModuleObject*)m)->md_state = NULL; - module_set_def(((PyModuleObject*)m), def); + PyModuleObject *mod = (PyModuleObject*)m; + mod->md_state = NULL; + module_copy_members_from_deflike(mod, def_like); + if (original_def) { + mod->md_def_or_null = original_def; + } #ifdef Py_GIL_DISABLED - ((PyModuleObject*)m)->md_gil = gil_slot; + mod->md_gil = gil_slot; #else (void)gil_slot; #endif } else { - if (def->m_size > 0 || def->m_traverse || def->m_clear || def->m_free) { + if (def_like->m_size > 0 || def_like->m_traverse || def_like->m_clear + || def_like->m_free) + { PyErr_Format( PyExc_SystemError, "module %s is not a module object, but requests module state", @@ -508,15 +519,15 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio } } - if (def->m_methods != NULL) { - ret = _add_methods_to_object(m, nameobj, def->m_methods); + if (def_like->m_methods != NULL) { + ret = _add_methods_to_object(m, nameobj, def_like->m_methods); if (ret != 0) { goto error; } } - if (def->m_doc != NULL) { - ret = PyModule_SetDocString(m, def->m_doc); + if (def_like->m_doc != NULL) { + ret = PyModule_SetDocString(m, def_like->m_doc); if (ret != 0) { goto error; } @@ -531,6 +542,13 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio return NULL; } +PyObject * +PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_version) +{ + PyModuleDef_Init(def); + return module_from_def_and_spec(def, spec, module_api_version, def); +} + PyObject * PyModule_FromSlotsAndSpec(PyModuleDef_Slot *slots, PyObject *spec) { @@ -550,7 +568,8 @@ PyModule_FromSlotsAndSpec(PyModuleDef_Slot *slots, PyObject *spec) // Fill in enough of a PyModuleDef to pass to common machinery PyModuleDef def_like = {.m_slots = slots}; - result = PyModule_FromDefAndSpec2(&def_like, spec, 3); + result = module_from_def_and_spec(&def_like, spec, PYTHON_API_VERSION, + NULL); finally: Py_XDECREF(nameobj); return result; From 7207e07ef004b1a29b6c76e6bd821d862f3fe933 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 12:03:00 +0200 Subject: [PATCH 14/81] Add test for Py_mod_name slot --- Lib/test/test_capi/test_module.py | 17 +++++++++++---- Modules/_testcapi/module.c | 35 ++++++++++++++----------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index b42d2f64576e82..29a2bdd6aa37c1 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -6,11 +6,20 @@ _testcapi = import_helper.import_module('_testcapi') +class FakeSpec: + name = 'testmod' + + class TestModFromSlotsAndSpec(unittest.TestCase): def test_empty(self): - spec_like = types.SimpleNamespace( - name='testmod', - ) - mod = _testcapi.module_from_slots_and_spec([], spec_like) + mod = _testcapi.module_from_slots_empty(FakeSpec()) + self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(mod.__name__, 'testmod') + + def test_name(self): + # Py_mod_name (and PyModuleDef.m_name) are currently ignored when + # spec is given. + # We still test that it's accepted. + mod = _testcapi.module_from_slots_name(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) self.assertEqual(mod.__name__, 'testmod') diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 3aa9e2888a18c5..8e6035b6d45e72 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -4,31 +4,28 @@ // Test PyModule_* API static PyObject * -module_from_slots_and_spec(PyObject *self, PyObject *args) +module_from_slots_empty(PyObject *self, PyObject *spec) { - PyObject *spec; - PyObject *py_slots; - if(PyArg_UnpackTuple(args, "module_from_slots_and_spec", 2, 2, - &py_slots, &spec) < 1) - { - return NULL; - } - assert(PyList_Check(py_slots)); - Py_ssize_t n_slots = PyList_GET_SIZE(py_slots); - PyModuleDef_Slot *slots = PyMem_Calloc(n_slots + 1, - sizeof(PyModuleDef_Slot)); - if (!slots) { - return PyErr_NoMemory(); - } + PyModuleDef_Slot slots[] = { + {0}, + }; + return PyModule_FromSlotsAndSpec(slots, spec); +} - PyObject *result = PyModule_FromSlotsAndSpec(slots, spec); - PyMem_Free(slots); - return result; +static PyObject * +module_from_slots_name(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_name, "currently ignored..."}, + {0}, + }; + return PyModule_FromSlotsAndSpec(slots, spec); } static PyMethodDef test_methods[] = { - {"module_from_slots_and_spec", module_from_slots_and_spec, METH_VARARGS}, + {"module_from_slots_empty", module_from_slots_empty, METH_O}, + {"module_from_slots_name", module_from_slots_name, METH_O}, {NULL}, }; From 8d4d2641227ea8640178b3fb32077699267ca640 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 12:40:33 +0200 Subject: [PATCH 15/81] Handle Py_mod_name --- Objects/moduleobject.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 25a1b61e186f66..0bdc0a0c317165 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -431,6 +431,23 @@ module_from_def_and_spec( goto error; } break; + case Py_mod_name: + if (original_def) { + PyErr_Format( + PyExc_SystemError, + "module %s: Py_mod_name used with PyModuleDef", + name); + goto error; + } + if (def_like->m_name) { + PyErr_Format( + PyExc_SystemError, + "module %s has more than one 'gil' slot", + name); + goto error; + } + def_like->m_name = cur_slot->value; + break; default: assert(cur_slot->slot < 0 || cur_slot->slot > _Py_mod_LAST_SLOT); PyErr_Format( From bd61e5bce53c6edede0375c6a94fedb223b9cfc6 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 12:42:45 +0200 Subject: [PATCH 16/81] Test repeated slot --- Lib/test/test_capi/test_module.py | 4 ++++ Modules/_testcapi/module.c | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 29a2bdd6aa37c1..0ade485931e225 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -23,3 +23,7 @@ def test_name(self): mod = _testcapi.module_from_slots_name(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) self.assertEqual(mod.__name__, 'testmod') + + def test_repeat_name(self): + with self.assertRaises(SystemError): + _testcapi.module_from_slots_repeat_name(FakeSpec()) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 8e6035b6d45e72..4b138f6d9e93f6 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -22,10 +22,22 @@ module_from_slots_name(PyObject *self, PyObject *spec) return PyModule_FromSlotsAndSpec(slots, spec); } +static PyObject * +module_from_slots_repeat_name(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_name, "currently ignored..."}, + {Py_mod_name, "currently ignored..."}, + {0}, + }; + return PyModule_FromSlotsAndSpec(slots, spec); +} + static PyMethodDef test_methods[] = { {"module_from_slots_empty", module_from_slots_empty, METH_O}, {"module_from_slots_name", module_from_slots_name, METH_O}, + {"module_from_slots_repeat_name", module_from_slots_repeat_name, METH_O}, {NULL}, }; From d638abf4f0414e713502152da9cb507ff09f9f10 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 12:47:04 +0200 Subject: [PATCH 17/81] Test Py_mod_name with a def --- Lib/test/test_capi/test_module.py | 10 +++++++++- Modules/_testcapi/module.c | 17 +++++++++++++++++ Objects/moduleobject.c | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 0ade485931e225..a736a4796344e3 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -24,6 +24,14 @@ def test_name(self): self.assertIsInstance(mod, types.ModuleType) self.assertEqual(mod.__name__, 'testmod') + def test_def_name(self): + with self.assertRaises(SystemError) as cm: + _testcapi.module_from_def_name(FakeSpec()) + self.assertIn("Py_mod_name", str(cm.exception),) + self.assertIn("PyModuleDef", str(cm.exception), ) + def test_repeat_name(self): - with self.assertRaises(SystemError): + with self.assertRaises(SystemError) as cm: _testcapi.module_from_slots_repeat_name(FakeSpec()) + self.assertIn("Py_mod_name", str(cm.exception),) + self.assertIn("repeated", str(cm.exception), ) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 4b138f6d9e93f6..c57cfabec0b7f4 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -34,10 +34,27 @@ module_from_slots_repeat_name(PyObject *self, PyObject *spec) } + +static PyObject * +module_from_def_name(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_name, "currently ignored..."}, + {0}, + }; + PyModuleDef def = { + PyModuleDef_HEAD_INIT, + .m_name = "currently ignored", + .m_slots = slots, + }; + return PyModule_FromDefAndSpec(&def, spec); +} + static PyMethodDef test_methods[] = { {"module_from_slots_empty", module_from_slots_empty, METH_O}, {"module_from_slots_name", module_from_slots_name, METH_O}, {"module_from_slots_repeat_name", module_from_slots_repeat_name, METH_O}, + {"module_from_def_name", module_from_def_name, METH_O}, {NULL}, }; diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 0bdc0a0c317165..97af08150c276a 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -442,7 +442,7 @@ module_from_def_and_spec( if (def_like->m_name) { PyErr_Format( PyExc_SystemError, - "module %s has more than one 'gil' slot", + "module %s: Py_mod_name slot repeated", name); goto error; } From b24e6a6650b5ba2d613be072168741ae44f9a446 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 12:51:42 +0200 Subject: [PATCH 18/81] Py_mod_doc --- Lib/test/test_capi/test_module.py | 7 ++++++ Modules/_testcapi/module.c | 11 +++++++++ Objects/moduleobject.c | 39 +++++++++++++++++-------------- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index a736a4796344e3..a96a9e8d3fed9a 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -23,6 +23,13 @@ def test_name(self): mod = _testcapi.module_from_slots_name(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) self.assertEqual(mod.__name__, 'testmod') + self.assertEqual(mod.__doc__, None) + + def test_doc(self): + mod = _testcapi.module_from_slots_doc(FakeSpec()) + self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(mod.__name__, 'testmod') + self.assertEqual(mod.__doc__, 'the docstring') def test_def_name(self): with self.assertRaises(SystemError) as cm: diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index c57cfabec0b7f4..35542cd4cea895 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -22,6 +22,16 @@ module_from_slots_name(PyObject *self, PyObject *spec) return PyModule_FromSlotsAndSpec(slots, spec); } +static PyObject * +module_from_slots_doc(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_doc, "the docstring"}, + {0}, + }; + return PyModule_FromSlotsAndSpec(slots, spec); +} + static PyObject * module_from_slots_repeat_name(PyObject *self, PyObject *spec) { @@ -53,6 +63,7 @@ module_from_def_name(PyObject *self, PyObject *spec) static PyMethodDef test_methods[] = { {"module_from_slots_empty", module_from_slots_empty, METH_O}, {"module_from_slots_name", module_from_slots_name, METH_O}, + {"module_from_slots_doc", module_from_slots_doc, METH_O}, {"module_from_slots_repeat_name", module_from_slots_repeat_name, METH_O}, {"module_from_def_name", module_from_def_name, METH_O}, {NULL}, diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 97af08150c276a..801e1cc35dd906 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -431,23 +431,28 @@ module_from_def_and_spec( goto error; } break; - case Py_mod_name: - if (original_def) { - PyErr_Format( - PyExc_SystemError, - "module %s: Py_mod_name used with PyModuleDef", - name); - goto error; - } - if (def_like->m_name) { - PyErr_Format( - PyExc_SystemError, - "module %s: Py_mod_name slot repeated", - name); - goto error; - } - def_like->m_name = cur_slot->value; - break; +#define COPY_SLOT_TO_DEFLIKE(SLOT, TYPE, DEST) \ + case SLOT: \ + if (original_def) { \ + PyErr_Format( \ + PyExc_SystemError, \ + "module %s: " #SLOT " used with PyModuleDef", \ + name); \ + goto error; \ + } \ + if (def_like->DEST) { \ + PyErr_Format( \ + PyExc_SystemError, \ + "module %s: " #SLOT " slot repeated", \ + name); \ + goto error; \ + } \ + def_like->DEST = (TYPE)(cur_slot->value); \ + break; \ + ///////////////////////////////////////////////////////////// + COPY_SLOT_TO_DEFLIKE(Py_mod_name, char*, m_name); + COPY_SLOT_TO_DEFLIKE(Py_mod_doc, char*, m_doc); +#undef COPY_SLOT_TO_DEFLIKE default: assert(cur_slot->slot < 0 || cur_slot->slot > _Py_mod_LAST_SLOT); PyErr_Format( From 756cbffb03c2511edc21b5a9f3cf0e60fef1329b Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 12:58:17 +0200 Subject: [PATCH 19/81] Disallow NULL slot value --- Lib/test/test_capi/test_module.py | 6 ++++++ Modules/_testcapi/module.c | 10 ++++++++++ Objects/moduleobject.c | 7 +++++++ 3 files changed, 23 insertions(+) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index a96a9e8d3fed9a..ee7b8c84d23aef 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -42,3 +42,9 @@ def test_repeat_name(self): _testcapi.module_from_slots_repeat_name(FakeSpec()) self.assertIn("Py_mod_name", str(cm.exception),) self.assertIn("repeated", str(cm.exception), ) + + def test_null_name(self): + with self.assertRaises(SystemError) as cm: + _testcapi.module_from_slots_null_name(FakeSpec()) + self.assertIn("Py_mod_name", str(cm.exception),) + self.assertIn("NULL", str(cm.exception), ) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 35542cd4cea895..1c9d82847c7f76 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -44,6 +44,15 @@ module_from_slots_repeat_name(PyObject *self, PyObject *spec) } +static PyObject * +module_from_slots_null_name(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_name, NULL}, + {0}, + }; + return PyModule_FromSlotsAndSpec(slots, spec); +} static PyObject * module_from_def_name(PyObject *self, PyObject *spec) @@ -65,6 +74,7 @@ static PyMethodDef test_methods[] = { {"module_from_slots_name", module_from_slots_name, METH_O}, {"module_from_slots_doc", module_from_slots_doc, METH_O}, {"module_from_slots_repeat_name", module_from_slots_repeat_name, METH_O}, + {"module_from_slots_null_name", module_from_slots_null_name, METH_O}, {"module_from_def_name", module_from_def_name, METH_O}, {NULL}, }; diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 801e1cc35dd906..5936b5ea89cd04 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -433,6 +433,13 @@ module_from_def_and_spec( break; #define COPY_SLOT_TO_DEFLIKE(SLOT, TYPE, DEST) \ case SLOT: \ + if (!(TYPE)(cur_slot->value)) { \ + PyErr_Format( \ + PyExc_SystemError, \ + "module %s: " #SLOT " must not be NULL", \ + name); \ + goto error; \ + } \ if (original_def) { \ PyErr_Format( \ PyExc_SystemError, \ From 320de14c745ba15c5b616c0760535398bc3a525d Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 13:06:20 +0200 Subject: [PATCH 20/81] Test repeats and NULL for all new slots --- Lib/test/test_capi/test_module.py | 26 +++++++++++------- Modules/_testcapi/module.c | 44 ++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index ee7b8c84d23aef..0bac5b97ecb85a 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -37,14 +37,22 @@ def test_def_name(self): self.assertIn("Py_mod_name", str(cm.exception),) self.assertIn("PyModuleDef", str(cm.exception), ) - def test_repeat_name(self): - with self.assertRaises(SystemError) as cm: - _testcapi.module_from_slots_repeat_name(FakeSpec()) - self.assertIn("Py_mod_name", str(cm.exception),) - self.assertIn("repeated", str(cm.exception), ) + def test_repeated_new_slot(self): + for name in 'Py_mod_name', 'Py_mod_doc': + with self.subTest(name): + spec = FakeSpec() + spec._test_slot_id = getattr(_testcapi, name) + with self.assertRaises(SystemError) as cm: + _testcapi.module_from_slots_repeat_slot(spec) + self.assertIn(name, str(cm.exception),) + self.assertIn("repeated", str(cm.exception), ) def test_null_name(self): - with self.assertRaises(SystemError) as cm: - _testcapi.module_from_slots_null_name(FakeSpec()) - self.assertIn("Py_mod_name", str(cm.exception),) - self.assertIn("NULL", str(cm.exception), ) + for name in 'Py_mod_name', 'Py_mod_doc': + with self.subTest(name): + spec = FakeSpec() + spec._test_slot_id = getattr(_testcapi, name) + with self.assertRaises(SystemError) as cm: + _testcapi.module_from_slots_null_slot(spec) + self.assertIn(name, str(cm.exception),) + self.assertIn("NULL", str(cm.exception), ) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 1c9d82847c7f76..36492d59b0bbc3 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -33,11 +33,19 @@ module_from_slots_doc(PyObject *self, PyObject *spec) } static PyObject * -module_from_slots_repeat_name(PyObject *self, PyObject *spec) +module_from_slots_repeat_slot(PyObject *self, PyObject *spec) { + PyObject *slot_id_obj = PyObject_GetAttrString(spec, "_test_slot_id"); + if (slot_id_obj == NULL) { + return NULL; + } + int slot_id = PyLong_AsLong(slot_id_obj); + if (PyErr_Occurred()) { + return NULL; + } PyModuleDef_Slot slots[] = { - {Py_mod_name, "currently ignored..."}, - {Py_mod_name, "currently ignored..."}, + {slot_id, "currently ignored..."}, + {slot_id, "currently ignored..."}, {0}, }; return PyModule_FromSlotsAndSpec(slots, spec); @@ -45,10 +53,18 @@ module_from_slots_repeat_name(PyObject *self, PyObject *spec) static PyObject * -module_from_slots_null_name(PyObject *self, PyObject *spec) +module_from_slots_null_slot(PyObject *self, PyObject *spec) { + PyObject *slot_id_obj = PyObject_GetAttrString(spec, "_test_slot_id"); + if (slot_id_obj == NULL) { + return NULL; + } + int slot_id = PyLong_AsLong(slot_id_obj); + if (PyErr_Occurred()) { + return NULL; + } PyModuleDef_Slot slots[] = { - {Py_mod_name, NULL}, + {slot_id, NULL}, {0}, }; return PyModule_FromSlotsAndSpec(slots, spec); @@ -73,8 +89,8 @@ static PyMethodDef test_methods[] = { {"module_from_slots_empty", module_from_slots_empty, METH_O}, {"module_from_slots_name", module_from_slots_name, METH_O}, {"module_from_slots_doc", module_from_slots_doc, METH_O}, - {"module_from_slots_repeat_name", module_from_slots_repeat_name, METH_O}, - {"module_from_slots_null_name", module_from_slots_null_name, METH_O}, + {"module_from_slots_repeat_slot", module_from_slots_repeat_slot, METH_O}, + {"module_from_slots_null_slot", module_from_slots_null_slot, METH_O}, {"module_from_def_name", module_from_def_name, METH_O}, {NULL}, }; @@ -82,5 +98,19 @@ static PyMethodDef test_methods[] = { int _PyTestCapi_Init_Module(PyObject *m) { +#define ADD_INT_MACRO(C) if (PyModule_AddIntConstant(m, #C, C) < 0) return -1; + ADD_INT_MACRO(Py_mod_create); + ADD_INT_MACRO(Py_mod_exec); + ADD_INT_MACRO(Py_mod_multiple_interpreters); + ADD_INT_MACRO(Py_mod_gil); + ADD_INT_MACRO(Py_mod_name); + ADD_INT_MACRO(Py_mod_doc); + ADD_INT_MACRO(Py_mod_size); + ADD_INT_MACRO(Py_mod_methods); + ADD_INT_MACRO(Py_mod_traverse); + ADD_INT_MACRO(Py_mod_clear); + ADD_INT_MACRO(Py_mod_free); + ADD_INT_MACRO(Py_mod_token); +#undef ADD_INT_MACRO return PyModule_AddFunctions(m, test_methods); } From 4d88cf38ed30e786d7e79d5631ea161ba077c2de Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 13:19:24 +0200 Subject: [PATCH 21/81] Add PyModule_GetSize --- Include/moduleobject.h | 1 + Objects/moduleobject.c | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/Include/moduleobject.h b/Include/moduleobject.h index 5e16787dc5c0be..64e43d3c12bd83 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -120,6 +120,7 @@ PyAPI_FUNC(int) PyUnstable_Module_SetGIL(PyObject *module, void *gil); #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) PyAPI_FUNC(PyObject *) PyModule_FromSlotsAndSpec(PyModuleDef_Slot *, PyObject *spec); +PyAPI_FUNC(int) PyModule_GetSize(PyObject *mod, Py_ssize_t *size_p); #endif #ifndef _Py_OPAQUE_PYOBJECT diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 5936b5ea89cd04..9916965ecca830 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -726,6 +726,19 @@ PyModule_GetDict(PyObject *m) return _PyModule_GetDict(m); // borrowed reference } +int +PyModule_GetSize(PyObject *m, Py_ssize_t *size_p) +{ + *size_p = -1; + if (!PyModule_Check(m)) { + PyErr_BadInternalCall(); + return -1; + } + PyModuleObject *mod = (PyModuleObject *)m; + *size_p = mod->md_size; + return 0; +} + PyObject* PyModule_GetNameObject(PyObject *mod) { From a1d3c1547999ed19669e950c9836f8e343ce2c4c Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 13:17:58 +0200 Subject: [PATCH 22/81] Test size --- Lib/test/test_capi/test_module.py | 7 +++++++ Modules/_testcapi/module.c | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 0bac5b97ecb85a..26375bfc3cec66 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -31,6 +31,13 @@ def test_doc(self): self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, 'the docstring') + def test_size(self): + mod = _testcapi.module_from_slots_size(FakeSpec()) + self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(mod.__name__, 'testmod') + self.assertEqual(mod.__doc__, None) + self.assertEqual(mod.size, 123) + def test_def_name(self): with self.assertRaises(SystemError) as cm: _testcapi.module_from_def_name(FakeSpec()) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 36492d59b0bbc3..6cde9f2026c9a4 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -32,6 +32,27 @@ module_from_slots_doc(PyObject *self, PyObject *spec) return PyModule_FromSlotsAndSpec(slots, spec); } +static PyObject * +module_from_slots_size(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_size, (void*)123}, + {0}, + }; + PyObject *mod = PyModule_FromSlotsAndSpec(slots, spec); + if (!mod) { + return NULL; + } + Py_ssize_t size; + if (PyModule_GetSize(mod, &size) < 0) { + return NULL; + } + if (PyModule_AddIntConstant(mod, "size", size) < 0) { + return NULL; + } + return mod; +} + static PyObject * module_from_slots_repeat_slot(PyObject *self, PyObject *spec) { @@ -89,6 +110,7 @@ static PyMethodDef test_methods[] = { {"module_from_slots_empty", module_from_slots_empty, METH_O}, {"module_from_slots_name", module_from_slots_name, METH_O}, {"module_from_slots_doc", module_from_slots_doc, METH_O}, + {"module_from_slots_size", module_from_slots_size, METH_O}, {"module_from_slots_repeat_slot", module_from_slots_repeat_slot, METH_O}, {"module_from_slots_null_slot", module_from_slots_null_slot, METH_O}, {"module_from_def_name", module_from_def_name, METH_O}, From 7fcee672ba91390b17b41c3959f31e3f5af7391e Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 13:23:45 +0200 Subject: [PATCH 23/81] Handle size --- Objects/moduleobject.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 9916965ecca830..04ec18a46cba5f 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -459,6 +459,7 @@ module_from_def_and_spec( ///////////////////////////////////////////////////////////// COPY_SLOT_TO_DEFLIKE(Py_mod_name, char*, m_name); COPY_SLOT_TO_DEFLIKE(Py_mod_doc, char*, m_doc); + COPY_SLOT_TO_DEFLIKE(Py_mod_size, Py_ssize_t, m_size); #undef COPY_SLOT_TO_DEFLIKE default: assert(cur_slot->slot < 0 || cur_slot->slot > _Py_mod_LAST_SLOT); From 9fd1e7e46838545f8fa6b19f1e4ff5b47250c37f Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 13:32:19 +0200 Subject: [PATCH 24/81] Test new slots with PyModuleDef --- Lib/test/test_capi/test_module.py | 26 +++++++++++++------ Modules/_testcapi/module.c | 42 +++++++++++++++++++------------ 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 26375bfc3cec66..958a7d8faa846e 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -9,6 +9,10 @@ class FakeSpec: name = 'testmod' +DEF_SLOTS = ( + 'Py_mod_name', 'Py_mod_doc', 'Py_mod_size', +) + class TestModFromSlotsAndSpec(unittest.TestCase): def test_empty(self): @@ -38,13 +42,20 @@ def test_size(self): self.assertEqual(mod.__doc__, None) self.assertEqual(mod.size, 123) - def test_def_name(self): - with self.assertRaises(SystemError) as cm: - _testcapi.module_from_def_name(FakeSpec()) - self.assertIn("Py_mod_name", str(cm.exception),) - self.assertIn("PyModuleDef", str(cm.exception), ) + def test_def_slot(self): + """Slots that replace PyModuleDef fields can't be used with PyModuleDef + """ + for name in 'Py_mod_name', 'Py_mod_doc': + with self.subTest(name): + spec = FakeSpec() + spec._test_slot_id = getattr(_testcapi, name) + with self.assertRaises(SystemError) as cm: + _testcapi.module_from_def_slot(spec) + self.assertIn(name, str(cm.exception),) + self.assertIn("PyModuleDef", str(cm.exception), ) - def test_repeated_new_slot(self): + def test_repeated_def_slot(self): + """Slots that replace PyModuleDef fields can't be repeated""" for name in 'Py_mod_name', 'Py_mod_doc': with self.subTest(name): spec = FakeSpec() @@ -54,7 +65,8 @@ def test_repeated_new_slot(self): self.assertIn(name, str(cm.exception),) self.assertIn("repeated", str(cm.exception), ) - def test_null_name(self): + def test_null_def_slot(self): + """Slots that replace PyModuleDef fields can't be NULL""" for name in 'Py_mod_name', 'Py_mod_doc': with self.subTest(name): spec = FakeSpec() diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 6cde9f2026c9a4..0672a15005263b 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -53,35 +53,41 @@ module_from_slots_size(PyObject *self, PyObject *spec) return mod; } -static PyObject * -module_from_slots_repeat_slot(PyObject *self, PyObject *spec) + +static int +slot_from_object(PyObject *obj) { - PyObject *slot_id_obj = PyObject_GetAttrString(spec, "_test_slot_id"); + PyObject *slot_id_obj = PyObject_GetAttrString(obj, "_test_slot_id"); if (slot_id_obj == NULL) { - return NULL; + return -1; } int slot_id = PyLong_AsLong(slot_id_obj); if (PyErr_Occurred()) { + return -1; + } + return slot_id; +} + +static PyObject * +module_from_slots_repeat_slot(PyObject *self, PyObject *spec) +{ + int slot_id = slot_from_object(spec); + if (slot_id < 0) { return NULL; } PyModuleDef_Slot slots[] = { - {slot_id, "currently ignored..."}, - {slot_id, "currently ignored..."}, + {slot_id, "anything"}, + {slot_id, "anything else"}, {0}, }; return PyModule_FromSlotsAndSpec(slots, spec); } - static PyObject * module_from_slots_null_slot(PyObject *self, PyObject *spec) { - PyObject *slot_id_obj = PyObject_GetAttrString(spec, "_test_slot_id"); - if (slot_id_obj == NULL) { - return NULL; - } - int slot_id = PyLong_AsLong(slot_id_obj); - if (PyErr_Occurred()) { + int slot_id = slot_from_object(spec); + if (slot_id < 0) { return NULL; } PyModuleDef_Slot slots[] = { @@ -92,10 +98,14 @@ module_from_slots_null_slot(PyObject *self, PyObject *spec) } static PyObject * -module_from_def_name(PyObject *self, PyObject *spec) +module_from_def_slot(PyObject *self, PyObject *spec) { + int slot_id = slot_from_object(spec); + if (slot_id < 0) { + return NULL; + } PyModuleDef_Slot slots[] = { - {Py_mod_name, "currently ignored..."}, + {slot_id, "anything"}, {0}, }; PyModuleDef def = { @@ -113,7 +123,7 @@ static PyMethodDef test_methods[] = { {"module_from_slots_size", module_from_slots_size, METH_O}, {"module_from_slots_repeat_slot", module_from_slots_repeat_slot, METH_O}, {"module_from_slots_null_slot", module_from_slots_null_slot, METH_O}, - {"module_from_def_name", module_from_def_name, METH_O}, + {"module_from_def_slot", module_from_def_slot, METH_O}, {NULL}, }; From fd95984efee0605e8517ca3c48c0024d1cbfd253 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 13:37:18 +0200 Subject: [PATCH 25/81] Py_mod_methods --- Lib/test/test_capi/test_module.py | 15 +++++++++++---- Modules/_testcapi/module.c | 22 ++++++++++++++++++++++ Objects/moduleobject.c | 1 + 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 958a7d8faa846e..cfbfc57b00a59d 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -10,7 +10,7 @@ class FakeSpec: name = 'testmod' DEF_SLOTS = ( - 'Py_mod_name', 'Py_mod_doc', 'Py_mod_size', + 'Py_mod_name', 'Py_mod_doc', 'Py_mod_size', 'Py_mod_methods', ) @@ -42,10 +42,17 @@ def test_size(self): self.assertEqual(mod.__doc__, None) self.assertEqual(mod.size, 123) + def test_methods(self): + mod = _testcapi.module_from_slots_methods(FakeSpec()) + self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(mod.__name__, 'testmod') + self.assertEqual(mod.__doc__, None) + self.assertEqual(mod.a_method(456), (mod, 456)) + def test_def_slot(self): """Slots that replace PyModuleDef fields can't be used with PyModuleDef """ - for name in 'Py_mod_name', 'Py_mod_doc': + for name in DEF_SLOTS: with self.subTest(name): spec = FakeSpec() spec._test_slot_id = getattr(_testcapi, name) @@ -56,7 +63,7 @@ def test_def_slot(self): def test_repeated_def_slot(self): """Slots that replace PyModuleDef fields can't be repeated""" - for name in 'Py_mod_name', 'Py_mod_doc': + for name in DEF_SLOTS: with self.subTest(name): spec = FakeSpec() spec._test_slot_id = getattr(_testcapi, name) @@ -67,7 +74,7 @@ def test_repeated_def_slot(self): def test_null_def_slot(self): """Slots that replace PyModuleDef fields can't be NULL""" - for name in 'Py_mod_name', 'Py_mod_doc': + for name in DEF_SLOTS: with self.subTest(name): spec = FakeSpec() spec._test_slot_id = getattr(_testcapi, name) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 0672a15005263b..381e1e436e116f 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -53,6 +53,27 @@ module_from_slots_size(PyObject *self, PyObject *spec) return mod; } +static PyObject * +a_method(PyObject *self, PyObject *arg) +{ + return PyTuple_Pack(2, self, arg); +} + +static PyMethodDef a_methoddef_array[] = { + {"a_method", a_method, METH_O}, + {0}, +}; + +static PyObject * +module_from_slots_methods(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_methods, a_methoddef_array}, + {0}, + }; + return PyModule_FromSlotsAndSpec(slots, spec); +} + static int slot_from_object(PyObject *obj) @@ -121,6 +142,7 @@ static PyMethodDef test_methods[] = { {"module_from_slots_name", module_from_slots_name, METH_O}, {"module_from_slots_doc", module_from_slots_doc, METH_O}, {"module_from_slots_size", module_from_slots_size, METH_O}, + {"module_from_slots_methods", module_from_slots_methods, METH_O}, {"module_from_slots_repeat_slot", module_from_slots_repeat_slot, METH_O}, {"module_from_slots_null_slot", module_from_slots_null_slot, METH_O}, {"module_from_def_slot", module_from_def_slot, METH_O}, diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 04ec18a46cba5f..469861ca89a012 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -460,6 +460,7 @@ module_from_def_and_spec( COPY_SLOT_TO_DEFLIKE(Py_mod_name, char*, m_name); COPY_SLOT_TO_DEFLIKE(Py_mod_doc, char*, m_doc); COPY_SLOT_TO_DEFLIKE(Py_mod_size, Py_ssize_t, m_size); + COPY_SLOT_TO_DEFLIKE(Py_mod_methods, PyMethodDef*, m_methods); #undef COPY_SLOT_TO_DEFLIKE default: assert(cur_slot->slot < 0 || cur_slot->slot > _Py_mod_LAST_SLOT); From 748e18f8e091c22d0dc2038e8444111bd3c99ed1 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 13:48:56 +0200 Subject: [PATCH 26/81] GC hooks --- Include/moduleobject.h | 2 ++ Lib/test/test_capi/test_module.py | 7 +++++++ Modules/_testcapi/module.c | 32 +++++++++++++++++++++++++++++++ Objects/moduleobject.c | 18 +++++++++++++++++ 4 files changed, 59 insertions(+) diff --git a/Include/moduleobject.h b/Include/moduleobject.h index 64e43d3c12bd83..7b2b6667f6f6e0 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -96,6 +96,8 @@ struct PyModuleDef_Slot { #ifndef Py_LIMITED_API #define _Py_mod_LAST_SLOT 13 +PyAPI_FUNC(int) _PyModule_GetGCHooks( + PyObject *, traverseproc*, inquiry*, freefunc*); // For testing #endif #endif /* New in 3.5 */ diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index cfbfc57b00a59d..39f98f319704a7 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -11,6 +11,7 @@ class FakeSpec: DEF_SLOTS = ( 'Py_mod_name', 'Py_mod_doc', 'Py_mod_size', 'Py_mod_methods', + 'Py_mod_traverse', 'Py_mod_clear', 'Py_mod_free', ) @@ -49,6 +50,12 @@ def test_methods(self): self.assertEqual(mod.__doc__, None) self.assertEqual(mod.a_method(456), (mod, 456)) + def test_gc(self): + mod = _testcapi.module_from_slots_gc(FakeSpec()) + self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(mod.__name__, 'testmod') + self.assertEqual(mod.__doc__, None) + def test_def_slot(self): """Slots that replace PyModuleDef fields can't be used with PyModuleDef """ diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 381e1e436e116f..4d9c9466f0da22 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -74,6 +74,37 @@ module_from_slots_methods(PyObject *self, PyObject *spec) return PyModule_FromSlotsAndSpec(slots, spec); } +static int trivial_traverse(PyObject *self, visitproc visit, void *arg) { + return 0; +} +static int trivial_clear(PyObject *self) { return 0; } +static void trivial_free(void *self) { } + +static PyObject * +module_from_slots_gc(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_traverse, trivial_traverse}, + {Py_mod_clear, trivial_clear}, + {Py_mod_free, trivial_free}, + {0}, + }; + PyObject *mod = PyModule_FromSlotsAndSpec(slots, spec); + if (!mod) { + return NULL; + } + traverseproc traverse; + inquiry clear; + freefunc free; + if (_PyModule_GetGCHooks(mod, &traverse, &clear, &free) < 0) { + return NULL; + } + assert(traverse == &trivial_traverse); + assert(clear == &trivial_clear); + assert(free == &trivial_free); + return mod; +} + static int slot_from_object(PyObject *obj) @@ -143,6 +174,7 @@ static PyMethodDef test_methods[] = { {"module_from_slots_doc", module_from_slots_doc, METH_O}, {"module_from_slots_size", module_from_slots_size, METH_O}, {"module_from_slots_methods", module_from_slots_methods, METH_O}, + {"module_from_slots_gc", module_from_slots_gc, METH_O}, {"module_from_slots_repeat_slot", module_from_slots_repeat_slot, METH_O}, {"module_from_slots_null_slot", module_from_slots_null_slot, METH_O}, {"module_from_def_slot", module_from_def_slot, METH_O}, diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 469861ca89a012..b86bd9e019702d 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -461,6 +461,9 @@ module_from_def_and_spec( COPY_SLOT_TO_DEFLIKE(Py_mod_doc, char*, m_doc); COPY_SLOT_TO_DEFLIKE(Py_mod_size, Py_ssize_t, m_size); COPY_SLOT_TO_DEFLIKE(Py_mod_methods, PyMethodDef*, m_methods); + COPY_SLOT_TO_DEFLIKE(Py_mod_traverse, traverseproc, m_traverse); + COPY_SLOT_TO_DEFLIKE(Py_mod_clear, inquiry, m_clear); + COPY_SLOT_TO_DEFLIKE(Py_mod_free, freefunc, m_free); #undef COPY_SLOT_TO_DEFLIKE default: assert(cur_slot->slot < 0 || cur_slot->slot > _Py_mod_LAST_SLOT); @@ -741,6 +744,21 @@ PyModule_GetSize(PyObject *m, Py_ssize_t *size_p) return 0; } +int +_PyModule_GetGCHooks(PyObject *m, traverseproc *traverse, + inquiry *clear, freefunc *free) +{ + if (!PyModule_Check(m)) { + PyErr_BadInternalCall(); + return -1; + } + PyModuleObject *mod = (PyModuleObject *)m; + *traverse = mod->md_traverse; + *clear = mod->md_clear; + *free = mod->md_free; + return 0; +} + PyObject* PyModule_GetNameObject(PyObject *mod) { From 1101f551cc446285e6d3776a06e47689a86c92a6 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 14:26:09 +0200 Subject: [PATCH 27/81] Py_mod_token --- Include/moduleobject.h | 1 + Lib/test/test_capi/test_module.py | 8 ++++- Modules/_testcapi/module.c | 26 ++++++++++++++++ Objects/moduleobject.c | 50 +++++++++++++++++++++++++------ 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/Include/moduleobject.h b/Include/moduleobject.h index 7b2b6667f6f6e0..99965480b236d3 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -123,6 +123,7 @@ PyAPI_FUNC(int) PyUnstable_Module_SetGIL(PyObject *module, void *gil); PyAPI_FUNC(PyObject *) PyModule_FromSlotsAndSpec(PyModuleDef_Slot *, PyObject *spec); PyAPI_FUNC(int) PyModule_GetSize(PyObject *mod, Py_ssize_t *size_p); +PyAPI_FUNC(int) PyModule_GetToken(PyObject *, void **token_p); #endif #ifndef _Py_OPAQUE_PYOBJECT diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 39f98f319704a7..94986c10bbc997 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -11,7 +11,7 @@ class FakeSpec: DEF_SLOTS = ( 'Py_mod_name', 'Py_mod_doc', 'Py_mod_size', 'Py_mod_methods', - 'Py_mod_traverse', 'Py_mod_clear', 'Py_mod_free', + 'Py_mod_traverse', 'Py_mod_clear', 'Py_mod_free', 'Py_mod_token', ) @@ -56,6 +56,12 @@ def test_gc(self): self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, None) + def test_token(self): + mod = _testcapi.module_from_slots_token(FakeSpec()) + self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(mod.__name__, 'testmod') + self.assertEqual(mod.__doc__, None) + def test_def_slot(self): """Slots that replace PyModuleDef fields can't be used with PyModuleDef """ diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 4d9c9466f0da22..6041a9fe532fba 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -45,9 +45,11 @@ module_from_slots_size(PyObject *self, PyObject *spec) } Py_ssize_t size; if (PyModule_GetSize(mod, &size) < 0) { + Py_DECREF(mod); return NULL; } if (PyModule_AddIntConstant(mod, "size", size) < 0) { + Py_DECREF(mod); return NULL; } return mod; @@ -97,6 +99,7 @@ module_from_slots_gc(PyObject *self, PyObject *spec) inquiry clear; freefunc free; if (_PyModule_GetGCHooks(mod, &traverse, &clear, &free) < 0) { + Py_DECREF(mod); return NULL; } assert(traverse == &trivial_traverse); @@ -105,6 +108,28 @@ module_from_slots_gc(PyObject *self, PyObject *spec) return mod; } +static char test_token; + +static PyObject * +module_from_slots_token(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_token, &test_token}, + {0}, + }; + PyObject *mod = PyModule_FromSlotsAndSpec(slots, spec); + if (!mod) { + return NULL; + } + void *got_token; + if (PyModule_GetToken(mod, &got_token) < 0) { + Py_DECREF(mod); + return NULL; + } + assert(got_token == &test_token); + return mod; +} + static int slot_from_object(PyObject *obj) @@ -175,6 +200,7 @@ static PyMethodDef test_methods[] = { {"module_from_slots_size", module_from_slots_size, METH_O}, {"module_from_slots_methods", module_from_slots_methods, METH_O}, {"module_from_slots_gc", module_from_slots_gc, METH_O}, + {"module_from_slots_token", module_from_slots_token, METH_O}, {"module_from_slots_repeat_slot", module_from_slots_repeat_slot, METH_O}, {"module_from_slots_null_slot", module_from_slots_null_slot, METH_O}, {"module_from_def_slot", module_from_def_slot, METH_O}, diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index b86bd9e019702d..e4573cd1403b07 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -366,6 +366,7 @@ module_from_def_and_spec( int has_execution_slots = 0; const char *name; int ret; + void *token = NULL; PyInterpreterState *interp = _PyInterpreterState_GET(); nameobj = PyObject_GetAttrString(spec, "name"); @@ -447,23 +448,26 @@ module_from_def_and_spec( name); \ goto error; \ } \ - if (def_like->DEST) { \ + if (DEST) { \ PyErr_Format( \ PyExc_SystemError, \ "module %s: " #SLOT " slot repeated", \ name); \ goto error; \ } \ - def_like->DEST = (TYPE)(cur_slot->value); \ + DEST = (TYPE)(cur_slot->value); \ break; \ ///////////////////////////////////////////////////////////// - COPY_SLOT_TO_DEFLIKE(Py_mod_name, char*, m_name); - COPY_SLOT_TO_DEFLIKE(Py_mod_doc, char*, m_doc); - COPY_SLOT_TO_DEFLIKE(Py_mod_size, Py_ssize_t, m_size); - COPY_SLOT_TO_DEFLIKE(Py_mod_methods, PyMethodDef*, m_methods); - COPY_SLOT_TO_DEFLIKE(Py_mod_traverse, traverseproc, m_traverse); - COPY_SLOT_TO_DEFLIKE(Py_mod_clear, inquiry, m_clear); - COPY_SLOT_TO_DEFLIKE(Py_mod_free, freefunc, m_free); + COPY_SLOT_TO_DEFLIKE(Py_mod_name, char*, def_like->m_name); + COPY_SLOT_TO_DEFLIKE(Py_mod_doc, char*, def_like->m_doc); + COPY_SLOT_TO_DEFLIKE(Py_mod_size, Py_ssize_t, def_like->m_size); + COPY_SLOT_TO_DEFLIKE(Py_mod_methods, PyMethodDef*, + def_like->m_methods); + COPY_SLOT_TO_DEFLIKE(Py_mod_traverse, traverseproc, + def_like->m_traverse); + COPY_SLOT_TO_DEFLIKE(Py_mod_clear, inquiry, def_like->m_clear); + COPY_SLOT_TO_DEFLIKE(Py_mod_free, freefunc, def_like->m_free); + COPY_SLOT_TO_DEFLIKE(Py_mod_token, void*, token); #undef COPY_SLOT_TO_DEFLIKE default: assert(cur_slot->slot < 0 || cur_slot->slot > _Py_mod_LAST_SLOT); @@ -533,6 +537,13 @@ module_from_def_and_spec( #else (void)gil_slot; #endif + if (original_def) { + mod->md_token = original_def; + assert (!token); + } + else { + mod->md_token = token; + } } else { if (def_like->m_size > 0 || def_like->m_traverse || def_like->m_clear || def_like->m_free) @@ -551,6 +562,14 @@ module_from_def_and_spec( name); goto error; } + if (token) { + PyErr_Format( + PyExc_SystemError, + "module %s specifies a token, but did not create " + "a ModuleType instance", + name); + goto error; + } } if (def_like->m_methods != NULL) { @@ -744,6 +763,19 @@ PyModule_GetSize(PyObject *m, Py_ssize_t *size_p) return 0; } +int +PyModule_GetToken(PyObject *m, void **token_p) +{ + *token_p = NULL; + if (!PyModule_Check(m)) { + PyErr_BadInternalCall(); + return -1; + } + PyModuleObject *mod = (PyModuleObject *)m; + *token_p = mod->md_token; + return 0; +} + int _PyModule_GetGCHooks(PyObject *m, traverseproc *traverse, inquiry *clear, freefunc *free) From 488eae7d659d9d20cbcb8a724a48c726620a84cd Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 14:39:55 +0200 Subject: [PATCH 28/81] Py_mod_exec --- Include/moduleobject.h | 1 + Lib/test/test_capi/test_module.py | 11 ++- Lib/test/test_import/__init__.py | 15 ++++ Modules/_testcapi/module.c | 31 +++++++++ Modules/_testmultiphase.c | 21 ++++++ Objects/moduleobject.c | 110 ++++++++++++++++++------------ 6 files changed, 143 insertions(+), 46 deletions(-) diff --git a/Include/moduleobject.h b/Include/moduleobject.h index 99965480b236d3..53f1726536f574 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -122,6 +122,7 @@ PyAPI_FUNC(int) PyUnstable_Module_SetGIL(PyObject *module, void *gil); #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) PyAPI_FUNC(PyObject *) PyModule_FromSlotsAndSpec(PyModuleDef_Slot *, PyObject *spec); +PyAPI_FUNC(int) PyModule_Exec(PyObject *mod); PyAPI_FUNC(int) PyModule_GetSize(PyObject *mod, Py_ssize_t *size_p); PyAPI_FUNC(int) PyModule_GetToken(PyObject *, void **token_p); #endif diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 94986c10bbc997..7ad1a92ff73513 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -62,6 +62,13 @@ def test_token(self): self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, None) + def test_exec(self): + mod = _testcapi.module_from_slots_exec(FakeSpec()) + self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(mod.__name__, 'testmod') + self.assertEqual(mod.__doc__, None) + self.assertEqual(mod.a_number, 456) + def test_def_slot(self): """Slots that replace PyModuleDef fields can't be used with PyModuleDef """ @@ -76,7 +83,7 @@ def test_def_slot(self): def test_repeated_def_slot(self): """Slots that replace PyModuleDef fields can't be repeated""" - for name in DEF_SLOTS: + for name in (*DEF_SLOTS, 'Py_mod_exec'): with self.subTest(name): spec = FakeSpec() spec._test_slot_id = getattr(_testcapi, name) @@ -87,7 +94,7 @@ def test_repeated_def_slot(self): def test_null_def_slot(self): """Slots that replace PyModuleDef fields can't be NULL""" - for name in DEF_SLOTS: + for name in (*DEF_SLOTS, 'Py_mod_exec'): with self.subTest(name): spec = FakeSpec() spec._test_slot_id = getattr(_testcapi, name) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index b71a36ec2f7aab..b93deca8980ca4 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -2467,6 +2467,21 @@ def test_multi_init_extension_per_interpreter_gil_compat(self): self.check_compatible_here( modname, filename, strict=False, isolated=False) + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_testmultiphase_exec_multiple(self): + modname = '_testmultiphase_exec_multiple' + filename = _testmultiphase.__file__ + module = import_extension_from_file(modname, filename, + put_in_sys_modules=False) + # All three exec's were called. + self.assertEqual(module.a, 1) + self.assertEqual(module.b, 2) + self.assertEqual(module.c, 3) + # They were called in order. + keys = list(module.__dict__) + self.assertLess(keys.index('a'), keys.index('b')) + self.assertLess(keys.index('b'), keys.index('c')) + @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") def test_python_compat(self): module = 'threading' diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 6041a9fe532fba..b6ccdc75e51e79 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -130,6 +130,36 @@ module_from_slots_token(PyObject *self, PyObject *spec) return mod; } +static int +simple_exec(PyObject *module) +{ + return PyModule_AddIntConstant(module, "a_number", 456); +} + +static PyObject * +module_from_slots_exec(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_exec, simple_exec}, + {0}, + }; + PyObject *mod = PyModule_FromSlotsAndSpec(slots, spec); + if (!mod) { + return NULL; + } + int res = PyObject_HasAttrStringWithError(mod, "a_number"); + if (res < 0) { + Py_DECREF(mod); + return NULL; + } + assert(res == 0); + if (PyModule_Exec(mod) < 0) { + Py_DECREF(mod); + return NULL; + } + return mod; +} + static int slot_from_object(PyObject *obj) @@ -201,6 +231,7 @@ static PyMethodDef test_methods[] = { {"module_from_slots_methods", module_from_slots_methods, METH_O}, {"module_from_slots_gc", module_from_slots_gc, METH_O}, {"module_from_slots_token", module_from_slots_token, METH_O}, + {"module_from_slots_exec", module_from_slots_exec, METH_O}, {"module_from_slots_repeat_slot", module_from_slots_repeat_slot, METH_O}, {"module_from_slots_null_slot", module_from_slots_null_slot, METH_O}, {"module_from_def_slot", module_from_def_slot, METH_O}, diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index 991cb7ca528f7d..607b805ffc24d7 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -850,6 +850,27 @@ PyInit__testmultiphase_exec_unreported_exception(void) return PyModuleDef_Init(&def_exec_unreported_exception); } +static int execfunc_a1(PyObject*m) {return PyModule_AddIntConstant(m, "a", 1);} +static int execfunc_b2(PyObject*m) {return PyModule_AddIntConstant(m, "b", 2);} +static int execfunc_c3(PyObject*m) {return PyModule_AddIntConstant(m, "c", 3);} + +PyMODINIT_FUNC +PyInit__testmultiphase_exec_multiple(void) +{ + static PyModuleDef_Slot slots[] = { + {Py_mod_exec, execfunc_a1}, + {Py_mod_exec, execfunc_b2}, + {Py_mod_exec, execfunc_c3}, + {0} + }; + static PyModuleDef def = { + PyModuleDef_HEAD_INIT, + .m_name="_testmultiphase_exec_multiple", + .m_slots=slots, + }; + return PyModuleDef_Init(&def); +} + static int meth_state_access_exec(PyObject *m) { diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index e4573cd1403b07..c9c5d238fb0ba9 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -642,71 +642,93 @@ PyUnstable_Module_SetGIL(PyObject *module, void *gil) } #endif -int -PyModule_ExecDef(PyObject *module, PyModuleDef *def) +static int +run_exec_func(PyObject *module, int (*exec)(PyObject *)) { - PyModuleDef_Slot *cur_slot; - const char *name; - int ret; + int ret = exec(module); + if (ret != 0) { + if (!PyErr_Occurred()) { + PyErr_Format( + PyExc_SystemError, + "execution of %S failed without setting an exception", + module); + } + return -1; + } + if (PyErr_Occurred()) { + _PyErr_FormatFromCause( + PyExc_SystemError, + "execution of module %S raised unreported exception", + module); + return -1; + } + return 0; +} - name = PyModule_GetName(module); - if (name == NULL) { +static int +alloc_state(PyObject *module) +{ + if(!PyModule_Check(module)) { + PyErr_Format(PyExc_TypeError, "expected module, got %T", module); return -1; } + PyModuleObject *md = (PyModuleObject*)module; - if (def->m_size >= 0) { - PyModuleObject *md = (PyModuleObject*)module; + if (md->md_size >= 0) { if (md->md_state == NULL) { /* Always set a state pointer; this serves as a marker to skip * multiple initialization (importlib.reload() is no-op) */ - md->md_state = PyMem_Malloc(def->m_size); + md->md_state = PyMem_Malloc(md->md_size); if (!md->md_state) { PyErr_NoMemory(); return -1; } - memset(md->md_state, 0, def->m_size); + memset(md->md_state, 0, md->md_size); } } + return 0; +} + +int +PyModule_Exec(PyObject *module) +{ + if (alloc_state(module) < 0) { + return -1; + } + PyModuleObject *md = (PyModuleObject*)module; + if (md->md_exec) { + assert(!md->md_def_or_null); + return run_exec_func(module, md->md_exec); + } + + if (md->md_def_or_null) { + return PyModule_ExecDef(module, md->md_def_or_null); + } + return 0; +} + +int +PyModule_ExecDef(PyObject *module, PyModuleDef *def) +{ + PyModuleDef_Slot *cur_slot; + + if (alloc_state(module) < 0) { + return -1; + } + + assert(PyModule_Check(module)); if (def->m_slots == NULL) { return 0; } for (cur_slot = def->m_slots; cur_slot && cur_slot->slot; cur_slot++) { - switch (cur_slot->slot) { - case Py_mod_create: - /* handled in PyModule_FromDefAndSpec2 */ - break; - case Py_mod_exec: - ret = ((int (*)(PyObject *))cur_slot->value)(module); - if (ret != 0) { - if (!PyErr_Occurred()) { - PyErr_Format( - PyExc_SystemError, - "execution of module %s failed without setting an exception", - name); - } - return -1; - } - if (PyErr_Occurred()) { - _PyErr_FormatFromCause( - PyExc_SystemError, - "execution of module %s raised unreported exception", - name); - return -1; - } - break; - case Py_mod_multiple_interpreters: - case Py_mod_gil: - case Py_mod_abi: - /* handled in PyModule_FromDefAndSpec2 */ - break; - default: - PyErr_Format( - PyExc_SystemError, - "module %s initialized with unknown slot %i", - name, cur_slot->slot); + if (cur_slot->slot == Py_mod_exec) { + int (*func)(PyObject *) = cur_slot->value; + if (run_exec_func(module, func) < 0) { return -1; + } + continue; } } return 0; From f9d0ddebaa2550b86380efd25b579cf17689299f Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 15:04:43 +0200 Subject: [PATCH 29/81] Clean up slot-handling a bit --- Include/internal/pycore_moduleobject.h | 4 +- Objects/moduleobject.c | 91 ++++++++++++++++---------- 2 files changed, 59 insertions(+), 36 deletions(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index 430eeced831244..65c27f4bab614c 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -16,6 +16,8 @@ extern int _PyModule_IsPossiblyShadowing(PyObject *); extern int _PyModule_IsExtension(PyObject *obj); +typedef int (*_Py_modexecfunc)(PyObject *); + typedef struct { PyObject_HEAD PyObject *md_dict; @@ -34,7 +36,7 @@ typedef struct { inquiry md_clear; freefunc md_free; void *md_token; - int (*md_exec)(PyObject *); + _Py_modexecfunc md_exec; /* only set if md_def_or_null is NULL */ } PyModuleObject; static inline PyModuleDef* _PyModule_GetDefOrNull(PyObject *mod) { diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index c9c5d238fb0ba9..77d99d4d41781b 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -367,6 +367,7 @@ module_from_def_and_spec( const char *name; int ret; void *token = NULL; + _Py_modexecfunc m_exec = NULL; PyInterpreterState *interp = _PyInterpreterState_GET(); nameobj = PyObject_GetAttrString(spec, "name"); @@ -391,6 +392,32 @@ module_from_def_and_spec( } for (cur_slot = def_like->m_slots; cur_slot && cur_slot->slot; cur_slot++) { + // Macro to copy a non-NULL, non-repeatable slot that's unusable with + // PyModuleDef. The destination must be initially NULL. +#define COPY_COMMON_SLOT(SLOT, TYPE, DEST) \ + if (!(TYPE)(cur_slot->value)) { \ + PyErr_Format( \ + PyExc_SystemError, \ + "module %s: " #SLOT " must not be NULL", \ + name); \ + goto error; \ + } \ + if (original_def) { \ + PyErr_Format( \ + PyExc_SystemError, \ + "module %s: " #SLOT " used with PyModuleDef", \ + name); \ + goto error; \ + } \ + if (DEST) { \ + PyErr_Format( \ + PyExc_SystemError, \ + "module %s: " #SLOT " slot repeated", \ + name); \ + goto error; \ + } \ + DEST = (TYPE)(cur_slot->value); \ + ///////////////////////////////////////////////////////////// switch (cur_slot->slot) { case Py_mod_create: if (create) { @@ -404,6 +431,9 @@ module_from_def_and_spec( break; case Py_mod_exec: has_execution_slots = 1; + if (!original_def) { + COPY_COMMON_SLOT(Py_mod_exec, _Py_modexecfunc, m_exec); + } break; case Py_mod_multiple_interpreters: if (has_multiple_interpreters_slot) { @@ -432,43 +462,32 @@ module_from_def_and_spec( goto error; } break; -#define COPY_SLOT_TO_DEFLIKE(SLOT, TYPE, DEST) \ - case SLOT: \ - if (!(TYPE)(cur_slot->value)) { \ - PyErr_Format( \ - PyExc_SystemError, \ - "module %s: " #SLOT " must not be NULL", \ - name); \ - goto error; \ - } \ - if (original_def) { \ - PyErr_Format( \ - PyExc_SystemError, \ - "module %s: " #SLOT " used with PyModuleDef", \ - name); \ - goto error; \ - } \ - if (DEST) { \ - PyErr_Format( \ - PyExc_SystemError, \ - "module %s: " #SLOT " slot repeated", \ - name); \ - goto error; \ - } \ - DEST = (TYPE)(cur_slot->value); \ - break; \ - ///////////////////////////////////////////////////////////// - COPY_SLOT_TO_DEFLIKE(Py_mod_name, char*, def_like->m_name); - COPY_SLOT_TO_DEFLIKE(Py_mod_doc, char*, def_like->m_doc); - COPY_SLOT_TO_DEFLIKE(Py_mod_size, Py_ssize_t, def_like->m_size); - COPY_SLOT_TO_DEFLIKE(Py_mod_methods, PyMethodDef*, + case Py_mod_name: + COPY_COMMON_SLOT(Py_mod_name, char*, def_like->m_name); + break; + case Py_mod_doc: + COPY_COMMON_SLOT(Py_mod_doc, char*, def_like->m_doc); + break; + case Py_mod_size: + COPY_COMMON_SLOT(Py_mod_size, Py_ssize_t, def_like->m_size); + break; + case Py_mod_methods: + COPY_COMMON_SLOT(Py_mod_methods, PyMethodDef*, def_like->m_methods); - COPY_SLOT_TO_DEFLIKE(Py_mod_traverse, traverseproc, + break; + case Py_mod_traverse: + COPY_COMMON_SLOT(Py_mod_traverse, traverseproc, def_like->m_traverse); - COPY_SLOT_TO_DEFLIKE(Py_mod_clear, inquiry, def_like->m_clear); - COPY_SLOT_TO_DEFLIKE(Py_mod_free, freefunc, def_like->m_free); - COPY_SLOT_TO_DEFLIKE(Py_mod_token, void*, token); -#undef COPY_SLOT_TO_DEFLIKE + break; + case Py_mod_clear: + COPY_COMMON_SLOT(Py_mod_clear, inquiry, def_like->m_clear); + break; + case Py_mod_free: + COPY_COMMON_SLOT(Py_mod_free, freefunc, def_like->m_free); + break; + case Py_mod_token: + COPY_COMMON_SLOT(Py_mod_token, void*, token); + break; default: assert(cur_slot->slot < 0 || cur_slot->slot > _Py_mod_LAST_SLOT); PyErr_Format( @@ -477,6 +496,7 @@ module_from_def_and_spec( name, cur_slot->slot); goto error; } +#undef COPY_COMMON_SLOT } /* By default, multi-phase init modules are expected @@ -544,6 +564,7 @@ module_from_def_and_spec( else { mod->md_token = token; } + mod->md_exec = m_exec; } else { if (def_like->m_size > 0 || def_like->m_traverse || def_like->m_clear || def_like->m_free) From 020dcc6c4daca3d2443171d692bde9e1c087867a Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 15:21:24 +0200 Subject: [PATCH 30/81] Py_mod_create --- Lib/test/test_capi/test_module.py | 19 +++++++++++++------ Modules/_testcapi/module.c | 18 ++++++++++++++++++ Objects/moduleobject.c | 4 ++-- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 7ad1a92ff73513..677a59e56bc9aa 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -69,6 +69,13 @@ def test_exec(self): self.assertEqual(mod.__doc__, None) self.assertEqual(mod.a_number, 456) + def test_create(self): + spec = FakeSpec() + spec._gimme_this = "not a module object" + mod = _testcapi.module_from_slots_create(spec) + self.assertIsInstance(mod, str) + self.assertEqual(mod, "not a module object") + def test_def_slot(self): """Slots that replace PyModuleDef fields can't be used with PyModuleDef """ @@ -78,8 +85,8 @@ def test_def_slot(self): spec._test_slot_id = getattr(_testcapi, name) with self.assertRaises(SystemError) as cm: _testcapi.module_from_def_slot(spec) - self.assertIn(name, str(cm.exception),) - self.assertIn("PyModuleDef", str(cm.exception), ) + self.assertIn(name, str(cm.exception)) + self.assertIn("PyModuleDef", str(cm.exception)) def test_repeated_def_slot(self): """Slots that replace PyModuleDef fields can't be repeated""" @@ -89,8 +96,8 @@ def test_repeated_def_slot(self): spec._test_slot_id = getattr(_testcapi, name) with self.assertRaises(SystemError) as cm: _testcapi.module_from_slots_repeat_slot(spec) - self.assertIn(name, str(cm.exception),) - self.assertIn("repeated", str(cm.exception), ) + self.assertIn(name, str(cm.exception)) + self.assertIn("more than one", str(cm.exception)) def test_null_def_slot(self): """Slots that replace PyModuleDef fields can't be NULL""" @@ -100,5 +107,5 @@ def test_null_def_slot(self): spec._test_slot_id = getattr(_testcapi, name) with self.assertRaises(SystemError) as cm: _testcapi.module_from_slots_null_slot(spec) - self.assertIn(name, str(cm.exception),) - self.assertIn("NULL", str(cm.exception), ) + self.assertIn(name, str(cm.exception)) + self.assertIn("NULL", str(cm.exception)) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index b6ccdc75e51e79..2e72bdaad78244 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -160,6 +160,23 @@ module_from_slots_exec(PyObject *self, PyObject *spec) return mod; } +static PyObject * +create_attr_from_spec(PyObject *spec, PyObject *def) +{ + assert(!def); + return PyObject_GetAttrString(spec, "_gimme_this"); +} + +static PyObject * +module_from_slots_create(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_create, create_attr_from_spec}, + {0}, + }; + return PyModule_FromSlotsAndSpec(slots, spec); +} + static int slot_from_object(PyObject *obj) @@ -232,6 +249,7 @@ static PyMethodDef test_methods[] = { {"module_from_slots_gc", module_from_slots_gc, METH_O}, {"module_from_slots_token", module_from_slots_token, METH_O}, {"module_from_slots_exec", module_from_slots_exec, METH_O}, + {"module_from_slots_create", module_from_slots_create, METH_O}, {"module_from_slots_repeat_slot", module_from_slots_repeat_slot, METH_O}, {"module_from_slots_null_slot", module_from_slots_null_slot, METH_O}, {"module_from_def_slot", module_from_def_slot, METH_O}, diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 77d99d4d41781b..30340362721a0c 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -412,7 +412,7 @@ module_from_def_and_spec( if (DEST) { \ PyErr_Format( \ PyExc_SystemError, \ - "module %s: " #SLOT " slot repeated", \ + "module %s has more than one " #SLOT " slot", \ name); \ goto error; \ } \ @@ -520,7 +520,7 @@ module_from_def_and_spec( } if (create) { - m = create(spec, def_like); + m = create(spec, original_def); if (m == NULL) { if (!PyErr_Occurred()) { PyErr_Format( From cf7409eec78c6a5fa1a715cc25bd5fd8d97b43f3 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 15:36:29 +0200 Subject: [PATCH 31/81] Use PyModule_Exec in exec_builtin_or_dynamic --- Include/internal/pycore_moduleobject.h | 2 +- Python/import.c | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index 65c27f4bab614c..7500ef6600b570 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -22,7 +22,7 @@ typedef struct { PyObject_HEAD PyObject *md_dict; // The PyModuleDef used to define the module, if any. - // (used to be `md_def` when all extension modules had one) + // (used to be `md_def`; renamed because all uses need inspection) PyModuleDef *md_def_or_null; void *md_state; PyObject *md_weaklist; diff --git a/Python/import.c b/Python/import.c index f2c82315aa01b6..083d9bd02d468f 100644 --- a/Python/import.c +++ b/Python/import.c @@ -839,25 +839,19 @@ _PyImport_SetDLOpenFlags(PyInterpreterState *interp, int new_val) /* Common implementation for _imp.exec_dynamic and _imp.exec_builtin */ static int exec_builtin_or_dynamic(PyObject *mod) { - PyModuleDef *def; void *state; if (!PyModule_Check(mod)) { return 0; } - def = PyModule_GetDef(mod); - if (def == NULL) { - return 0; - } - state = PyModule_GetState(mod); if (state) { /* Already initialized; skip reload */ return 0; } - return PyModule_ExecDef(mod, def); + return PyModule_Exec(mod); } From d1673de22ab0805dc43fea7c36110fed1f18dfb9 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 15:53:18 +0200 Subject: [PATCH 32/81] PyModExport --- Include/internal/pycore_importdl.h | 17 +++-- Lib/test/test_capi/test_modexport.py | 29 --------- Lib/test/test_import/__init__.py | 9 +++ Python/import.c | 75 ++++++++++++++++++++-- Python/importdl.c | 95 ++++++++++++++++++---------- 5 files changed, 152 insertions(+), 73 deletions(-) delete mode 100644 Lib/test/test_capi/test_modexport.py diff --git a/Include/internal/pycore_importdl.h b/Include/internal/pycore_importdl.h index 3ba9229cc21378..75a32b607dfbbc 100644 --- a/Include/internal/pycore_importdl.h +++ b/Include/internal/pycore_importdl.h @@ -28,6 +28,11 @@ typedef enum ext_module_origin { _Py_ext_module_origin_DYNAMIC = 3, } _Py_ext_module_origin; +struct hook_prefixes { + const char *const init_prefix; + const char *const export_prefix; +}; + /* Input for loading an extension module. */ struct _Py_ext_module_loader_info { PyObject *filename; @@ -40,7 +45,7 @@ struct _Py_ext_module_loader_info { * depending on if it's builtin or not. */ PyObject *path; _Py_ext_module_origin origin; - const char *hook_prefix; + const struct hook_prefixes *hook_prefixes; const char *newcontext; }; extern void _Py_ext_module_loader_info_clear( @@ -62,7 +67,9 @@ extern int _Py_ext_module_loader_info_init_from_spec( PyObject *spec); #endif -/* The result from running an extension module's init function. */ +/* The result from running an extension module's init function. + * Not used for modules defined via PyModExport (slots array). + */ struct _Py_ext_module_loader_result { PyModuleDef *def; PyObject *module; @@ -89,10 +96,12 @@ extern void _Py_ext_module_loader_result_apply_error( /* The module init function. */ typedef PyObject *(*PyModInitFunction)(void); +typedef PyModuleDef_Slot *(*PyModExportFunction)(PyObject *spec); #ifdef HAVE_DYNAMIC_LOADING -extern PyModInitFunction _PyImport_GetModInitFunc( +// function changed signature, the "2" suffix helps avoid ABI issues +extern int _PyImport_GetModInitFunc2( struct _Py_ext_module_loader_info *info, - FILE *fp); + FILE *fp, PyModInitFunction *modinit, PyModExportFunction *modexport); #endif extern int _PyImport_RunModInitFunc( PyModInitFunction p0, diff --git a/Lib/test/test_capi/test_modexport.py b/Lib/test/test_capi/test_modexport.py deleted file mode 100644 index b89e1ab3109194..00000000000000 --- a/Lib/test/test_capi/test_modexport.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest -import importlib -import sys -from importlib.machinery import ExtensionFileLoader - -try: - import _testmultiphase -except ImportError: - _testmultiphase = None - -def import_extension_from_file(modname, filename, *, put_in_sys_modules=True): - loader = ExtensionFileLoader(modname, filename) - spec = importlib.util.spec_from_loader(modname, loader) - module = importlib.util.module_from_spec(spec) - loader.exec_module(module) - if put_in_sys_modules: - sys.modules[modname] = module - return module - - -class TestModExport(unittest.TestCase): - @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") - def test_modexport(self): - modname = '_test_modexport' - filename = _testmultiphase.__file__ - module = import_extension_from_file(modname, filename, - put_in_sys_modules=False) - - self.assertEqual(module.__name__, modname) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index b93deca8980ca4..029369ce11ef23 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -2482,6 +2482,15 @@ def test_testmultiphase_exec_multiple(self): self.assertLess(keys.index('a'), keys.index('b')) self.assertLess(keys.index('b'), keys.index('c')) + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_from_modexport(self): + modname = '_test_from_modexport' + filename = _testmultiphase.__file__ + module = import_extension_from_file(modname, filename, + put_in_sys_modules=False) + + self.assertEqual(module.__name__, modname) + @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") def test_python_compat(self): module = 'threading' diff --git a/Python/import.c b/Python/import.c index 083d9bd02d468f..094e83d647e2f2 100644 --- a/Python/import.c +++ b/Python/import.c @@ -743,7 +743,7 @@ _PyImport_ClearModulesByIndex(PyInterpreterState *interp) A. noop - ...for multi-phase init modules: + ...for multi-phase init modules from PyModInit_* (PyModuleDef): (6). every time: A. _imp_create_dynamic_impl() -> import_find_extension() (not found) @@ -753,6 +753,9 @@ _PyImport_ClearModulesByIndex(PyInterpreterState *interp) E. import_run_extension() -> _PyImport_RunModInitFunc() F. _PyImport_RunModInitFunc(): call G. import_run_extension() -> PyModule_FromDefAndSpec() + + PyModule_FromDefAndSpec(): + H. PyModule_FromDefAndSpec(): gather/check moduledef slots I. if there's a Py_mod_create slot: 1. PyModule_FromDefAndSpec(): call its function @@ -765,10 +768,29 @@ _PyImport_ClearModulesByIndex(PyInterpreterState *interp) (10). every time: A. _imp_exec_dynamic_impl() -> exec_builtin_or_dynamic() B. if mod->md_state == NULL (including if m_size == 0): - 1. exec_builtin_or_dynamic() -> PyModule_ExecDef() - 2. PyModule_ExecDef(): allocate mod->md_state + 1. exec_builtin_or_dynamic() -> PyModule_Exec() + 2. PyModule_Exec(): allocate mod->md_state 3. if there's a Py_mod_exec slot: - 1. PyModule_ExecDef(): call its function + 1. PyModule_Exec(): call its function + + + ...for multi-phase init modules from PyModExport_* (slots array): + + (6). every time: + + A. _imp_create_dynamic_impl() -> import_find_extension() (not found) + B. _imp_create_dynamic_impl() -> _PyImport_GetModInitFunc() + C. _PyImport_GetModInitFunc(): load + D. _imp_create_dynamic_impl() -> import_run_modexport() + E. import_run_modexport(): call + F. import_run_modexport() -> PyModule_FromSlotsAndSpec() + G. PyModule_FromSlotsAndSpec(): create temporary PyModuleDef-like + H. PyModule_FromSlotsAndSpec() -> PyModule_FromDefAndSpec() + + (PyModule_FromDefAndSpec behaves as for PyModInit_*, above) + + (10). every time: as for PyModInit_*, above + */ @@ -1961,6 +1983,42 @@ import_find_extension(PyThreadState *tstate, return mod; } +static PyObject * +import_run_modexport(PyThreadState *tstate, PyModExportFunction ex0, + struct _Py_ext_module_loader_info *info, + PyObject *spec) +{ + /* This is like import_run_extension, but avoids interpreter switching + * and code for for single-phase modules. + */ + PyModuleDef_Slot *slots = ex0(spec); + if (!slots) { + if (!PyErr_Occurred()) { + PyErr_Format( + PyExc_SystemError, + "slot export function for module %s failed without setting an exception", + info->name); + } + return NULL; + } + if (PyErr_Occurred()) { + PyErr_Format( + PyExc_SystemError, + "slot export function for module %s raised unreported exception", + info->name); + } + PyObject *result = PyModule_FromSlotsAndSpec(slots, spec); + if (!result) { + return NULL; + } + assert(PyModule_Check(result)); + PyModuleObject *mod = (PyModuleObject *)result; + if (mod && !mod->md_token) { + mod->md_token = slots; + } + return result; +} + static PyObject * import_run_extension(PyThreadState *tstate, PyModInitFunction p0, struct _Py_ext_module_loader_info *info, @@ -4701,7 +4759,13 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file) fp = NULL; } - PyModInitFunction p0 = _PyImport_GetModInitFunc(&info, fp); + PyModInitFunction p0 = NULL; + PyModExportFunction ex0 = NULL; + _PyImport_GetModInitFunc2(&info, fp, &p0, &ex0); + if (ex0) { + mod = import_run_modexport(tstate, ex0, &info, spec); + goto cleanup; + } if (p0 == NULL) { goto finally; } @@ -4723,6 +4787,7 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file) } #endif +cleanup: // XXX Shouldn't this happen in the error cases too (i.e. in "finally")? if (fp) { fclose(fp); diff --git a/Python/importdl.c b/Python/importdl.c index b640f649aa1c65..02f50c0c970708 100644 --- a/Python/importdl.c +++ b/Python/importdl.c @@ -5,7 +5,7 @@ #include "pycore_call.h" // _PyObject_CallMethod() #include "pycore_import.h" // _PyImport_SwapPackageContext() #include "pycore_importdl.h" -#include "pycore_moduleobject.h" // _PyModule_GetDef() +#include "pycore_moduleobject.h" // _PyModule_GetDefOrNull() #include "pycore_pyerrors.h" // _PyErr_FormatFromCause() #include "pycore_runtime.h" // _Py_ID() @@ -35,8 +35,10 @@ extern dl_funcptr _PyImport_FindSharedFuncptr(const char *prefix, /* module info to use when loading */ /***********************************/ -static const char * const ascii_only_prefix = "PyInit"; -static const char * const nonascii_prefix = "PyInitU"; +static const struct hook_prefixes ascii_only_prefixes = { + "PyInit", "PyModExport"}; +static const struct hook_prefixes nonascii_prefixes = { + "PyInitU", "PyModExportU"}; /* Get the variable part of a module's export symbol name. * Returns a bytes instance. For non-ASCII-named modules, the name is @@ -45,7 +47,7 @@ static const char * const nonascii_prefix = "PyInitU"; * nonascii_prefix, as appropriate. */ static PyObject * -get_encoded_name(PyObject *name, const char **hook_prefix) { +get_encoded_name(PyObject *name, const struct hook_prefixes **hook_prefixes) { PyObject *tmp; PyObject *encoded = NULL; PyObject *modname = NULL; @@ -72,7 +74,7 @@ get_encoded_name(PyObject *name, const char **hook_prefix) { /* Encode to ASCII or Punycode, as needed */ encoded = PyUnicode_AsEncodedString(name, "ascii", NULL); if (encoded != NULL) { - *hook_prefix = ascii_only_prefix; + *hook_prefixes = &ascii_only_prefixes; } else { if (PyErr_ExceptionMatches(PyExc_UnicodeEncodeError)) { PyErr_Clear(); @@ -80,7 +82,7 @@ get_encoded_name(PyObject *name, const char **hook_prefix) { if (encoded == NULL) { goto error; } - *hook_prefix = nonascii_prefix; + *hook_prefixes = &nonascii_prefixes; } else { goto error; } @@ -130,7 +132,7 @@ _Py_ext_module_loader_info_init(struct _Py_ext_module_loader_info *p_info, assert(PyUnicode_GetLength(name) > 0); info.name = Py_NewRef(name); - info.name_encoded = get_encoded_name(info.name, &info.hook_prefix); + info.name_encoded = get_encoded_name(info.name, &info.hook_prefixes); if (info.name_encoded == NULL) { _Py_ext_module_loader_info_clear(&info); return -1; @@ -189,7 +191,7 @@ _Py_ext_module_loader_info_init_for_builtin( /* We won't need filename. */ .path=name, .origin=_Py_ext_module_origin_BUILTIN, - .hook_prefix=ascii_only_prefix, + .hook_prefixes=&ascii_only_prefixes, .newcontext=NULL, }; return 0; @@ -377,39 +379,62 @@ _Py_ext_module_loader_result_apply_error( /********************************************/ #ifdef HAVE_DYNAMIC_LOADING -PyModInitFunction -_PyImport_GetModInitFunc(struct _Py_ext_module_loader_info *info, - FILE *fp) +static dl_funcptr +findfuncptr(const char *prefix, const char *name_buf, + struct _Py_ext_module_loader_info *info, + FILE *fp) { - const char *name_buf = PyBytes_AS_STRING(info->name_encoded); - dl_funcptr exportfunc; #ifdef MS_WINDOWS - exportfunc = _PyImport_FindSharedFuncptrWindows( - info->hook_prefix, name_buf, info->filename, fp); + return _PyImport_FindSharedFuncptrWindows( + prefix, name_buf, info->filename, fp); #else - { - const char *path_buf = PyBytes_AS_STRING(info->filename_encoded); - exportfunc = _PyImport_FindSharedFuncptr( - info->hook_prefix, name_buf, path_buf, fp); - } + const char *path_buf = PyBytes_AS_STRING(info->filename_encoded); + return _PyImport_FindSharedFuncptr( + prefix, name_buf, path_buf, fp); #endif +} - if (exportfunc == NULL) { - if (!PyErr_Occurred()) { - PyObject *msg; - msg = PyUnicode_FromFormat( - "dynamic module does not define " - "module export function (%s_%s)", - info->hook_prefix, name_buf); - if (msg != NULL) { - PyErr_SetImportError(msg, info->name, info->filename); - Py_DECREF(msg); - } - } - return NULL; +int +_PyImport_GetModInitFunc2(struct _Py_ext_module_loader_info *info, + FILE *fp, + PyModInitFunction *modinit, + PyModExportFunction *modexport) +{ + *modinit = NULL; + *modexport = NULL; + + const char *name_buf = PyBytes_AS_STRING(info->name_encoded); + dl_funcptr exportfunc; + + exportfunc = findfuncptr( + info->hook_prefixes->export_prefix, + name_buf, info, fp); + if (exportfunc) { + *modexport = (PyModExportFunction)exportfunc; + return 2; + } + + exportfunc = findfuncptr( + info->hook_prefixes->init_prefix, + name_buf, info, fp); + if (exportfunc) { + *modinit = (PyModInitFunction)exportfunc; + return 1; } - return (PyModInitFunction)exportfunc; + if (!PyErr_Occurred()) { + PyObject *msg; + msg = PyUnicode_FromFormat( + "dynamic module does not define " + "module export function (%s_%s or %s_%s)", + info->hook_prefixes->export_prefix, name_buf, + info->hook_prefixes->init_prefix, name_buf); + if (msg != NULL) { + PyErr_SetImportError(msg, info->name, info->filename); + Py_DECREF(msg); + } + } + return -1; } #endif /* HAVE_DYNAMIC_LOADING */ @@ -477,7 +502,7 @@ _PyImport_RunModInitFunc(PyModInitFunction p0, res.def = (PyModuleDef *)m; /* Run PyModule_FromDefAndSpec() to finish loading the module. */ } - else if (info->hook_prefix == nonascii_prefix) { + else if (info->hook_prefixes == &nonascii_prefixes) { /* Non-ASCII is only supported for multi-phase init. */ res.kind = _Py_ext_module_kind_MULTIPHASE; /* Don't allow legacy init for non-ASCII module names. */ From c3dd9358b89071a53c6c0ea01add2bfd282e9635 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 16:48:18 +0200 Subject: [PATCH 33/81] _PyModule_IsExtension: Check md_exec --- Objects/moduleobject.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 30340362721a0c..679516a649ed4c 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -56,6 +56,9 @@ _PyModule_IsExtension(PyObject *obj) } PyModuleObject *module = (PyModuleObject*)obj; + if (module->md_exec) { + return 1; + } PyModuleDef *def = module->md_def_or_null; return (def != NULL && def->m_methods != NULL); } From 1de2c5071131e068a47a94372f715e191e6ce0c4 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 22 May 2025 16:50:38 +0200 Subject: [PATCH 34/81] Small style/doc fixups --- Modules/_testsinglephase.c | 2 ++ Objects/moduleobject.c | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Modules/_testsinglephase.c b/Modules/_testsinglephase.c index 2c59085d15b5be..ee38d61b43a82a 100644 --- a/Modules/_testsinglephase.c +++ b/Modules/_testsinglephase.c @@ -244,6 +244,8 @@ static inline module_state * get_module_state(PyObject *module) { PyModuleDef *def = PyModule_GetDef(module); + assert(def); + if (def->m_size == -1) { return &global_state.module; } diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 679516a649ed4c..c304916a89c24a 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -8,7 +8,7 @@ #include "pycore_interp.h" // PyInterpreterState.importlib #include "pycore_long.h" // _PyLong_GetOne() #include "pycore_modsupport.h" // _PyModule_CreateInitialized() -#include "pycore_moduleobject.h" // _PyModule_GetDef() +#include "pycore_moduleobject.h" // _PyModule_GetDefOrNull() #include "pycore_object.h" // _PyType_AllocNoTrack #include "pycore_pyerrors.h" // _PyErr_FormatFromCause() #include "pycore_pystate.h" // _PyInterpreterState_GET() @@ -1110,8 +1110,9 @@ module_dealloc(PyObject *self) Py_XDECREF(m->md_dict); Py_XDECREF(m->md_name); - if (m->md_state != NULL) + if (m->md_state != NULL) { PyMem_Free(m->md_state); + } Py_TYPE(m)->tp_free((PyObject *)m); } @@ -1447,8 +1448,9 @@ module_clear(PyObject *self) m->md_name ? " " : "", m->md_name, ""); } - if (res) + if (res) { return res; + } } Py_CLEAR(m->md_dict); return 0; From e486cdcd5765769200bbde2a6d463c455f4195e0 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 23 May 2025 15:06:08 +0200 Subject: [PATCH 35/81] Handle NULL slots & bad spec; some light refactoring --- Lib/test/test_capi/test_module.py | 13 +++++++++++++ Modules/_testcapi/module.c | 11 +++++++++++ Objects/moduleobject.c | 23 ++++++----------------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 677a59e56bc9aa..51bdff9491d190 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -14,6 +14,8 @@ class FakeSpec: 'Py_mod_traverse', 'Py_mod_clear', 'Py_mod_free', 'Py_mod_token', ) +# The C functions used by this module are in: +# Modules/_testcapi/module.c class TestModFromSlotsAndSpec(unittest.TestCase): def test_empty(self): @@ -21,6 +23,17 @@ def test_empty(self): self.assertIsInstance(mod, types.ModuleType) self.assertEqual(mod.__name__, 'testmod') + def test_null_slots(self): + with self.assertRaises(SystemError): + _testcapi.module_from_slots_null(FakeSpec()) + + def test_none_spec(self): + # The spec currently must contain a name + with self.assertRaises(AttributeError): + _testcapi.module_from_slots_empty(None) + with self.assertRaises(AttributeError): + _testcapi.module_from_slots_name(None) + def test_name(self): # Py_mod_name (and PyModuleDef.m_name) are currently ignored when # spec is given. diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 2e72bdaad78244..443bb78df478bb 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -3,6 +3,10 @@ // Test PyModule_* API +/* unittest Cases that use these functions are in: + * Lib/test/test_capi/test_module.py + */ + static PyObject * module_from_slots_empty(PyObject *self, PyObject *spec) { @@ -12,6 +16,12 @@ module_from_slots_empty(PyObject *self, PyObject *spec) return PyModule_FromSlotsAndSpec(slots, spec); } +static PyObject * +module_from_slots_null(PyObject *self, PyObject *spec) +{ + return PyModule_FromSlotsAndSpec(NULL, spec); +} + static PyObject * module_from_slots_name(PyObject *self, PyObject *spec) { @@ -242,6 +252,7 @@ module_from_def_slot(PyObject *self, PyObject *spec) static PyMethodDef test_methods[] = { {"module_from_slots_empty", module_from_slots_empty, 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}, {"module_from_slots_size", module_from_slots_size, METH_O}, diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index c304916a89c24a..d11ed7c0ab44e0 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -629,30 +629,19 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio PyObject * PyModule_FromSlotsAndSpec(PyModuleDef_Slot *slots, PyObject *spec) { - PyObject *result = NULL; if (!slots) { - PyErr_BadArgument(); - } - PyObject *nameobj = PyObject_GetAttrString(spec, "name"); - if (nameobj == NULL) { - goto finally; - } - const char *name = PyUnicode_AsUTF8(nameobj); - if (name == NULL) { - goto finally; + PyErr_Format( + PyExc_SystemError, + "PyModule_FromSlotsAndSpec called with NULL slots"); + return NULL; } - // Fill in enough of a PyModuleDef to pass to common machinery PyModuleDef def_like = {.m_slots = slots}; - result = module_from_def_and_spec(&def_like, spec, PYTHON_API_VERSION, - NULL); -finally: - Py_XDECREF(nameobj); - return result; + return module_from_def_and_spec(&def_like, spec, PYTHON_API_VERSION, + NULL); } - #ifdef Py_GIL_DISABLED int PyUnstable_Module_SetGIL(PyObject *module, void *gil) From 31a5cc54432e7443bcc1a7bd34979b73c7bb7bee Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 23 May 2025 15:57:09 +0200 Subject: [PATCH 36/81] Allow non-Module instances --- Python/import.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Python/import.c b/Python/import.c index 094e83d647e2f2..32f7a5206f175e 100644 --- a/Python/import.c +++ b/Python/import.c @@ -2011,10 +2011,11 @@ import_run_modexport(PyThreadState *tstate, PyModExportFunction ex0, if (!result) { return NULL; } - assert(PyModule_Check(result)); - PyModuleObject *mod = (PyModuleObject *)result; - if (mod && !mod->md_token) { - mod->md_token = slots; + if (PyModule_Check(result)) { + PyModuleObject *mod = (PyModuleObject *)result; + if (mod && !mod->md_token) { + mod->md_token = slots; + } } return result; } From 3dcadbb9086b39b4370c14368bb8ef00f745d840 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 23 May 2025 15:57:34 +0200 Subject: [PATCH 37/81] Tests for the export hook --- Lib/test/test_import/__init__.py | 36 ++++++++++ Modules/_testmultiphase.c | 120 ++++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 029369ce11ef23..c2064e979c3988 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -2491,6 +2491,42 @@ def test_from_modexport(self): self.assertEqual(module.__name__, modname) + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_from_modexport_null(self): + modname = '_test_from_modexport_null' + filename = _testmultiphase.__file__ + with self.assertRaises(SystemError): + import_extension_from_file(modname, filename, + put_in_sys_modules=False) + + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_from_modexport_exception(self): + modname = '_test_from_modexport_exception' + filename = _testmultiphase.__file__ + with self.assertRaises(ValueError): + import_extension_from_file(modname, filename, + put_in_sys_modules=False) + + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_from_modexport_create_nonmodule(self): + modname = '_test_from_modexport_create_nonmodule' + filename = _testmultiphase.__file__ + module = import_extension_from_file(modname, filename, + put_in_sys_modules=False) + self.assertIsInstance(module, str) + + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_from_modexport_smoke(self): + # General positive test for sundry features + # (PyModule_FromSlotsAndSpec tests exercise test these more carefully) + modname = '_test_from_modexport_smoke' + filename = _testmultiphase.__file__ + module = import_extension_from_file(modname, filename, + put_in_sys_modules=False) + self.assertEqual(module.__doc__, "the expected docstring") + self.assertEqual(module.number, 147) + self.assertEqual(module.get_state_int(), 258) + @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") def test_python_compat(self): module = 'threading' diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index 607b805ffc24d7..dc845ecabbcca9 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -1015,12 +1015,128 @@ PyInit__test_no_multiple_interpreter_slot(void) return PyModuleDef_Init(&no_multiple_interpreter_slot_def); } + +/* PyModExport_* hooks */ + PyMODEXPORT_FUNC PyModExport__test_from_modexport(PyObject *spec) { - static PyModuleDef_Slot from_modexport_slots[] = { + static PyModuleDef_Slot slots[] = { {Py_mod_name, "_test_from_modexport"}, {0}, }; - return from_modexport_slots; + return slots; +} + +PyMODEXPORT_FUNC +PyModExport__test_from_modexport_null(PyObject *spec) +{ + PyObject *exc; + if (PyObject_GetOptionalAttrString(spec, "_test_exception", &exc) < 0) { + return NULL; + } + if (exc) { + PyErr_SetObject((PyObject*)Py_TYPE(exc), exc); + } + Py_XDECREF(exc); + return NULL; +} + +PyMODINIT_FUNC +PyModInit__test_from_modexport_null(PyObject *spec) +{ + // This is not called as fallback for failed PyModExport_* + assert(0); + PyErr_SetString(PyExc_AssertionError, "PyInit_ fallback called"); + return NULL; +} + +PyMODEXPORT_FUNC +PyModExport__test_from_modexport_exception(PyObject *spec) +{ + PyErr_SetString(PyExc_ValueError, "failed as requested"); + return NULL; +} + +PyMODINIT_FUNC +PyModInit__test_from_modexport_exception(PyObject *spec) +{ + // This is not called as fallback for failed PyModExport_* + assert(0); + PyErr_SetString(PyExc_AssertionError, "PyInit_ fallback called"); + return NULL; +} + +static PyObject * +modexport_create_string(PyObject *spec, PyObject *def) +{ + assert(def == NULL); + return PyUnicode_FromString("is this \xf0\x9f\xa6\x8b... a module?"); +} + +PyMODEXPORT_FUNC +PyModExport__test_from_modexport_create_nonmodule(PyObject *spec) +{ + static PyModuleDef_Slot slots[] = { + {Py_mod_name, "_test_from_modexport_create_nonmodule"}, + {Py_mod_create, modexport_create_string}, + {0}, + }; + return slots; +} + +static int +modexport_smoke_exec(PyObject *mod) +{ + // "magic" values 147 & 258 are expected in the test + if (PyModule_AddIntConstant(mod, "number", 147) < 0) { + return 0; + } + int *state = PyModule_GetState(mod); + if (!state) { + return -1; + } + *state = 258; + Py_INCREF(mod); // do be cleared in modexport_smoke_free + return 0; +} + +static PyObject * +modexport_smoke_get_state_int(PyObject *mod, PyObject *arg) +{ + int *state = PyModule_GetState(mod); + if (!state) { + return NULL; + } + return PyLong_FromLong(*state); +} + +static void +modexport_smoke_free(PyObject *mod) +{ + int *state = PyModule_GetState(mod); + if (!state) { + PyErr_WriteUnraisable(mod); + } + assert(*state == 258); + Py_DECREF(mod); +} + +PyMODEXPORT_FUNC +PyModExport__test_from_modexport_smoke(PyObject *spec) +{ + static PyMethodDef methods[] = { + {"get_state_int", modexport_smoke_get_state_int, METH_NOARGS}, + {0}, + }; + static PyModuleDef_Slot slots[] = { + {Py_mod_name, "_test_from_modexport_smoke"}, + {Py_mod_doc, "the expected docstring"}, + {Py_mod_exec, modexport_smoke_exec}, + {Py_mod_size, (void*)sizeof(int)}, + {Py_mod_methods, methods}, + {Py_mod_free, modexport_smoke_free}, + {0}, + }; + return slots; } From bd1c352d3189449fb167930a8c0bddab5843a33b Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 23 May 2025 16:18:25 +0200 Subject: [PATCH 38/81] `state_` in slot names --- Include/moduleobject.h | 8 ++++---- Lib/test/test_capi/test_module.py | 5 +++-- Modules/_testcapi/module.c | 16 ++++++++-------- Modules/_testmultiphase.c | 4 ++-- Objects/moduleobject.c | 19 +++++++++++-------- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/Include/moduleobject.h b/Include/moduleobject.h index 53f1726536f574..89b2ba45c09d19 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -85,11 +85,11 @@ struct PyModuleDef_Slot { # define Py_mod_abi 5 # define Py_mod_name 6 # define Py_mod_doc 7 -# define Py_mod_size 8 +# define Py_mod_state_size 8 # define Py_mod_methods 9 -# define Py_mod_traverse 10 -# define Py_mod_clear 11 -# define Py_mod_free 12 +# define Py_mod_state_traverse 10 +# define Py_mod_state_clear 11 +# define Py_mod_state_free 12 # define Py_mod_token 13 #endif diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 51bdff9491d190..311eeafca2a568 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -10,8 +10,9 @@ class FakeSpec: name = 'testmod' DEF_SLOTS = ( - 'Py_mod_name', 'Py_mod_doc', 'Py_mod_size', 'Py_mod_methods', - 'Py_mod_traverse', 'Py_mod_clear', 'Py_mod_free', 'Py_mod_token', + 'Py_mod_name', 'Py_mod_doc', 'Py_mod_state_size', 'Py_mod_methods', + 'Py_mod_state_traverse', 'Py_mod_state_clear', 'Py_mod_state_free', + 'Py_mod_token', ) # The C functions used by this module are in: diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 443bb78df478bb..6a15732e12afcc 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -46,7 +46,7 @@ static PyObject * module_from_slots_size(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { - {Py_mod_size, (void*)123}, + {Py_mod_state_size, (void*)123}, {0}, }; PyObject *mod = PyModule_FromSlotsAndSpec(slots, spec); @@ -96,9 +96,9 @@ static PyObject * module_from_slots_gc(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { - {Py_mod_traverse, trivial_traverse}, - {Py_mod_clear, trivial_clear}, - {Py_mod_free, trivial_free}, + {Py_mod_state_traverse, trivial_traverse}, + {Py_mod_state_clear, trivial_clear}, + {Py_mod_state_free, trivial_free}, {0}, }; PyObject *mod = PyModule_FromSlotsAndSpec(slots, spec); @@ -277,11 +277,11 @@ _PyTestCapi_Init_Module(PyObject *m) ADD_INT_MACRO(Py_mod_gil); ADD_INT_MACRO(Py_mod_name); ADD_INT_MACRO(Py_mod_doc); - ADD_INT_MACRO(Py_mod_size); + ADD_INT_MACRO(Py_mod_state_size); ADD_INT_MACRO(Py_mod_methods); - ADD_INT_MACRO(Py_mod_traverse); - ADD_INT_MACRO(Py_mod_clear); - ADD_INT_MACRO(Py_mod_free); + ADD_INT_MACRO(Py_mod_state_traverse); + ADD_INT_MACRO(Py_mod_state_clear); + ADD_INT_MACRO(Py_mod_state_free); ADD_INT_MACRO(Py_mod_token); #undef ADD_INT_MACRO return PyModule_AddFunctions(m, test_methods); diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index dc845ecabbcca9..2bdde4f988cd76 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -1133,9 +1133,9 @@ PyModExport__test_from_modexport_smoke(PyObject *spec) {Py_mod_name, "_test_from_modexport_smoke"}, {Py_mod_doc, "the expected docstring"}, {Py_mod_exec, modexport_smoke_exec}, - {Py_mod_size, (void*)sizeof(int)}, + {Py_mod_state_size, (void*)sizeof(int)}, {Py_mod_methods, methods}, - {Py_mod_free, modexport_smoke_free}, + {Py_mod_state_free, modexport_smoke_free}, {0}, }; return slots; diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index d11ed7c0ab44e0..e98879ed618024 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -471,22 +471,25 @@ module_from_def_and_spec( case Py_mod_doc: COPY_COMMON_SLOT(Py_mod_doc, char*, def_like->m_doc); break; - case Py_mod_size: - COPY_COMMON_SLOT(Py_mod_size, Py_ssize_t, def_like->m_size); + case Py_mod_state_size: + COPY_COMMON_SLOT(Py_mod_state_size, Py_ssize_t, + def_like->m_size); break; case Py_mod_methods: COPY_COMMON_SLOT(Py_mod_methods, PyMethodDef*, def_like->m_methods); break; - case Py_mod_traverse: - COPY_COMMON_SLOT(Py_mod_traverse, traverseproc, + case Py_mod_state_traverse: + COPY_COMMON_SLOT(Py_mod_state_traverse, traverseproc, def_like->m_traverse); break; - case Py_mod_clear: - COPY_COMMON_SLOT(Py_mod_clear, inquiry, def_like->m_clear); + case Py_mod_state_clear: + COPY_COMMON_SLOT(Py_mod_state_clear, inquiry, + def_like->m_clear); break; - case Py_mod_free: - COPY_COMMON_SLOT(Py_mod_free, freefunc, def_like->m_free); + case Py_mod_state_free: + COPY_COMMON_SLOT(Py_mod_state_free, freefunc, + def_like->m_free); break; case Py_mod_token: COPY_COMMON_SLOT(Py_mod_token, void*, token); From 56525faf4ccc69ec1bca5d45c295254801859c70 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sat, 7 Jun 2025 18:04:45 +0200 Subject: [PATCH 39/81] Test PyModule_Exec with PyModuleDef-defined module --- Lib/test/test_capi/test_module.py | 11 ++++++ Modules/_testcapi/module.c | 58 +++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 311eeafca2a568..427a01e41897f7 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -123,3 +123,14 @@ def test_null_def_slot(self): _testcapi.module_from_slots_null_slot(spec) self.assertIn(name, str(cm.exception)) self.assertIn("NULL", str(cm.exception)) + + def test_def_multiple_exec(self): + """PyModule_Exec runs all exec slots of PyModuleDef-defined module""" + mod = _testcapi.module_from_def_multiple_exec(FakeSpec()) + self.assertFalse(hasattr(mod, 'a_number')) + _testcapi.pymodule_exec(mod) + self.assertEqual(mod.a_number, 456) + self.assertEqual(mod.another_number, 789) + _testcapi.pymodule_exec(mod) + self.assertEqual(mod.a_number, 456) + self.assertEqual(mod.another_number, -789) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 6a15732e12afcc..61739571fc7d1e 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -247,9 +247,65 @@ module_from_def_slot(PyObject *self, PyObject *spec) .m_name = "currently ignored", .m_slots = slots, }; + // PyModuleDef is normally static; the real requirement is that it + // must outlive its module. + // Here, module creation fails, so it's fine on the stack. + PyObject *result = PyModule_FromDefAndSpec(&def, spec); + assert(result == NULL); + return result; +} + +static int +another_exec(PyObject *module) +{ + /* Make sure simple_exec was called */ + assert(PyObject_HasAttrString(module, "a_number")); + + /* Add or negate a global called 'another_number' */ + PyObject *another_number; + if (PyObject_GetOptionalAttrString(module, "another_number", + &another_number) < 0) { + return -1; + } + if (!another_number) { + return PyModule_AddIntConstant(module, "another_number", 789); + } + PyObject *neg_number = PyNumber_Negative(another_number); + Py_DECREF(another_number); + if (!neg_number) { + return -1; + } + int result = PyObject_SetAttrString(module, "another_number", + neg_number); + Py_DECREF(neg_number); + return result; +} + +static PyObject * +module_from_def_multiple_exec(PyObject *self, PyObject *spec) +{ + static PyModuleDef_Slot slots[] = { + {Py_mod_exec, simple_exec}, + {Py_mod_exec, another_exec}, + {0}, + }; + static PyModuleDef def = { + PyModuleDef_HEAD_INIT, + .m_name = "currently ignored", + .m_slots = slots, + }; return PyModule_FromDefAndSpec(&def, spec); } +static PyObject * +pymodule_exec(PyObject *self, PyObject *module) +{ + if (PyModule_Exec(module) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + static PyMethodDef test_methods[] = { {"module_from_slots_empty", module_from_slots_empty, METH_O}, {"module_from_slots_null", module_from_slots_null, METH_O}, @@ -263,7 +319,9 @@ static PyMethodDef test_methods[] = { {"module_from_slots_create", module_from_slots_create, METH_O}, {"module_from_slots_repeat_slot", module_from_slots_repeat_slot, METH_O}, {"module_from_slots_null_slot", module_from_slots_null_slot, METH_O}, + {"module_from_def_multiple_exec", module_from_def_multiple_exec, METH_O}, {"module_from_def_slot", module_from_def_slot, METH_O}, + {"pymodule_exec", pymodule_exec, METH_O}, {NULL}, }; From 4b93132af7a3d76f9cfc24c4a60faa43fad46992 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 17 Jun 2025 15:40:05 +0200 Subject: [PATCH 40/81] Test PyModule_GetToken, token setting, and default tokens --- Lib/test/test_capi/test_module.py | 27 +++++++++++++++++++++++++ Lib/test/test_import/__init__.py | 33 ++++++++++++++++++++++++++++++- Modules/_testcapi/module.c | 27 +++++++++++++++++++++++++ Modules/_testmultiphase.c | 31 +++++++++++++++++++++++++++++ Objects/moduleobject.c | 4 ++-- 5 files changed, 119 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 427a01e41897f7..09350c7614ed82 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -18,10 +18,17 @@ class FakeSpec: # The C functions used by this module are in: # Modules/_testcapi/module.c +def def_and_token(mod): + return ( + _testcapi.pymodule_get_def(mod), + _testcapi.pymodule_get_token(mod), + ) + class TestModFromSlotsAndSpec(unittest.TestCase): def test_empty(self): mod = _testcapi.module_from_slots_empty(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(def_and_token(mod), (0, 0)) self.assertEqual(mod.__name__, 'testmod') def test_null_slots(self): @@ -41,18 +48,21 @@ def test_name(self): # We still test that it's accepted. mod = _testcapi.module_from_slots_name(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(def_and_token(mod), (0, 0)) self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, None) def test_doc(self): mod = _testcapi.module_from_slots_doc(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(def_and_token(mod), (0, 0)) self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, 'the docstring') def test_size(self): mod = _testcapi.module_from_slots_size(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(def_and_token(mod), (0, 0)) self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, None) self.assertEqual(mod.size, 123) @@ -60,6 +70,7 @@ def test_size(self): def test_methods(self): mod = _testcapi.module_from_slots_methods(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(def_and_token(mod), (0, 0)) self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, None) self.assertEqual(mod.a_method(456), (mod, 456)) @@ -67,18 +78,21 @@ def test_methods(self): def test_gc(self): mod = _testcapi.module_from_slots_gc(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(def_and_token(mod), (0, 0)) self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, None) def test_token(self): mod = _testcapi.module_from_slots_token(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(def_and_token(mod), (0, _testcapi.module_test_token)) self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, None) def test_exec(self): mod = _testcapi.module_from_slots_exec(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) + self.assertEqual(def_and_token(mod), (0, 0)) self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, None) self.assertEqual(mod.a_number, 456) @@ -89,6 +103,10 @@ def test_create(self): mod = _testcapi.module_from_slots_create(spec) self.assertIsInstance(mod, str) self.assertEqual(mod, "not a module object") + with self.assertRaises(TypeError): + _testcapi.pymodule_get_def(mod), + with self.assertRaises(TypeError): + _testcapi.pymodule_get_token(mod) def test_def_slot(self): """Slots that replace PyModuleDef fields can't be used with PyModuleDef @@ -134,3 +152,12 @@ def test_def_multiple_exec(self): _testcapi.pymodule_exec(mod) self.assertEqual(mod.a_number, 456) self.assertEqual(mod.another_number, -789) + def_ptr, token = def_and_token(mod) + self.assertEqual(def_ptr, token) + + def test_def_token(self): + """In PyModuleDef-defined modules, the def is the token""" + mod = _testcapi.module_from_def_multiple_exec(FakeSpec()) + def_ptr, token = def_and_token(mod) + self.assertEqual(def_ptr, token) + self.assertGreater(def_ptr, 0) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index c2064e979c3988..03dfe5f6b8ca3b 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -2518,7 +2518,7 @@ def test_from_modexport_create_nonmodule(self): @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") def test_from_modexport_smoke(self): # General positive test for sundry features - # (PyModule_FromSlotsAndSpec tests exercise test these more carefully) + # (PyModule_FromSlotsAndSpec tests exercise these more carefully) modname = '_test_from_modexport_smoke' filename = _testmultiphase.__file__ module = import_extension_from_file(modname, filename, @@ -2526,6 +2526,37 @@ def test_from_modexport_smoke(self): self.assertEqual(module.__doc__, "the expected docstring") self.assertEqual(module.number, 147) self.assertEqual(module.get_state_int(), 258) + self.assertGreater(module.get_test_token(), 0) + + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_from_modexport_smoke_token(self): + _testcapi = import_module("_testcapi") + + modname = '_test_from_modexport_smoke' + filename = _testmultiphase.__file__ + module = import_extension_from_file(modname, filename, + put_in_sys_modules=False) + self.assertEqual(_testcapi.pymodule_get_token(module), + module.get_test_token()) + + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_from_modexport_empty_slots(self): + # Module to test that: + # - no slots are mandatory for PyModExport + # - the slots array is used as the default token + modname = '_test_from_modexport_empty_slots' + filename = _testmultiphase.__file__ + module = import_extension_from_file( + modname, filename, put_in_sys_modules=False) + + self.assertEqual(module.__name__, modname) + self.assertEqual(module.__doc__, None) + + _testcapi = import_module("_testcapi") + 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()) @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") def test_python_compat(self): diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 61739571fc7d1e..2f60652e14a123 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -306,6 +306,26 @@ pymodule_exec(PyObject *self, PyObject *module) Py_RETURN_NONE; } +static PyObject * +pymodule_get_token(PyObject *self, PyObject *module) +{ + void *token; + if (PyModule_GetToken(module, &token) < 0) { + return NULL; + } + return PyLong_FromVoidPtr(token); +} + +static PyObject * +pymodule_get_def(PyObject *self, PyObject *module) +{ + PyModuleDef *def = PyModule_GetDef(module); + if (!def && PyErr_Occurred()) { + return NULL; + } + return PyLong_FromVoidPtr(def); +} + static PyMethodDef test_methods[] = { {"module_from_slots_empty", module_from_slots_empty, METH_O}, {"module_from_slots_null", module_from_slots_null, METH_O}, @@ -321,6 +341,8 @@ static PyMethodDef test_methods[] = { {"module_from_slots_null_slot", module_from_slots_null_slot, METH_O}, {"module_from_def_multiple_exec", module_from_def_multiple_exec, METH_O}, {"module_from_def_slot", module_from_def_slot, METH_O}, + {"pymodule_get_token", pymodule_get_token, METH_O}, + {"pymodule_get_def", pymodule_get_def, METH_O}, {"pymodule_exec", pymodule_exec, METH_O}, {NULL}, }; @@ -342,5 +364,10 @@ _PyTestCapi_Init_Module(PyObject *m) ADD_INT_MACRO(Py_mod_state_free); ADD_INT_MACRO(Py_mod_token); #undef ADD_INT_MACRO + if (PyModule_Add(m, "module_test_token", + PyLong_FromVoidPtr(&test_token)) < 0) + { + return -1; + } return PyModule_AddFunctions(m, test_methods); } diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index 2bdde4f988cd76..2634a272d1e99f 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -1085,6 +1085,16 @@ PyModExport__test_from_modexport_create_nonmodule(PyObject *spec) return slots; } +static PyModuleDef_Slot modexport_empty_slots[] = { + {0}, +}; + +PyMODEXPORT_FUNC +PyModExport__test_from_modexport_empty_slots(PyObject *spec) +{ + return modexport_empty_slots; +} + static int modexport_smoke_exec(PyObject *mod) { @@ -1111,6 +1121,24 @@ modexport_smoke_get_state_int(PyObject *mod, PyObject *arg) return PyLong_FromLong(*state); } +static char modexport_smoke_test_token; + +static PyObject * +modexport_smoke_get_test_token(PyObject *mod, PyObject *arg) +{ + return PyLong_FromVoidPtr(&modexport_smoke_test_token); +} + +static PyObject * +modexport_get_empty_slots(PyObject *mod, PyObject *arg) +{ + /* Get the address of modexport_empty_slots. + * This method would be in the `_test_from_modexport_empty_slots` module, + * if it had a methods slot. + */ + return PyLong_FromVoidPtr(&modexport_empty_slots); +} + static void modexport_smoke_free(PyObject *mod) { @@ -1127,6 +1155,8 @@ PyModExport__test_from_modexport_smoke(PyObject *spec) { 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}, {0}, }; static PyModuleDef_Slot slots[] = { @@ -1136,6 +1166,7 @@ PyModExport__test_from_modexport_smoke(PyObject *spec) {Py_mod_state_size, (void*)sizeof(int)}, {Py_mod_methods, methods}, {Py_mod_state_free, modexport_smoke_free}, + {Py_mod_token, (void*)&modexport_smoke_test_token}, {0}, }; return slots; diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index e98879ed618024..d669b0d7b83abb 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -793,7 +793,7 @@ PyModule_GetSize(PyObject *m, Py_ssize_t *size_p) { *size_p = -1; if (!PyModule_Check(m)) { - PyErr_BadInternalCall(); + PyErr_Format(PyExc_TypeError, "expected module, got %T", m); return -1; } PyModuleObject *mod = (PyModuleObject *)m; @@ -806,7 +806,7 @@ PyModule_GetToken(PyObject *m, void **token_p) { *token_p = NULL; if (!PyModule_Check(m)) { - PyErr_BadInternalCall(); + PyErr_Format(PyExc_TypeError, "expected module, got %T", m); return -1; } PyModuleObject *mod = (PyModuleObject *)m; From bb2dd7df3b98f4c18d4d44153a4f21037ca3dd3e Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 17 Jun 2025 16:32:56 +0200 Subject: [PATCH 41/81] Add PyType_GetModuleByToken --- Include/internal/pycore_moduleobject.h | 9 +++++++++ Include/object.h | 4 ++++ Lib/test/test_capi/test_type.py | 18 ++++++++++++++++++ Lib/test/test_import/__init__.py | 10 ++++++++-- Modules/_testcapi/heaptype.c | 16 ++++++++++++++++ Modules/_testmultiphase.c | 5 +++++ Objects/moduleobject.c | 4 ++-- Objects/typeobject.c | 22 +++++++++++++++++----- 8 files changed, 79 insertions(+), 9 deletions(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index 7500ef6600b570..2567429ac1b6dc 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -44,6 +44,15 @@ static inline PyModuleDef* _PyModule_GetDefOrNull(PyObject *mod) { return ((PyModuleObject *)mod)->md_def_or_null; } +static inline PyModuleDef* _PyModule_GetToken(PyObject *arg) { + assert(PyModule_Check(arg)); + PyModuleObject *mod = (PyModuleObject *)arg; + if (mod->md_def_or_null) { + assert(mod->md_def_or_null == mod->md_token); + } + return mod->md_token; +} + static inline void* _PyModule_GetState(PyObject* mod) { assert(PyModule_Check(mod)); return ((PyModuleObject *)mod)->md_state; diff --git a/Include/object.h b/Include/object.h index 9585f4a1d67a52..12a978229a2846 100644 --- a/Include/object.h +++ b/Include/object.h @@ -834,6 +834,10 @@ PyAPI_FUNC(PyObject *) PyType_GetModuleByDef(PyTypeObject *, PyModuleDef *); PyAPI_FUNC(int) PyType_Freeze(PyTypeObject *type); #endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) +PyAPI_FUNC(PyObject *) PyType_GetModuleByToken(PyTypeObject *, void *); +#endif + #ifdef __cplusplus } #endif diff --git a/Lib/test/test_capi/test_type.py b/Lib/test/test_capi/test_type.py index 15fb4a93e2ad74..450f57046f7cbc 100644 --- a/Lib/test/test_capi/test_type.py +++ b/Lib/test/test_capi/test_type.py @@ -195,6 +195,24 @@ class H2(int): pass with self.assertRaises(TypeError): _testcapi.pytype_getmodulebydef(H2) + def test_get_module_by_token(self): + token = _testcapi.pymodule_get_token(_testcapi) + + heaptype = _testcapi.create_type_with_token('_testcapi.H', 0) + mod = _testcapi.pytype_getmodulebytoken(heaptype, token) + self.assertIs(mod, _testcapi) + + class H1(heaptype): pass + mod = _testcapi.pytype_getmodulebytoken(H1, token) + self.assertIs(mod, _testcapi) + + with self.assertRaises(TypeError): + _testcapi.pytype_getmodulebytoken(int, token) + + class H2(int): pass + with self.assertRaises(TypeError): + _testcapi.pytype_getmodulebytoken(H2, token) + def test_freeze(self): # test PyType_Freeze() type_freeze = _testcapi.type_freeze diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 03dfe5f6b8ca3b..288b45ae0c3e47 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -2536,8 +2536,14 @@ def test_from_modexport_smoke_token(self): filename = _testmultiphase.__file__ module = import_extension_from_file(modname, filename, put_in_sys_modules=False) - self.assertEqual(_testcapi.pymodule_get_token(module), - module.get_test_token()) + token = module.get_test_token() + self.assertEqual(_testcapi.pymodule_get_token(module), token) + + tp = module.Example + self.assertEqual(_testcapi.pytype_getmodulebytoken(tp, token), module) + class Sub(tp): + pass + self.assertEqual(_testcapi.pytype_getmodulebytoken(Sub, token), module) @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") def test_from_modexport_empty_slots(self): diff --git a/Modules/_testcapi/heaptype.c b/Modules/_testcapi/heaptype.c index 69dcf072da1815..4fdcc850a339b4 100644 --- a/Modules/_testcapi/heaptype.c +++ b/Modules/_testcapi/heaptype.c @@ -528,6 +528,21 @@ pytype_getmodulebydef(PyObject *self, PyObject *type) return Py_XNewRef(mod); } +static PyObject * +pytype_getmodulebytoken(PyObject *self, PyObject *args) +{ + PyObject *type; + PyObject *py_token; + if (!PyArg_ParseTuple(args, "OO", &type, &py_token)) { + return NULL; + } + void *token = PyLong_AsVoidPtr(py_token); + if ((!token) && PyErr_Occurred()) { + return NULL; + } + return PyType_GetModuleByToken((PyTypeObject *)type, token); +} + static PyMethodDef TestMethods[] = { {"pytype_fromspec_meta", pytype_fromspec_meta, METH_O}, @@ -546,6 +561,7 @@ static PyMethodDef TestMethods[] = { {"get_tp_token", get_tp_token, METH_O}, {"pytype_getbasebytoken", pytype_getbasebytoken, METH_VARARGS}, {"pytype_getmodulebydef", pytype_getmodulebydef, METH_O}, + {"pytype_getmodulebytoken", pytype_getmodulebytoken, METH_VARARGS}, {NULL}, }; diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index 2634a272d1e99f..b2957d1413d647 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -1108,6 +1108,11 @@ modexport_smoke_exec(PyObject *mod) } *state = 258; Py_INCREF(mod); // do be cleared in modexport_smoke_free + + PyModule_AddObjectRef( + mod, "Example", + PyType_FromModuleAndSpec(mod, &StateAccessType_spec, NULL)); + return 0; } diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index d669b0d7b83abb..ea5b5426f3fab8 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -344,6 +344,7 @@ _PyModule_CreateInitialized(PyModuleDef* module, int module_api_version) } } m->md_def_or_null = module; + m->md_token = module; module_copy_members_from_deflike(m, module); #ifdef Py_GIL_DISABLED m->md_gil = Py_MOD_GIL_USED; @@ -809,8 +810,7 @@ PyModule_GetToken(PyObject *m, void **token_p) PyErr_Format(PyExc_TypeError, "expected module, got %T", m); return -1; } - PyModuleObject *mod = (PyModuleObject *)m; - *token_p = mod->md_token; + *token_p = _PyModule_GetToken(m); return 0; } diff --git a/Objects/typeobject.c b/Objects/typeobject.c index dd3f4f3d76d3f7..346d6f6f83f8bd 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -5764,11 +5764,11 @@ PyType_GetModuleState(PyTypeObject *type) } -/* Get the module of the first superclass where the module has the - * given PyModuleDef. +/* Return borrowed ref to the module of the first superclass where the module + * has the given token. */ PyObject * -PyType_GetModuleByDef(PyTypeObject *type, PyModuleDef *def) +borrow_module_by_token(PyTypeObject *type, void *token) { assert(PyType_Check(type)); @@ -5780,7 +5780,7 @@ PyType_GetModuleByDef(PyTypeObject *type, PyModuleDef *def) else { PyHeapTypeObject *ht = (PyHeapTypeObject*)type; PyObject *module = ht->ht_module; - if (module && _PyModule_GetDefOrNull(module) == def) { + if (module && _PyModule_GetToken(module) == token) { return module; } } @@ -5808,7 +5808,7 @@ PyType_GetModuleByDef(PyTypeObject *type, PyModuleDef *def) PyHeapTypeObject *ht = (PyHeapTypeObject*)super; PyObject *module = ht->ht_module; - if (module && _PyModule_GetDefOrNull(module) == def) { + if (module && _PyModule_GetToken(module) == token) { res = module; break; } @@ -5826,6 +5826,18 @@ PyType_GetModuleByDef(PyTypeObject *type, PyModuleDef *def) return NULL; } +PyObject * +PyType_GetModuleByDef(PyTypeObject *type, PyModuleDef *def) +{ + return borrow_module_by_token(type, def); +} + +PyObject * +PyType_GetModuleByToken(PyTypeObject *type, void *token) +{ + return Py_XNewRef(borrow_module_by_token(type, token)); +} + static PyTypeObject * get_base_by_token_recursive(PyObject *bases, void *token) From 36cc311bfef2295906252617b29d0b7b14faedd6 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 17 Jun 2025 16:48:05 +0200 Subject: [PATCH 42/81] Fix refcounting --- Modules/_testmultiphase.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index b2957d1413d647..add00dc385dff4 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -1107,11 +1107,11 @@ modexport_smoke_exec(PyObject *mod) return -1; } *state = 258; - Py_INCREF(mod); // do be cleared in modexport_smoke_free - PyModule_AddObjectRef( - mod, "Example", - PyType_FromModuleAndSpec(mod, &StateAccessType_spec, NULL)); + PyObject *tp = PyType_FromModuleAndSpec(mod, &StateAccessType_spec, NULL); + if (PyModule_Add(mod, "Example", tp) < 0) { + return -1; + } return 0; } @@ -1152,7 +1152,6 @@ modexport_smoke_free(PyObject *mod) PyErr_WriteUnraisable(mod); } assert(*state == 258); - Py_DECREF(mod); } PyMODEXPORT_FUNC From 37905ae2321c1483376b2deb83ef7463ab1faffa Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 19 Jun 2025 13:41:01 +0200 Subject: [PATCH 43/81] Rename and test PyModule_GetStateSize --- Include/moduleobject.h | 2 +- Lib/test/test_capi/test_module.py | 17 +++++++++++++++-- Modules/_testcapi/module.c | 20 +++++++++++--------- Objects/moduleobject.c | 2 +- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Include/moduleobject.h b/Include/moduleobject.h index 89b2ba45c09d19..bc45f0bb194a11 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -123,7 +123,7 @@ PyAPI_FUNC(int) PyUnstable_Module_SetGIL(PyObject *module, void *gil); PyAPI_FUNC(PyObject *) PyModule_FromSlotsAndSpec(PyModuleDef_Slot *, PyObject *spec); PyAPI_FUNC(int) PyModule_Exec(PyObject *mod); -PyAPI_FUNC(int) PyModule_GetSize(PyObject *mod, Py_ssize_t *size_p); +PyAPI_FUNC(int) PyModule_GetStateSize(PyObject *mod, Py_ssize_t *size_p); PyAPI_FUNC(int) PyModule_GetToken(PyObject *, void **token_p); #endif diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 09350c7614ed82..bc19ef76cd2174 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -1,6 +1,6 @@ import unittest import types -from test.support import import_helper +from test.support import import_helper, subTests # Skip this test if the _testcapi module isn't available. _testcapi = import_helper.import_module('_testcapi') @@ -30,6 +30,8 @@ def test_empty(self): self.assertIsInstance(mod, types.ModuleType) self.assertEqual(def_and_token(mod), (0, 0)) self.assertEqual(mod.__name__, 'testmod') + size = _testcapi.pymodule_get_state_size(mod) + self.assertEqual(size, 0) def test_null_slots(self): with self.assertRaises(SystemError): @@ -65,7 +67,8 @@ def test_size(self): self.assertEqual(def_and_token(mod), (0, 0)) self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, None) - self.assertEqual(mod.size, 123) + size = _testcapi.pymodule_get_state_size(mod) + self.assertEqual(size, 123) def test_methods(self): mod = _testcapi.module_from_slots_methods(FakeSpec()) @@ -161,3 +164,13 @@ def test_def_token(self): def_ptr, token = def_and_token(mod) self.assertEqual(def_ptr, token) self.assertGreater(def_ptr, 0) + + @subTests('name, expected_size', [ + (__name__, 0), # Python module + ('_testsinglephase', -1), # single-phase init + ('sys', -1), + ]) + def test_get_state_size(self, name, expected_size): + mod = import_helper.import_module(name) + size = _testcapi.pymodule_get_state_size(mod) + self.assertEqual(size, expected_size) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 2f60652e14a123..28ba603889c1e6 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -53,15 +53,6 @@ module_from_slots_size(PyObject *self, PyObject *spec) if (!mod) { return NULL; } - Py_ssize_t size; - if (PyModule_GetSize(mod, &size) < 0) { - Py_DECREF(mod); - return NULL; - } - if (PyModule_AddIntConstant(mod, "size", size) < 0) { - Py_DECREF(mod); - return NULL; - } return mod; } @@ -326,6 +317,16 @@ pymodule_get_def(PyObject *self, PyObject *module) return PyLong_FromVoidPtr(def); } +static PyObject * +pymodule_get_state_size(PyObject *self, PyObject *module) +{ + Py_ssize_t size; + if (PyModule_GetStateSize(module, &size) < 0) { + return NULL; + } + return PyLong_FromSsize_t(size); +} + static PyMethodDef test_methods[] = { {"module_from_slots_empty", module_from_slots_empty, METH_O}, {"module_from_slots_null", module_from_slots_null, METH_O}, @@ -343,6 +344,7 @@ static PyMethodDef test_methods[] = { {"module_from_def_slot", module_from_def_slot, 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}, {"pymodule_exec", pymodule_exec, METH_O}, {NULL}, }; diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index ea5b5426f3fab8..6dc67c89a5e4b8 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -790,7 +790,7 @@ PyModule_GetDict(PyObject *m) } int -PyModule_GetSize(PyObject *m, Py_ssize_t *size_p) +PyModule_GetStateSize(PyObject *m, Py_ssize_t *size_p) { *size_p = -1; if (!PyModule_Check(m)) { From 1df228a81d18afaad7b1a0a51ec865661168b768 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 19 Jun 2025 14:06:51 +0200 Subject: [PATCH 44/81] Rename new internal module members --- Include/internal/pycore_moduleobject.h | 8 ++--- Objects/moduleobject.c | 44 +++++++++++++------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index 2567429ac1b6dc..bab4844a79e058 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -31,10 +31,10 @@ typedef struct { #ifdef Py_GIL_DISABLED void *md_gil; #endif - Py_ssize_t md_size; - traverseproc md_traverse; - inquiry md_clear; - freefunc md_free; + Py_ssize_t md_state_size; + traverseproc md_state_traverse; + inquiry md_state_clear; + freefunc md_state_free; void *md_token; _Py_modexecfunc md_exec; /* only set if md_def_or_null is NULL */ } PyModuleObject; diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 6dc67c89a5e4b8..70adb03dcbc09d 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -30,7 +30,7 @@ static PyMemberDef module_members[] = { static void assert_def_missing_or_redundant(PyModuleObject *m) { if (m->md_def_or_null) { -#define DO_ASSERT(F) assert (m->md_def_or_null->m_ ## F == m->md_ ## F); +#define DO_ASSERT(F) assert (m->md_def_or_null->m_ ## F == m->md_state_ ## F); DO_ASSERT(size); DO_ASSERT(traverse); DO_ASSERT(clear); @@ -165,10 +165,10 @@ new_module_notrack(PyTypeObject *mt) m->md_state = NULL; m->md_weaklist = NULL; m->md_name = NULL; - m->md_size = 0; - m->md_traverse = NULL; - m->md_clear = NULL; - m->md_free = NULL; + m->md_state_size = 0; + m->md_state_traverse = NULL; + m->md_state_clear = NULL; + m->md_state_free = NULL; m->md_exec = NULL; m->md_token = NULL; m->md_dict = PyDict_New(); @@ -291,10 +291,10 @@ module_copy_members_from_deflike( PyModuleDef *def_like /* not necessarily a valid Python object */) { /* def may not be a valid PyObject*, see */ - md->md_size = def_like->m_size; - md->md_traverse = def_like->m_traverse; - md->md_clear = def_like->m_clear; - md->md_free = def_like->m_free; + md->md_state_size = def_like->m_size; + md->md_state_traverse = def_like->m_traverse; + md->md_state_clear = def_like->m_clear; + md->md_state_free = def_like->m_free; } PyObject * @@ -691,16 +691,16 @@ alloc_state(PyObject *module) } PyModuleObject *md = (PyModuleObject*)module; - if (md->md_size >= 0) { + if (md->md_state_size >= 0) { if (md->md_state == NULL) { /* Always set a state pointer; this serves as a marker to skip * multiple initialization (importlib.reload() is no-op) */ - md->md_state = PyMem_Malloc(md->md_size); + md->md_state = PyMem_Malloc(md->md_state_size); if (!md->md_state) { PyErr_NoMemory(); return -1; } - memset(md->md_state, 0, md->md_size); + memset(md->md_state, 0, md->md_state_size); } } return 0; @@ -798,7 +798,7 @@ PyModule_GetStateSize(PyObject *m, Py_ssize_t *size_p) return -1; } PyModuleObject *mod = (PyModuleObject *)m; - *size_p = mod->md_size; + *size_p = mod->md_state_size; return 0; } @@ -823,9 +823,9 @@ _PyModule_GetGCHooks(PyObject *m, traverseproc *traverse, return -1; } PyModuleObject *mod = (PyModuleObject *)m; - *traverse = mod->md_traverse; - *clear = mod->md_clear; - *free = mod->md_free; + *traverse = mod->md_state_traverse; + *clear = mod->md_state_clear; + *free = mod->md_state_free; return 0; } @@ -1095,9 +1095,9 @@ module_dealloc(PyObject *self) assert_def_missing_or_redundant(m); /* bpo-39824: Don't call m_free() if m_size > 0 and md_state=NULL */ - if (m->md_free && (m->md_size <= 0 || m->md_state != NULL)) + if (m->md_state_free && (m->md_state_size <= 0 || m->md_state != NULL)) { - m->md_free(m); + m->md_state_free(m); } Py_XDECREF(m->md_dict); @@ -1414,9 +1414,9 @@ module_traverse(PyObject *self, visitproc visit, void *arg) assert_def_missing_or_redundant(m); /* bpo-39824: Don't call m_traverse() if m_size > 0 and md_state=NULL */ - if (m->md_traverse && (m->md_size <= 0 || m->md_state != NULL)) + if (m->md_state_traverse && (m->md_state_size <= 0 || m->md_state != NULL)) { - int res = m->md_traverse((PyObject*)m, visit, arg); + int res = m->md_state_traverse((PyObject*)m, visit, arg); if (res) return res; } @@ -1432,9 +1432,9 @@ module_clear(PyObject *self) assert_def_missing_or_redundant(m); /* bpo-39824: Don't call m_clear() if m_size > 0 and md_state=NULL */ - if (m->md_clear && (m->md_size <= 0 || m->md_state != NULL)) + if (m->md_state_clear && (m->md_state_size <= 0 || m->md_state != NULL)) { - int res = m->md_clear((PyObject*)m); + int res = m->md_state_clear((PyObject*)m); if (PyErr_Occurred()) { PyErr_FormatUnraisable("Exception ignored in m_clear of module%s%V", m->md_name ? " " : "", From 71e043fedd2499f5b3fdbc70f8067246d916185a Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 19 Jun 2025 14:48:44 +0200 Subject: [PATCH 45/81] Share storage between def and token --- Include/internal/pycore_moduleobject.h | 20 ++++++------ Lib/test/test_sys.py | 2 +- Objects/moduleobject.c | 43 ++++++++++++++++---------- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index bab4844a79e058..8930577c23b069 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -1,5 +1,8 @@ #ifndef Py_INTERNAL_MODULEOBJECT_H #define Py_INTERNAL_MODULEOBJECT_H + +#include + #ifdef __cplusplus extern "C" { #endif @@ -21,13 +24,11 @@ typedef int (*_Py_modexecfunc)(PyObject *); typedef struct { PyObject_HEAD PyObject *md_dict; - // The PyModuleDef used to define the module, if any. - // (used to be `md_def`; renamed because all uses need inspection) - PyModuleDef *md_def_or_null; void *md_state; PyObject *md_weaklist; // for logging purposes after md_dict is cleared PyObject *md_name; + bool md_token_is_def; /* if true, `md_token` is the PyModuleDef */ #ifdef Py_GIL_DISABLED void *md_gil; #endif @@ -39,17 +40,18 @@ typedef struct { _Py_modexecfunc md_exec; /* only set if md_def_or_null is NULL */ } PyModuleObject; -static inline PyModuleDef* _PyModule_GetDefOrNull(PyObject *mod) { - assert(PyModule_Check(mod)); - return ((PyModuleObject *)mod)->md_def_or_null; +static inline PyModuleDef* _PyModule_GetDefOrNull(PyObject *arg) { + assert(PyModule_Check(arg)); + PyModuleObject *mod = (PyModuleObject *)arg; + if (mod->md_token_is_def) { + return (PyModuleDef*)((PyModuleObject *)mod)->md_token; + } + return NULL; } static inline PyModuleDef* _PyModule_GetToken(PyObject *arg) { assert(PyModule_Check(arg)); PyModuleObject *mod = (PyModuleObject *)arg; - if (mod->md_def_or_null) { - assert(mod->md_def_or_null == mod->md_token); - } return mod->md_token; } diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index b1454f3aa6b4bb..3ceed019ac43cf 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1728,7 +1728,7 @@ def get_gen(): yield 1 md_gil = 'P' else: md_gil = '' - check(unittest, size('PPPPP' + md_gil + 'NPPPPP')) + check(unittest, size('PPPP?' + md_gil + 'NPPPPP')) # None check(None, size('')) # NotImplementedType diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 70adb03dcbc09d..37e7c96a489775 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -29,14 +29,18 @@ static PyMemberDef module_members[] = { static void assert_def_missing_or_redundant(PyModuleObject *m) { - if (m->md_def_or_null) { -#define DO_ASSERT(F) assert (m->md_def_or_null->m_ ## F == m->md_state_ ## F); +#ifdef Py_DEBUG + if (m->md_token_is_def) { + PyModuleDef *def = (PyModuleDef *)m->md_token; + assert(def); +#define DO_ASSERT(F) assert (def->m_ ## F == m->md_state_ ## F); DO_ASSERT(size); DO_ASSERT(traverse); DO_ASSERT(clear); DO_ASSERT(free); #undef DO_ASSERT } +#endif // Py_DEBUG } @@ -59,8 +63,11 @@ _PyModule_IsExtension(PyObject *obj) if (module->md_exec) { return 1; } - PyModuleDef *def = module->md_def_or_null; - return (def != NULL && def->m_methods != NULL); + if (module->md_token_is_def) { + PyModuleDef *def = (PyModuleDef *)module->md_token; + return (module->md_token_is_def && def->m_methods != NULL); + } + return 0; } @@ -161,10 +168,13 @@ new_module_notrack(PyTypeObject *mt) m = (PyModuleObject *)_PyType_AllocNoTrack(mt, 0); if (m == NULL) return NULL; - m->md_def_or_null = NULL; m->md_state = NULL; m->md_weaklist = NULL; m->md_name = NULL; + m->md_token_is_def = false; +#ifdef Py_GIL_DISABLED + m->md_gil = Py_MOD_GIL_USED; +#endif m->md_state_size = 0; m->md_state_traverse = NULL; m->md_state_clear = NULL; @@ -343,8 +353,8 @@ _PyModule_CreateInitialized(PyModuleDef* module, int module_api_version) return NULL; } } - m->md_def_or_null = module; m->md_token = module; + m->md_token_is_def = true; module_copy_members_from_deflike(m, module); #ifdef Py_GIL_DISABLED m->md_gil = Py_MOD_GIL_USED; @@ -557,20 +567,18 @@ module_from_def_and_spec( mod->md_state = NULL; module_copy_members_from_deflike(mod, def_like); if (original_def) { - mod->md_def_or_null = original_def; + assert (!token); + mod->md_token = original_def; + mod->md_token_is_def = 1; + } + else { + mod->md_token = token; } #ifdef Py_GIL_DISABLED mod->md_gil = gil_slot; #else (void)gil_slot; #endif - if (original_def) { - mod->md_token = original_def; - assert (!token); - } - else { - mod->md_token = token; - } mod->md_exec = m_exec; } else { if (def_like->m_size > 0 || def_like->m_traverse || def_like->m_clear @@ -714,12 +722,13 @@ PyModule_Exec(PyObject *module) } PyModuleObject *md = (PyModuleObject*)module; if (md->md_exec) { - assert(!md->md_def_or_null); + assert(!md->md_token_is_def); return run_exec_func(module, md->md_exec); } - if (md->md_def_or_null) { - return PyModule_ExecDef(module, md->md_def_or_null); + PyModuleDef *def = _PyModule_GetDefOrNull(module); + if(def) { + return PyModule_ExecDef(module, def); } return 0; } From dd3c25f551126ac1e189016b7f8957f43d3b7735 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 9 Oct 2025 15:55:42 +0200 Subject: [PATCH 46/81] Lose the spec argument --- Include/internal/pycore_importdl.h | 2 +- Modules/_testmultiphase.c | 24 ++++++++---------------- Python/import.c | 2 +- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Include/internal/pycore_importdl.h b/Include/internal/pycore_importdl.h index 75a32b607dfbbc..88367740c3f023 100644 --- a/Include/internal/pycore_importdl.h +++ b/Include/internal/pycore_importdl.h @@ -96,7 +96,7 @@ extern void _Py_ext_module_loader_result_apply_error( /* The module init function. */ typedef PyObject *(*PyModInitFunction)(void); -typedef PyModuleDef_Slot *(*PyModExportFunction)(PyObject *spec); +typedef PyModuleDef_Slot *(*PyModExportFunction)(void); #ifdef HAVE_DYNAMIC_LOADING // function changed signature, the "2" suffix helps avoid ABI issues extern int _PyImport_GetModInitFunc2( diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index add00dc385dff4..aa734df9d15eac 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -1019,7 +1019,7 @@ PyInit__test_no_multiple_interpreter_slot(void) /* PyModExport_* hooks */ PyMODEXPORT_FUNC -PyModExport__test_from_modexport(PyObject *spec) +PyModExport__test_from_modexport(void) { static PyModuleDef_Slot slots[] = { {Py_mod_name, "_test_from_modexport"}, @@ -1029,21 +1029,13 @@ PyModExport__test_from_modexport(PyObject *spec) } PyMODEXPORT_FUNC -PyModExport__test_from_modexport_null(PyObject *spec) +PyModExport__test_from_modexport_null(void) { - PyObject *exc; - if (PyObject_GetOptionalAttrString(spec, "_test_exception", &exc) < 0) { - return NULL; - } - if (exc) { - PyErr_SetObject((PyObject*)Py_TYPE(exc), exc); - } - Py_XDECREF(exc); return NULL; } PyMODINIT_FUNC -PyModInit__test_from_modexport_null(PyObject *spec) +PyModInit__test_from_modexport_null(void) { // This is not called as fallback for failed PyModExport_* assert(0); @@ -1052,14 +1044,14 @@ PyModInit__test_from_modexport_null(PyObject *spec) } PyMODEXPORT_FUNC -PyModExport__test_from_modexport_exception(PyObject *spec) +PyModExport__test_from_modexport_exception(void) { PyErr_SetString(PyExc_ValueError, "failed as requested"); return NULL; } PyMODINIT_FUNC -PyModInit__test_from_modexport_exception(PyObject *spec) +PyModInit__test_from_modexport_exception(void) { // This is not called as fallback for failed PyModExport_* assert(0); @@ -1075,7 +1067,7 @@ modexport_create_string(PyObject *spec, PyObject *def) } PyMODEXPORT_FUNC -PyModExport__test_from_modexport_create_nonmodule(PyObject *spec) +PyModExport__test_from_modexport_create_nonmodule(void) { static PyModuleDef_Slot slots[] = { {Py_mod_name, "_test_from_modexport_create_nonmodule"}, @@ -1090,7 +1082,7 @@ static PyModuleDef_Slot modexport_empty_slots[] = { }; PyMODEXPORT_FUNC -PyModExport__test_from_modexport_empty_slots(PyObject *spec) +PyModExport__test_from_modexport_empty_slots(void) { return modexport_empty_slots; } @@ -1155,7 +1147,7 @@ modexport_smoke_free(PyObject *mod) } PyMODEXPORT_FUNC -PyModExport__test_from_modexport_smoke(PyObject *spec) +PyModExport__test_from_modexport_smoke(void) { static PyMethodDef methods[] = { {"get_state_int", modexport_smoke_get_state_int, METH_NOARGS}, diff --git a/Python/import.c b/Python/import.c index 32f7a5206f175e..bda3d8cd591ded 100644 --- a/Python/import.c +++ b/Python/import.c @@ -1991,7 +1991,7 @@ import_run_modexport(PyThreadState *tstate, PyModExportFunction ex0, /* This is like import_run_extension, but avoids interpreter switching * and code for for single-phase modules. */ - PyModuleDef_Slot *slots = ex0(spec); + PyModuleDef_Slot *slots = ex0(); if (!slots) { if (!PyErr_Occurred()) { PyErr_Format( From 391281da07dd8e0e092e89f2f4950dbb3f76d608 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 9 Oct 2025 16:00:38 +0200 Subject: [PATCH 47/81] Put `const` in signatures --- Include/moduleobject.h | 2 +- Include/object.h | 2 +- Objects/moduleobject.c | 4 ++-- Objects/typeobject.c | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Include/moduleobject.h b/Include/moduleobject.h index bc45f0bb194a11..925bbfa7f2a10c 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -120,7 +120,7 @@ PyAPI_FUNC(int) PyUnstable_Module_SetGIL(PyObject *module, void *gil); #endif #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) -PyAPI_FUNC(PyObject *) PyModule_FromSlotsAndSpec(PyModuleDef_Slot *, +PyAPI_FUNC(PyObject *) PyModule_FromSlotsAndSpec(const PyModuleDef_Slot *, PyObject *spec); PyAPI_FUNC(int) PyModule_Exec(PyObject *mod); PyAPI_FUNC(int) PyModule_GetStateSize(PyObject *mod, Py_ssize_t *size_p); diff --git a/Include/object.h b/Include/object.h index 12a978229a2846..1b04ebaac3fc0f 100644 --- a/Include/object.h +++ b/Include/object.h @@ -835,7 +835,7 @@ PyAPI_FUNC(int) PyType_Freeze(PyTypeObject *type); #endif #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) -PyAPI_FUNC(PyObject *) PyType_GetModuleByToken(PyTypeObject *, void *); +PyAPI_FUNC(PyObject *) PyType_GetModuleByToken(PyTypeObject *, const void *); #endif #ifdef __cplusplus diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 37e7c96a489775..d84056d08d8d15 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -639,7 +639,7 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio } PyObject * -PyModule_FromSlotsAndSpec(PyModuleDef_Slot *slots, PyObject *spec) +PyModule_FromSlotsAndSpec(const PyModuleDef_Slot *slots, PyObject *spec) { if (!slots) { PyErr_Format( @@ -648,7 +648,7 @@ PyModule_FromSlotsAndSpec(PyModuleDef_Slot *slots, PyObject *spec) return NULL; } // Fill in enough of a PyModuleDef to pass to common machinery - PyModuleDef def_like = {.m_slots = slots}; + PyModuleDef def_like = {.m_slots = (PyModuleDef_Slot *)slots}; return module_from_def_and_spec(&def_like, spec, PYTHON_API_VERSION, NULL); diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 346d6f6f83f8bd..ad46ba129ed328 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -5768,7 +5768,7 @@ PyType_GetModuleState(PyTypeObject *type) * has the given token. */ PyObject * -borrow_module_by_token(PyTypeObject *type, void *token) +borrow_module_by_token(PyTypeObject *type, const void *token) { assert(PyType_Check(type)); @@ -5833,7 +5833,7 @@ PyType_GetModuleByDef(PyTypeObject *type, PyModuleDef *def) } PyObject * -PyType_GetModuleByToken(PyTypeObject *type, void *token) +PyType_GetModuleByToken(PyTypeObject *type, const void *token) { return Py_XNewRef(borrow_module_by_token(type, token)); } From 058e0db9f99672012216ce11cec00ae58da39fa6 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 21 Oct 2025 16:11:15 +0200 Subject: [PATCH 48/81] test_cext: Remove the 'create_moduledef.c' workaround --- Lib/test/test_cext/__init__.py | 1 - Lib/test/test_cext/create_moduledef.c | 29 --------------- Lib/test/test_cext/extension.c | 52 ++++++--------------------- Lib/test/test_cext/setup.py | 1 - 4 files changed, 10 insertions(+), 73 deletions(-) delete mode 100644 Lib/test/test_cext/create_moduledef.c diff --git a/Lib/test/test_cext/__init__.py b/Lib/test/test_cext/__init__.py index fb93c6ccbb637d..a52c2241f5d9d4 100644 --- a/Lib/test/test_cext/__init__.py +++ b/Lib/test/test_cext/__init__.py @@ -14,7 +14,6 @@ SOURCES = [ os.path.join(os.path.dirname(__file__), 'extension.c'), - os.path.join(os.path.dirname(__file__), 'create_moduledef.c'), ] SETUP = os.path.join(os.path.dirname(__file__), 'setup.py') diff --git a/Lib/test/test_cext/create_moduledef.c b/Lib/test/test_cext/create_moduledef.c deleted file mode 100644 index 249c3163552c03..00000000000000 --- a/Lib/test/test_cext/create_moduledef.c +++ /dev/null @@ -1,29 +0,0 @@ -// Workaround for testing _Py_OPAQUE_PYOBJECT. -// See end of 'extension.c' - - -#undef _Py_OPAQUE_PYOBJECT -#undef Py_LIMITED_API -#include "Python.h" - - -// (repeated definition to avoid creating a header) -extern PyObject *testcext_create_moduledef( - const char *name, const char *doc, - PyMethodDef *methods, PyModuleDef_Slot *slots); - -PyObject *testcext_create_moduledef( - const char *name, const char *doc, - PyMethodDef *methods, PyModuleDef_Slot *slots) { - - static struct PyModuleDef _testcext_module = { - PyModuleDef_HEAD_INIT, - }; - if (!_testcext_module.m_name) { - _testcext_module.m_name = name; - _testcext_module.m_doc = doc; - _testcext_module.m_methods = methods; - _testcext_module.m_slots = slots; - } - return PyModuleDef_Init(&_testcext_module); -} diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c index 73fc67ae59d18f..8ccc5f484a4cbf 100644 --- a/Lib/test/test_cext/extension.c +++ b/Lib/test/test_cext/extension.c @@ -78,7 +78,7 @@ _testcext_exec( return 0; } -#define _FUNC_NAME(NAME) PyInit_ ## NAME +#define _FUNC_NAME(NAME) PyModExport_ ## NAME #define FUNC_NAME(NAME) _FUNC_NAME(NAME) // Converting from function pointer to void* has undefined behavior, but @@ -88,58 +88,26 @@ _testcext_exec( _Py_COMP_DIAG_PUSH #if defined(__GNUC__) #pragma GCC diagnostic ignored "-Wpedantic" +#pragma GCC diagnostic ignored "-Wcast-qual" #elif defined(__clang__) #pragma clang diagnostic ignored "-Wpedantic" +#pragma clang diagnostic ignored "-Wcast-qual" #endif +PyDoc_STRVAR(_testcext_doc, "C test extension."); + static PyModuleDef_Slot _testcext_slots[] = { + {Py_mod_name, STR(MODULE_NAME)}, + {Py_mod_doc, (void*)(char*)_testcext_doc}, {Py_mod_exec, (void*)_testcext_exec}, + {Py_mod_methods, _testcext_methods}, {0, NULL} }; _Py_COMP_DIAG_POP -PyDoc_STRVAR(_testcext_doc, "C test extension."); - -#ifndef _Py_OPAQUE_PYOBJECT - -static struct PyModuleDef _testcext_module = { - PyModuleDef_HEAD_INIT, // m_base - STR(MODULE_NAME), // m_name - _testcext_doc, // m_doc - 0, // m_size - _testcext_methods, // m_methods - _testcext_slots, // m_slots - NULL, // m_traverse - NULL, // m_clear - NULL, // m_free -}; - - -PyMODINIT_FUNC -FUNC_NAME(MODULE_NAME)(void) -{ - return PyModuleDef_Init(&_testcext_module); -} - -#else // _Py_OPAQUE_PYOBJECT - -// Opaque PyObject means that PyModuleDef is also opaque and cannot be -// declared statically. See PEP 793. -// So, this part of module creation is split into a separate source file -// which uses non-limited API. - -// (repeated definition to avoid creating a header) -extern PyObject *testcext_create_moduledef( - const char *name, const char *doc, - PyMethodDef *methods, PyModuleDef_Slot *slots); - - -PyMODINIT_FUNC +PyMODEXPORT_FUNC FUNC_NAME(MODULE_NAME)(void) { - return testcext_create_moduledef( - STR(MODULE_NAME), _testcext_doc, _testcext_methods, _testcext_slots); + return _testcext_slots; } - -#endif // _Py_OPAQUE_PYOBJECT diff --git a/Lib/test/test_cext/setup.py b/Lib/test/test_cext/setup.py index 4d71e4751f7afd..67dfddec751791 100644 --- a/Lib/test/test_cext/setup.py +++ b/Lib/test/test_cext/setup.py @@ -99,7 +99,6 @@ def main(): # Define _Py_OPAQUE_PYOBJECT macro if opaque_pyobject: cflags.append(f'-D_Py_OPAQUE_PYOBJECT') - sources.append('create_moduledef.c') if internal: cflags.append('-DTEST_INTERNAL_C_API=1') From e6ff4b906f91eaef62d3cfdccc7ccb54f0eeb441 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 24 Oct 2025 16:15:21 +0200 Subject: [PATCH 49/81] Make helpers static --- Objects/moduleobject.c | 2 +- Objects/typeobject.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index d84056d08d8d15..3a53cdfd5cd50f 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -362,7 +362,7 @@ _PyModule_CreateInitialized(PyModuleDef* module, int module_api_version) return (PyObject*)m; } -PyObject * +static PyObject * module_from_def_and_spec( PyModuleDef* def_like, /* not necessarily a valid Python object */ PyObject *spec, diff --git a/Objects/typeobject.c b/Objects/typeobject.c index ad46ba129ed328..355735d7ace9f9 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -5767,7 +5767,7 @@ PyType_GetModuleState(PyTypeObject *type) /* Return borrowed ref to the module of the first superclass where the module * has the given token. */ -PyObject * +static PyObject * borrow_module_by_token(PyTypeObject *type, const void *token) { assert(PyType_Check(type)); From b651cefb660178c5d799ec3653d43e7e1d3cc973 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 24 Oct 2025 16:15:31 +0200 Subject: [PATCH 50/81] Add comment to assertion --- Objects/moduleobject.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 3a53cdfd5cd50f..bde28e7cd0e0fb 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -29,6 +29,10 @@ static PyMemberDef module_members[] = { static void assert_def_missing_or_redundant(PyModuleObject *m) { + /* We copy all relevant info into the module object. + * Modules created using a def keep a reference to that (statically + * allocated) def; the info there should match what we have in the module. + */ #ifdef Py_DEBUG if (m->md_token_is_def) { PyModuleDef *def = (PyModuleDef *)m->md_token; From 4d4c6b7dbb9f276ad5b32c86b97b7f37a0034dbd Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 24 Oct 2025 17:00:58 +0200 Subject: [PATCH 51/81] Mark test PyModuleDefs as GIL-friendly --- Modules/_testcapi/module.c | 1 + Modules/_testmultiphase.c | 1 + 2 files changed, 2 insertions(+) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 28ba603889c1e6..e895c2573209a9 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -278,6 +278,7 @@ module_from_def_multiple_exec(PyObject *self, PyObject *spec) static PyModuleDef_Slot slots[] = { {Py_mod_exec, simple_exec}, {Py_mod_exec, another_exec}, + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, {0}, }; static PyModuleDef def = { diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index aa734df9d15eac..ad03924572a895 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -861,6 +861,7 @@ PyInit__testmultiphase_exec_multiple(void) {Py_mod_exec, execfunc_a1}, {Py_mod_exec, execfunc_b2}, {Py_mod_exec, execfunc_c3}, + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, {0} }; static PyModuleDef def = { From 54c496e7ce35fdf9f5c4069659f5071ebed508ee Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 24 Oct 2025 18:24:39 +0200 Subject: [PATCH 52/81] Add vcxproj entries for test files --- PCbuild/_testcapi.vcxproj | 1 + PCbuild/_testcapi.vcxproj.filters | 3 +++ 2 files changed, 4 insertions(+) diff --git a/PCbuild/_testcapi.vcxproj b/PCbuild/_testcapi.vcxproj index a355a5fc25707a..68707a54ff6b87 100644 --- a/PCbuild/_testcapi.vcxproj +++ b/PCbuild/_testcapi.vcxproj @@ -126,6 +126,7 @@ + diff --git a/PCbuild/_testcapi.vcxproj.filters b/PCbuild/_testcapi.vcxproj.filters index 05128d3ac36efc..b0e75ce433ab14 100644 --- a/PCbuild/_testcapi.vcxproj.filters +++ b/PCbuild/_testcapi.vcxproj.filters @@ -111,6 +111,9 @@ Source Files + + Source Files + Source Files From fdc49edee56a4c3cce4035c53cc060dde721514d Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sun, 26 Oct 2025 15:06:57 +0100 Subject: [PATCH 53/81] Add to Limited API --- Doc/data/stable_abi.dat | 6 ++++++ Lib/test/test_stable_abi_ctypes.py | 5 +++++ Misc/stable_abi.toml | 28 ++++++++++++++++++++++++++++ PC/python3dll.c | 5 +++++ 4 files changed, 44 insertions(+) diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 7ad5f3ecfab5b4..a694ac7ca5da6b 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -391,6 +391,7 @@ func,PyLong_FromUnsignedNativeBytes,3.14,, func,PyLong_FromVoidPtr,3.2,, func,PyLong_GetInfo,3.2,, data,PyLong_Type,3.2,, +macro,PyMODEXPORT_FUNC,3.15,, data,PyMap_Type,3.2,, func,PyMapping_Check,3.2,, func,PyMapping_GetItemString,3.2,, @@ -437,8 +438,10 @@ func,PyModule_AddObjectRef,3.10,, func,PyModule_AddStringConstant,3.2,, func,PyModule_AddType,3.10,, func,PyModule_Create2,3.2,, +func,PyModule_Exec,3.15,, func,PyModule_ExecDef,3.7,, func,PyModule_FromDefAndSpec2,3.7,, +func,PyModule_FromSlotsAndSpec,3.15,, func,PyModule_GetDef,3.2,, func,PyModule_GetDict,3.2,, func,PyModule_GetFilename,3.2,, @@ -446,6 +449,8 @@ func,PyModule_GetFilenameObject,3.2,, func,PyModule_GetName,3.2,, func,PyModule_GetNameObject,3.7,, func,PyModule_GetState,3.2,, +func,PyModule_GetStateSize,3.15,, +func,PyModule_GetToken,3.15,, func,PyModule_New,3.2,, func,PyModule_NewObject,3.7,, func,PyModule_SetDocString,3.7,, @@ -704,6 +709,7 @@ func,PyType_GetFlags,3.2,, func,PyType_GetFullyQualifiedName,3.13,, func,PyType_GetModule,3.10,, func,PyType_GetModuleByDef,3.13,, +func,PyType_GetModuleByToken,3.15,, func,PyType_GetModuleName,3.13,, func,PyType_GetModuleState,3.10,, func,PyType_GetName,3.11,, diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index cbec7e43a7c9fb..7167646ecc6734 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -469,8 +469,10 @@ def test_windows_feature_macros(self): "PyModule_AddStringConstant", "PyModule_AddType", "PyModule_Create2", + "PyModule_Exec", "PyModule_ExecDef", "PyModule_FromDefAndSpec2", + "PyModule_FromSlotsAndSpec", "PyModule_GetDef", "PyModule_GetDict", "PyModule_GetFilename", @@ -478,6 +480,8 @@ def test_windows_feature_macros(self): "PyModule_GetName", "PyModule_GetNameObject", "PyModule_GetState", + "PyModule_GetStateSize", + "PyModule_GetToken", "PyModule_New", "PyModule_NewObject", "PyModule_SetDocString", @@ -733,6 +737,7 @@ def test_windows_feature_macros(self): "PyType_GetFullyQualifiedName", "PyType_GetModule", "PyType_GetModuleByDef", + "PyType_GetModuleByToken", "PyType_GetModuleName", "PyType_GetModuleState", "PyType_GetName", diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml index 4a03cc76f5e1e9..672953d23e350f 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -2607,3 +2607,31 @@ added = '3.15' [const.PyABIInfo_FREETHREADING_AGNOSTIC] added = '3.15' +[function.PyModule_FromSlotsAndSpec] + added = '3.15' +[function.PyModule_Exec] + added = '3.15' +[function.PyModule_GetToken] + added = '3.15' +[function.PyType_GetModuleByToken] + added = '3.15' +[function.PyModule_GetStateSize] + added = '3.15' +[macro.PyMODEXPORT_FUNC] + added = '3.15' +[const.Py_mod_name] + added = '3.15' +[const.Py_mod_doc] + added = '3.15' +[const.Py_mod_state_size] + added = '3.15' +[const.Py_mod_methods] + added = '3.15' +[const.Py_mod_state_traverse] + added = '3.15' +[const.Py_mod_state_clear] + added = '3.15' +[const.Py_mod_state_free] + added = '3.15' +[const.Py_mod_token] + added = '3.15' diff --git a/PC/python3dll.c b/PC/python3dll.c index 05c86e6d5924d4..99e0f05fe03209 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -416,8 +416,10 @@ EXPORT_FUNC(PyModule_AddObjectRef) EXPORT_FUNC(PyModule_AddStringConstant) EXPORT_FUNC(PyModule_AddType) EXPORT_FUNC(PyModule_Create2) +EXPORT_FUNC(PyModule_Exec) EXPORT_FUNC(PyModule_ExecDef) EXPORT_FUNC(PyModule_FromDefAndSpec2) +EXPORT_FUNC(PyModule_FromSlotsAndSpec) EXPORT_FUNC(PyModule_GetDef) EXPORT_FUNC(PyModule_GetDict) EXPORT_FUNC(PyModule_GetFilename) @@ -425,6 +427,8 @@ EXPORT_FUNC(PyModule_GetFilenameObject) EXPORT_FUNC(PyModule_GetName) EXPORT_FUNC(PyModule_GetNameObject) EXPORT_FUNC(PyModule_GetState) +EXPORT_FUNC(PyModule_GetStateSize) +EXPORT_FUNC(PyModule_GetToken) EXPORT_FUNC(PyModule_New) EXPORT_FUNC(PyModule_NewObject) EXPORT_FUNC(PyModule_SetDocString) @@ -668,6 +672,7 @@ EXPORT_FUNC(PyType_GetFlags) EXPORT_FUNC(PyType_GetFullyQualifiedName) EXPORT_FUNC(PyType_GetModule) EXPORT_FUNC(PyType_GetModuleByDef) +EXPORT_FUNC(PyType_GetModuleByToken) EXPORT_FUNC(PyType_GetModuleName) EXPORT_FUNC(PyType_GetModuleState) EXPORT_FUNC(PyType_GetName) From dab2207fd603e93a6f9ba228584132e64aa8f8a6 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sun, 26 Oct 2025 16:22:08 +0100 Subject: [PATCH 54/81] gh-140633: AppleFrameworkLoader: Ignore AttributeError when setting __file__ --- Lib/importlib/_bootstrap_external.py | 8 +++++++- .../2025-10-26-16-24-12.gh-issue-140633.ioayC1.rst | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-26-16-24-12.gh-issue-140633.ioayC1.rst diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 9269bb77806c83..a1319c0f6a91aa 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -1506,7 +1506,13 @@ def create_module(self, spec): ) # Ensure that the __file__ points at the .fwork location - module.__file__ = path + try: + module.__file__ = path + except AttributeError: + # Not important enough to report. + # (The error is also ignored in _bootstrap._init_module_attrs or + # import_run_extension in import.c) + pass return module diff --git a/Misc/NEWS.d/next/Library/2025-10-26-16-24-12.gh-issue-140633.ioayC1.rst b/Misc/NEWS.d/next/Library/2025-10-26-16-24-12.gh-issue-140633.ioayC1.rst new file mode 100644 index 00000000000000..9675a5d427a0d9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-26-16-24-12.gh-issue-140633.ioayC1.rst @@ -0,0 +1,2 @@ +Ignore :exc:`AttributeError` when setting a module's ``__file__`` attribute +when loading an extension module packaged as Apple Framework. From 4f7bccb7e9c83483933e5e2d1f3526f96e7cb6fb Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sun, 26 Oct 2025 16:43:17 +0100 Subject: [PATCH 55/81] test_cext: Define PyInit_* function which asserts that it wasn't called --- Lib/test/test_cext/extension.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c index 8ccc5f484a4cbf..0f668c1da32d6e 100644 --- a/Lib/test/test_cext/extension.c +++ b/Lib/test/test_cext/extension.c @@ -111,3 +111,17 @@ FUNC_NAME(MODULE_NAME)(void) { return _testcext_slots; } + +// Also define the soft-deprecated entrypoint to ensure it isn't called + +#define _INITFUNC_NAME(NAME) PyInit_ ## NAME +#define INITFUNC_NAME(NAME) _INITFUNC_NAME(NAME) + +PyMODINIT_FUNC +INITFUNC_NAME(MODULE_NAME)(void) +{ + PyErr_SetString( + PyExc_AssertionError, + "PyInit_* function called while a PyModExport_* one is available"); + return NULL; +} From 628942c42281bff8e5bf9b98ebc4367460eafcad Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sun, 26 Oct 2025 16:45:31 +0100 Subject: [PATCH 56/81] Add a blurb --- .../next/C_API/2025-10-26-16-45-28.gh-issue-140556.s__Dae.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/C_API/2025-10-26-16-45-28.gh-issue-140556.s__Dae.rst diff --git a/Misc/NEWS.d/next/C_API/2025-10-26-16-45-28.gh-issue-140556.s__Dae.rst b/Misc/NEWS.d/next/C_API/2025-10-26-16-45-28.gh-issue-140556.s__Dae.rst new file mode 100644 index 00000000000000..61da60903ee821 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-10-26-16-45-28.gh-issue-140556.s__Dae.rst @@ -0,0 +1,2 @@ +:pep:`793`: Add a new entry point for C extension modules, +``PyModExport_``. From 08ee61690235e928eec18bf4747a0250d98f34a3 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 27 Oct 2025 08:13:17 +0100 Subject: [PATCH 57/81] Make tokens `const` to pass check for C globals --- Modules/_testcapi/module.c | 6 +++--- Modules/_testmultiphase.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index e895c2573209a9..ab32bc29cac839 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -109,13 +109,13 @@ module_from_slots_gc(PyObject *self, PyObject *spec) return mod; } -static char test_token; +static const char test_token; static PyObject * module_from_slots_token(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { - {Py_mod_token, &test_token}, + {Py_mod_token, (void*)&test_token}, {0}, }; PyObject *mod = PyModule_FromSlotsAndSpec(slots, spec); @@ -368,7 +368,7 @@ _PyTestCapi_Init_Module(PyObject *m) ADD_INT_MACRO(Py_mod_token); #undef ADD_INT_MACRO if (PyModule_Add(m, "module_test_token", - PyLong_FromVoidPtr(&test_token)) < 0) + PyLong_FromVoidPtr((void*)&test_token)) < 0) { return -1; } diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index ad03924572a895..3be60b2b6ea0d3 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -1119,7 +1119,7 @@ modexport_smoke_get_state_int(PyObject *mod, PyObject *arg) return PyLong_FromLong(*state); } -static char modexport_smoke_test_token; +static const char modexport_smoke_test_token; static PyObject * modexport_smoke_get_test_token(PyObject *mod, PyObject *arg) From ffe332209f10ccc2dc398a4cf70add67ba7b1f3e Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 27 Oct 2025 09:06:53 +0100 Subject: [PATCH 58/81] One more cast --- Modules/_testmultiphase.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index 3be60b2b6ea0d3..7247bf1ef480ad 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -1124,7 +1124,7 @@ static const char modexport_smoke_test_token; static PyObject * modexport_smoke_get_test_token(PyObject *mod, PyObject *arg) { - return PyLong_FromVoidPtr(&modexport_smoke_test_token); + return PyLong_FromVoidPtr((void*)&modexport_smoke_test_token); } static PyObject * From 0855bdd9928c6a3457b24398946d7c758dd9ddd3 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 27 Oct 2025 09:04:53 +0100 Subject: [PATCH 59/81] test_import: Move modexport tests to their own class --- Lib/test/test_import/__init__.py | 159 +++++++++++++++---------------- 1 file changed, 77 insertions(+), 82 deletions(-) diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 288b45ae0c3e47..abc048e186c202 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -2482,88 +2482,6 @@ def test_testmultiphase_exec_multiple(self): self.assertLess(keys.index('a'), keys.index('b')) self.assertLess(keys.index('b'), keys.index('c')) - @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") - def test_from_modexport(self): - modname = '_test_from_modexport' - filename = _testmultiphase.__file__ - module = import_extension_from_file(modname, filename, - put_in_sys_modules=False) - - self.assertEqual(module.__name__, modname) - - @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") - def test_from_modexport_null(self): - modname = '_test_from_modexport_null' - filename = _testmultiphase.__file__ - with self.assertRaises(SystemError): - import_extension_from_file(modname, filename, - put_in_sys_modules=False) - - @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") - def test_from_modexport_exception(self): - modname = '_test_from_modexport_exception' - filename = _testmultiphase.__file__ - with self.assertRaises(ValueError): - import_extension_from_file(modname, filename, - put_in_sys_modules=False) - - @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") - def test_from_modexport_create_nonmodule(self): - modname = '_test_from_modexport_create_nonmodule' - filename = _testmultiphase.__file__ - module = import_extension_from_file(modname, filename, - put_in_sys_modules=False) - self.assertIsInstance(module, str) - - @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") - def test_from_modexport_smoke(self): - # General positive test for sundry features - # (PyModule_FromSlotsAndSpec tests exercise these more carefully) - modname = '_test_from_modexport_smoke' - filename = _testmultiphase.__file__ - module = import_extension_from_file(modname, filename, - put_in_sys_modules=False) - self.assertEqual(module.__doc__, "the expected docstring") - self.assertEqual(module.number, 147) - self.assertEqual(module.get_state_int(), 258) - self.assertGreater(module.get_test_token(), 0) - - @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") - def test_from_modexport_smoke_token(self): - _testcapi = import_module("_testcapi") - - modname = '_test_from_modexport_smoke' - filename = _testmultiphase.__file__ - module = import_extension_from_file(modname, filename, - put_in_sys_modules=False) - token = module.get_test_token() - self.assertEqual(_testcapi.pymodule_get_token(module), token) - - tp = module.Example - self.assertEqual(_testcapi.pytype_getmodulebytoken(tp, token), module) - class Sub(tp): - pass - self.assertEqual(_testcapi.pytype_getmodulebytoken(Sub, token), module) - - @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") - def test_from_modexport_empty_slots(self): - # Module to test that: - # - no slots are mandatory for PyModExport - # - the slots array is used as the default token - modname = '_test_from_modexport_empty_slots' - filename = _testmultiphase.__file__ - module = import_extension_from_file( - modname, filename, put_in_sys_modules=False) - - self.assertEqual(module.__name__, modname) - self.assertEqual(module.__doc__, None) - - _testcapi = import_module("_testcapi") - 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()) - @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") def test_python_compat(self): module = 'threading' @@ -3460,6 +3378,83 @@ def test_basic_multiple_interpreters_reset_each(self): # * module's global state was initialized, not reset +@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") +class ModexportTests(unittest.TestCase): + def test_from_modexport(self): + modname = '_test_from_modexport' + filename = _testmultiphase.__file__ + module = import_extension_from_file(modname, filename, + put_in_sys_modules=False) + + self.assertEqual(module.__name__, modname) + + def test_from_modexport_null(self): + modname = '_test_from_modexport_null' + filename = _testmultiphase.__file__ + with self.assertRaises(SystemError): + import_extension_from_file(modname, filename, + put_in_sys_modules=False) + + def test_from_modexport_exception(self): + modname = '_test_from_modexport_exception' + filename = _testmultiphase.__file__ + with self.assertRaises(ValueError): + import_extension_from_file(modname, filename, + put_in_sys_modules=False) + + def test_from_modexport_create_nonmodule(self): + modname = '_test_from_modexport_create_nonmodule' + filename = _testmultiphase.__file__ + module = import_extension_from_file(modname, filename, + put_in_sys_modules=False) + self.assertIsInstance(module, str) + + def test_from_modexport_smoke(self): + # General positive test for sundry features + # (PyModule_FromSlotsAndSpec tests exercise these more carefully) + modname = '_test_from_modexport_smoke' + filename = _testmultiphase.__file__ + module = import_extension_from_file(modname, filename, + put_in_sys_modules=False) + self.assertEqual(module.__doc__, "the expected docstring") + self.assertEqual(module.number, 147) + self.assertEqual(module.get_state_int(), 258) + self.assertGreater(module.get_test_token(), 0) + + def test_from_modexport_smoke_token(self): + _testcapi = import_module("_testcapi") + + modname = '_test_from_modexport_smoke' + filename = _testmultiphase.__file__ + module = import_extension_from_file(modname, filename, + put_in_sys_modules=False) + token = module.get_test_token() + self.assertEqual(_testcapi.pymodule_get_token(module), token) + + tp = module.Example + self.assertEqual(_testcapi.pytype_getmodulebytoken(tp, token), module) + class Sub(tp): + pass + self.assertEqual(_testcapi.pytype_getmodulebytoken(Sub, token), module) + + def test_from_modexport_empty_slots(self): + # Module to test that: + # - no slots are mandatory for PyModExport + # - the slots array is used as the default token + modname = '_test_from_modexport_empty_slots' + filename = _testmultiphase.__file__ + module = import_extension_from_file( + modname, filename, put_in_sys_modules=False) + + self.assertEqual(module.__name__, modname) + self.assertEqual(module.__doc__, None) + + _testcapi = import_module("_testcapi") + 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()) + @cpython_only class TestMagicNumber(unittest.TestCase): def test_magic_number_endianness(self): From 895a04616fc568d66699022dbd0a0cc5d611d90b Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 27 Oct 2025 14:25:28 +0100 Subject: [PATCH 60/81] Remove redundant comment --- Objects/moduleobject.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index bde28e7cd0e0fb..9f4cfd454fd059 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -304,7 +304,6 @@ module_copy_members_from_deflike( PyModuleObject *md, PyModuleDef *def_like /* not necessarily a valid Python object */) { - /* def may not be a valid PyObject*, see */ md->md_state_size = def_like->m_size; md->md_state_traverse = def_like->m_traverse; md->md_state_clear = def_like->m_clear; From 25fe91303526cf7023a6f41d891eded8a6471c59 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 14:16:43 +0100 Subject: [PATCH 61/81] Rename _PyImport_GetModInitFunc2 to _PyImport_GetModuleExportHooks --- Include/internal/pycore_importdl.h | 3 +-- Python/import.c | 18 +++++++++--------- Python/importdl.c | 9 +++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Include/internal/pycore_importdl.h b/Include/internal/pycore_importdl.h index 88367740c3f023..12a32a5f70e51f 100644 --- a/Include/internal/pycore_importdl.h +++ b/Include/internal/pycore_importdl.h @@ -98,8 +98,7 @@ extern void _Py_ext_module_loader_result_apply_error( typedef PyObject *(*PyModInitFunction)(void); typedef PyModuleDef_Slot *(*PyModExportFunction)(void); #ifdef HAVE_DYNAMIC_LOADING -// function changed signature, the "2" suffix helps avoid ABI issues -extern int _PyImport_GetModInitFunc2( +extern int _PyImport_GetModuleExportHooks( struct _Py_ext_module_loader_info *info, FILE *fp, PyModInitFunction *modinit, PyModExportFunction *modexport); #endif diff --git a/Python/import.c b/Python/import.c index bda3d8cd591ded..50a75c866d149e 100644 --- a/Python/import.c +++ b/Python/import.c @@ -672,8 +672,8 @@ _PyImport_ClearModulesByIndex(PyInterpreterState *interp) (6). first time (not found in _PyRuntime.imports.extensions): A. _imp_create_dynamic_impl() -> import_find_extension() - B. _imp_create_dynamic_impl() -> _PyImport_GetModInitFunc() - C. _PyImport_GetModInitFunc(): load + B. _imp_create_dynamic_impl() -> _PyImport_GetModuleExportHooks() + C. _PyImport_GetModuleExportHooks(): load D. _imp_create_dynamic_impl() -> import_run_extension() E. import_run_extension() -> _PyImport_RunModInitFunc() F. _PyImport_RunModInitFunc(): call @@ -747,8 +747,8 @@ _PyImport_ClearModulesByIndex(PyInterpreterState *interp) (6). every time: A. _imp_create_dynamic_impl() -> import_find_extension() (not found) - B. _imp_create_dynamic_impl() -> _PyImport_GetModInitFunc() - C. _PyImport_GetModInitFunc(): load + B. _imp_create_dynamic_impl() -> _PyImport_GetModuleExportHooks() + C. _PyImport_GetModuleExportHooks(): load D. _imp_create_dynamic_impl() -> import_run_extension() E. import_run_extension() -> _PyImport_RunModInitFunc() F. _PyImport_RunModInitFunc(): call @@ -779,8 +779,8 @@ _PyImport_ClearModulesByIndex(PyInterpreterState *interp) (6). every time: A. _imp_create_dynamic_impl() -> import_find_extension() (not found) - B. _imp_create_dynamic_impl() -> _PyImport_GetModInitFunc() - C. _PyImport_GetModInitFunc(): load + B. _imp_create_dynamic_impl() -> _PyImport_GetModuleExportHooks() + C. _PyImport_GetModuleExportHooks(): load D. _imp_create_dynamic_impl() -> import_run_modexport() E. import_run_modexport(): call F. import_run_modexport() -> PyModule_FromSlotsAndSpec() @@ -2192,7 +2192,7 @@ import_run_extension(PyThreadState *tstate, PyModInitFunction p0, assert_multiphase_def(def); assert(mod == NULL); /* Note that we cheat a little by not repeating the calls - * to _PyImport_GetModInitFunc() and _PyImport_RunModInitFunc(). */ + * to _PyImport_GetModuleExportHooks() and _PyImport_RunModInitFunc(). */ mod = PyModule_FromDefAndSpec(def, spec); if (mod == NULL) { goto error; @@ -4747,7 +4747,7 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file) } /* We would move this (and the fclose() below) into - * _PyImport_GetModInitFunc(), but it isn't clear if the intervening + * _PyImport_GetModuleExportHooks(), but it isn't clear if the intervening * code relies on fp still being open. */ FILE *fp; if (file != NULL) { @@ -4762,7 +4762,7 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file) PyModInitFunction p0 = NULL; PyModExportFunction ex0 = NULL; - _PyImport_GetModInitFunc2(&info, fp, &p0, &ex0); + _PyImport_GetModuleExportHooks(&info, fp, &p0, &ex0); if (ex0) { mod = import_run_modexport(tstate, ex0, &info, spec); goto cleanup; diff --git a/Python/importdl.c b/Python/importdl.c index 02f50c0c970708..23a55c39677100 100644 --- a/Python/importdl.c +++ b/Python/importdl.c @@ -395,10 +395,11 @@ findfuncptr(const char *prefix, const char *name_buf, } int -_PyImport_GetModInitFunc2(struct _Py_ext_module_loader_info *info, - FILE *fp, - PyModInitFunction *modinit, - PyModExportFunction *modexport) +_PyImport_GetModuleExportHooks( + struct _Py_ext_module_loader_info *info, + FILE *fp, + PyModInitFunction *modinit, + PyModExportFunction *modexport) { *modinit = NULL; *modexport = NULL; From ab04b28280d583a8d107c95c75d2d5c3edbd5395 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:09:46 +0100 Subject: [PATCH 62/81] Update outdated comment --- Include/internal/pycore_moduleobject.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index 8930577c23b069..be83712ebed6a0 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -37,7 +37,7 @@ typedef struct { inquiry md_state_clear; freefunc md_state_free; void *md_token; - _Py_modexecfunc md_exec; /* only set if md_def_or_null is NULL */ + _Py_modexecfunc md_exec; /* only set if md_token_is_def is true */ } PyModuleObject; static inline PyModuleDef* _PyModule_GetDefOrNull(PyObject *arg) { From d9013b16c09a4c53d4465300287230051e0d1168 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:11:10 +0100 Subject: [PATCH 63/81] Add a _PyModule_CAST macro --- Include/internal/pycore_moduleobject.h | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index be83712ebed6a0..98c77da7dd6116 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -40,9 +40,11 @@ typedef struct { _Py_modexecfunc md_exec; /* only set if md_token_is_def is true */ } PyModuleObject; +#define _PyModule_CAST(op) \ + (assert(PyModule_Check(op)), _Py_CAST(PyModuleObject*, (op))) + static inline PyModuleDef* _PyModule_GetDefOrNull(PyObject *arg) { - assert(PyModule_Check(arg)); - PyModuleObject *mod = (PyModuleObject *)arg; + PyModuleObject *mod = _PyModule_CAST(arg); if (mod->md_token_is_def) { return (PyModuleDef*)((PyModuleObject *)mod)->md_token; } @@ -50,19 +52,16 @@ static inline PyModuleDef* _PyModule_GetDefOrNull(PyObject *arg) { } static inline PyModuleDef* _PyModule_GetToken(PyObject *arg) { - assert(PyModule_Check(arg)); - PyModuleObject *mod = (PyModuleObject *)arg; + PyModuleObject *mod = _PyModule_CAST(arg); return mod->md_token; } static inline void* _PyModule_GetState(PyObject* mod) { - assert(PyModule_Check(mod)); - return ((PyModuleObject *)mod)->md_state; + return _PyModule_CAST(mod)->md_state; } static inline PyObject* _PyModule_GetDict(PyObject *mod) { - assert(PyModule_Check(mod)); - PyObject *dict = ((PyModuleObject *)mod) -> md_dict; + PyObject *dict = _PyModule_CAST(mod)->md_dict; // _PyModule_GetDict(mod) must not be used after calling module_clear(mod) assert(dict != NULL); return dict; // borrowed reference From 788835e2f7b1bf7bcc4c476f49881f410ef16c7f Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:12:25 +0100 Subject: [PATCH 64/81] Better argument naming in headers --- Include/moduleobject.h | 4 ++-- Include/object.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Include/moduleobject.h b/Include/moduleobject.h index 925bbfa7f2a10c..f7bc10adebf4a7 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -123,8 +123,8 @@ PyAPI_FUNC(int) PyUnstable_Module_SetGIL(PyObject *module, void *gil); PyAPI_FUNC(PyObject *) PyModule_FromSlotsAndSpec(const PyModuleDef_Slot *, PyObject *spec); PyAPI_FUNC(int) PyModule_Exec(PyObject *mod); -PyAPI_FUNC(int) PyModule_GetStateSize(PyObject *mod, Py_ssize_t *size_p); -PyAPI_FUNC(int) PyModule_GetToken(PyObject *, void **token_p); +PyAPI_FUNC(int) PyModule_GetStateSize(PyObject *mod, Py_ssize_t *result); +PyAPI_FUNC(int) PyModule_GetToken(PyObject *, void **result); #endif #ifndef _Py_OPAQUE_PYOBJECT diff --git a/Include/object.h b/Include/object.h index 210808f168e3ae..dec779f1ba4ee8 100644 --- a/Include/object.h +++ b/Include/object.h @@ -840,7 +840,8 @@ PyAPI_FUNC(int) PyType_Freeze(PyTypeObject *type); #endif #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) -PyAPI_FUNC(PyObject *) PyType_GetModuleByToken(PyTypeObject *, const void *); +PyAPI_FUNC(PyObject *) PyType_GetModuleByToken(PyTypeObject *type, + const void *token); #endif #ifdef __cplusplus From fde9850c4a3dcba713a239e418678747cd9926d5 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:15:27 +0100 Subject: [PATCH 65/81] Move comment to top --- Lib/test/test_capi/test_module.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index bc19ef76cd2174..cf60b3d6f9f7a2 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -1,3 +1,6 @@ +# The C functions used by this module are in: +# Modules/_testcapi/module.c + import unittest import types from test.support import import_helper, subTests @@ -15,9 +18,6 @@ class FakeSpec: 'Py_mod_token', ) -# The C functions used by this module are in: -# Modules/_testcapi/module.c - def def_and_token(mod): return ( _testcapi.pymodule_get_def(mod), From 0465b0dc3f7585eec36af7b117476fc00ae58c67 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:19:59 +0100 Subject: [PATCH 66/81] 'noop', not 'trivial --- Modules/_testcapi/module.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index ab32bc29cac839..6deb703d5d619b 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -77,19 +77,19 @@ module_from_slots_methods(PyObject *self, PyObject *spec) return PyModule_FromSlotsAndSpec(slots, spec); } -static int trivial_traverse(PyObject *self, visitproc visit, void *arg) { +static int noop_traverse(PyObject *self, visitproc visit, void *arg) { return 0; } -static int trivial_clear(PyObject *self) { return 0; } -static void trivial_free(void *self) { } +static int noop_clear(PyObject *self) { return 0; } +static void noop_free(void *self) { } static PyObject * module_from_slots_gc(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { - {Py_mod_state_traverse, trivial_traverse}, - {Py_mod_state_clear, trivial_clear}, - {Py_mod_state_free, trivial_free}, + {Py_mod_state_traverse, noop_traverse}, + {Py_mod_state_clear, noop_clear}, + {Py_mod_state_free, noop_free}, {0}, }; PyObject *mod = PyModule_FromSlotsAndSpec(slots, spec); @@ -103,9 +103,9 @@ module_from_slots_gc(PyObject *self, PyObject *spec) Py_DECREF(mod); return NULL; } - assert(traverse == &trivial_traverse); - assert(clear == &trivial_clear); - assert(free == &trivial_free); + assert(traverse == &noop_traverse); + assert(clear == &noop_clear); + assert(free == &noop_free); return mod; } From 268840a4ac53a645e60510d3293be18a2e7b7404 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:20:43 +0100 Subject: [PATCH 67/81] PyLong_AsInt --- Modules/_testcapi/module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 6deb703d5d619b..b730b25e57653f 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -186,7 +186,7 @@ slot_from_object(PyObject *obj) if (slot_id_obj == NULL) { return -1; } - int slot_id = PyLong_AsLong(slot_id_obj); + int slot_id = PyLong_AsInt(slot_id_obj); if (PyErr_Occurred()) { return -1; } From 3e13677f51e04d83b47050f59199c2b82739f468 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:21:37 +0100 Subject: [PATCH 68/81] Add spaces inside curly braces --- Modules/_testmultiphase.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index 7247bf1ef480ad..628f7cc567bfbc 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -850,17 +850,17 @@ PyInit__testmultiphase_exec_unreported_exception(void) return PyModuleDef_Init(&def_exec_unreported_exception); } -static int execfunc_a1(PyObject*m) {return PyModule_AddIntConstant(m, "a", 1);} -static int execfunc_b2(PyObject*m) {return PyModule_AddIntConstant(m, "b", 2);} -static int execfunc_c3(PyObject*m) {return PyModule_AddIntConstant(m, "c", 3);} +static int execfn_a1(PyObject*m) { return PyModule_AddIntConstant(m, "a", 1); } +static int execfn_b2(PyObject*m) { return PyModule_AddIntConstant(m, "b", 2); } +static int execfn_c3(PyObject*m) { return PyModule_AddIntConstant(m, "c", 3); } PyMODINIT_FUNC PyInit__testmultiphase_exec_multiple(void) { static PyModuleDef_Slot slots[] = { - {Py_mod_exec, execfunc_a1}, - {Py_mod_exec, execfunc_b2}, - {Py_mod_exec, execfunc_c3}, + {Py_mod_exec, execfn_a1}, + {Py_mod_exec, execfn_b2}, + {Py_mod_exec, execfn_c3}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, {0} }; From fdff39bdfbf47ca26962c3994e822a7ad2b918cc Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:22:25 +0100 Subject: [PATCH 69/81] PyErr_FormatUnraisable, not PyErr_WriteUnraisable --- Modules/_testmultiphase.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index 628f7cc567bfbc..220fa888e49a52 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -1142,7 +1142,7 @@ modexport_smoke_free(PyObject *mod) { int *state = PyModule_GetState(mod); if (!state) { - PyErr_WriteUnraisable(mod); + PyErr_FormatUnraisable("Exception ignored in module %R free", mod); } assert(*state == 258); } From 7c29575d9d1e7cdc90e8ad5c220e485de8cd64b5 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:24:09 +0100 Subject: [PATCH 70/81] Use NDEBUG --- Objects/moduleobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 9f4cfd454fd059..da3f67fe3c96b1 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -33,7 +33,7 @@ assert_def_missing_or_redundant(PyModuleObject *m) { * Modules created using a def keep a reference to that (statically * allocated) def; the info there should match what we have in the module. */ -#ifdef Py_DEBUG +#ifdef NDEBUG if (m->md_token_is_def) { PyModuleDef *def = (PyModuleDef *)m->md_token; assert(def); @@ -44,7 +44,7 @@ assert_def_missing_or_redundant(PyModuleObject *m) { DO_ASSERT(free); #undef DO_ASSERT } -#endif // Py_DEBUG +#endif // NDEBUG } From b163b28770f35ebdb3ef4da4192d6e40215e9214 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:25:28 +0100 Subject: [PATCH 71/81] Wrap in do-while --- Objects/moduleobject.c | 50 ++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index da3f67fe3c96b1..c5e4a74494494c 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -411,30 +411,32 @@ module_from_def_and_spec( for (cur_slot = def_like->m_slots; cur_slot && cur_slot->slot; cur_slot++) { // Macro to copy a non-NULL, non-repeatable slot that's unusable with // PyModuleDef. The destination must be initially NULL. -#define COPY_COMMON_SLOT(SLOT, TYPE, DEST) \ - if (!(TYPE)(cur_slot->value)) { \ - PyErr_Format( \ - PyExc_SystemError, \ - "module %s: " #SLOT " must not be NULL", \ - name); \ - goto error; \ - } \ - if (original_def) { \ - PyErr_Format( \ - PyExc_SystemError, \ - "module %s: " #SLOT " used with PyModuleDef", \ - name); \ - goto error; \ - } \ - if (DEST) { \ - PyErr_Format( \ - PyExc_SystemError, \ - "module %s has more than one " #SLOT " slot", \ - name); \ - goto error; \ - } \ - DEST = (TYPE)(cur_slot->value); \ - ///////////////////////////////////////////////////////////// +#define COPY_COMMON_SLOT(SLOT, TYPE, DEST) \ + do { \ + if (!(TYPE)(cur_slot->value)) { \ + PyErr_Format( \ + PyExc_SystemError, \ + "module %s: " #SLOT " must not be NULL", \ + name); \ + goto error; \ + } \ + if (original_def) { \ + PyErr_Format( \ + PyExc_SystemError, \ + "module %s: " #SLOT " used with PyModuleDef", \ + name); \ + goto error; \ + } \ + if (DEST) { \ + PyErr_Format( \ + PyExc_SystemError, \ + "module %s has more than one " #SLOT " slot", \ + name); \ + goto error; \ + } \ + DEST = (TYPE)(cur_slot->value); \ + } while (0); \ + ///////////////////////////////////////////////////////////////// switch (cur_slot->slot) { case Py_mod_create: if (create) { From 6ddcb01aa5901fbb61ac4743bc453ab0f88f4467 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:26:40 +0100 Subject: [PATCH 72/81] PyErr_SetString instead of PyErr_Format --- Objects/moduleobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index c5e4a74494494c..8d5cc15a48e9ca 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -647,7 +647,7 @@ PyObject * PyModule_FromSlotsAndSpec(const PyModuleDef_Slot *slots, PyObject *spec) { if (!slots) { - PyErr_Format( + PyErr_SetString( PyExc_SystemError, "PyModule_FromSlotsAndSpec called with NULL slots"); return NULL; From 2013284464513601539e33ce23784afb2e70b9be Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:27:40 +0100 Subject: [PATCH 73/81] Repr in error messages --- Objects/moduleobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 8d5cc15a48e9ca..16400a56582037 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -680,7 +680,7 @@ run_exec_func(PyObject *module, int (*exec)(PyObject *)) if (!PyErr_Occurred()) { PyErr_Format( PyExc_SystemError, - "execution of %S failed without setting an exception", + "execution of %R failed without setting an exception", module); } return -1; @@ -688,7 +688,7 @@ run_exec_func(PyObject *module, int (*exec)(PyObject *)) if (PyErr_Occurred()) { _PyErr_FormatFromCause( PyExc_SystemError, - "execution of module %S raised unreported exception", + "execution of module %R raised unreported exception", module); return -1; } From 52af39790cfc44b37a4ced45f5df69a4cd6e5fa4 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:30:09 +0100 Subject: [PATCH 74/81] PEP 7 whitespace --- Objects/moduleobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 16400a56582037..8a7de23b3db35d 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -698,7 +698,7 @@ run_exec_func(PyObject *module, int (*exec)(PyObject *)) static int alloc_state(PyObject *module) { - if(!PyModule_Check(module)) { + if (!PyModule_Check(module)) { PyErr_Format(PyExc_TypeError, "expected module, got %T", module); return -1; } @@ -732,7 +732,7 @@ PyModule_Exec(PyObject *module) } PyModuleDef *def = _PyModule_GetDefOrNull(module); - if(def) { + if (def) { return PyModule_ExecDef(module, def); } return 0; From 01d52ba6203076eebb92c6c8dc04d1a6d83ead56 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 28 Oct 2025 18:30:35 +0100 Subject: [PATCH 75/81] Make sure Victor's edits are credited Co-Authored-By: Victor Stinner From ba6e602ae2bacf540525fe0004c9391352ae4f12 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 29 Oct 2025 09:02:04 +0100 Subject: [PATCH 76/81] Correct logic --- Objects/moduleobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 8a7de23b3db35d..c67e84fcfa749c 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -33,7 +33,7 @@ assert_def_missing_or_redundant(PyModuleObject *m) { * Modules created using a def keep a reference to that (statically * allocated) def; the info there should match what we have in the module. */ -#ifdef NDEBUG +#ifndef NDEBUG if (m->md_token_is_def) { PyModuleDef *def = (PyModuleDef *)m->md_token; assert(def); From 26b57f2767da0bcdcac4c04277c614f2023b0d7a Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 3 Nov 2025 14:52:25 +0100 Subject: [PATCH 77/81] Get the spaces just right Co-authored-by: Kumar Aditya --- Include/internal/pycore_moduleobject.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index 98c77da7dd6116..1814e3f26726cc 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -43,15 +43,15 @@ typedef struct { #define _PyModule_CAST(op) \ (assert(PyModule_Check(op)), _Py_CAST(PyModuleObject*, (op))) -static inline PyModuleDef* _PyModule_GetDefOrNull(PyObject *arg) { +static inline PyModuleDef *_PyModule_GetDefOrNull(PyObject *arg) { PyModuleObject *mod = _PyModule_CAST(arg); if (mod->md_token_is_def) { - return (PyModuleDef*)((PyModuleObject *)mod)->md_token; + return (PyModuleDef *)((PyModuleObject *)mod)->md_token; } return NULL; } -static inline PyModuleDef* _PyModule_GetToken(PyObject *arg) { +static inline PyModuleDef *_PyModule_GetToken(PyObject *arg) { PyModuleObject *mod = _PyModule_CAST(arg); return mod->md_token; } From 928f23ba9aec51e8ad13d5cc8fef1d434c6c9112 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 3 Nov 2025 15:40:47 +0100 Subject: [PATCH 78/81] _PyModule_GetDefOrNull never raises --- Python/import.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Python/import.c b/Python/import.c index ffb1a21d99f597..6cf4a061ca610f 100644 --- a/Python/import.c +++ b/Python/import.c @@ -2294,9 +2294,8 @@ _PyImport_FixupBuiltin(PyThreadState *tstate, PyObject *mod, const char *name, PyModuleDef *def = _PyModule_GetDefOrNull(mod); if (def == NULL) { - if (!PyErr_Occurred()) { - PyErr_BadInternalCall(); - } + assert(!PyErr_Occurred()); + PyErr_BadInternalCall(); goto finally; } From 55ae793e08bcf512e2823561c939eecb4aef9fff Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 4 Nov 2025 13:11:32 +0100 Subject: [PATCH 79/81] Remove _PyModule_GetGCHooks; get the hooks directly via internal API --- Include/moduleobject.h | 2 -- Lib/test/test_capi/test_module.py | 9 +++++++++ Modules/_testcapi/module.c | 13 +++++++++---- Modules/_testinternalcapi.c | 29 +++++++++++++++++++++++++++++ Objects/moduleobject.c | 15 --------------- 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/Include/moduleobject.h b/Include/moduleobject.h index f7bc10adebf4a7..e83bc395aa4618 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -96,8 +96,6 @@ struct PyModuleDef_Slot { #ifndef Py_LIMITED_API #define _Py_mod_LAST_SLOT 13 -PyAPI_FUNC(int) _PyModule_GetGCHooks( - PyObject *, traverseproc*, inquiry*, freefunc*); // For testing #endif #endif /* New in 3.5 */ diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index cf60b3d6f9f7a2..7ec23e637d7de6 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -85,6 +85,15 @@ def test_gc(self): self.assertEqual(mod.__name__, 'testmod') self.assertEqual(mod.__doc__, None) + # Check that the requested hook functions (which module_from_slots_gc + # stores as attributes) match what's in the module (as retrieved by + # _testinternalcapi.module_get_gc_hooks) + _testinternalcapi = import_helper.import_module('_testinternalcapi') + traverse, clear, free = _testinternalcapi.module_get_gc_hooks(mod) + self.assertEqual(traverse, mod.traverse) + self.assertEqual(clear, mod.clear) + self.assertEqual(free, mod.free) + def test_token(self): mod = _testcapi.module_from_slots_token(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index b730b25e57653f..ad6ea2bc17715b 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -99,13 +99,18 @@ module_from_slots_gc(PyObject *self, PyObject *spec) traverseproc traverse; inquiry clear; freefunc free; - if (_PyModule_GetGCHooks(mod, &traverse, &clear, &free) < 0) { + if (PyModule_Add(mod, "traverse", PyLong_FromVoidPtr(&noop_traverse)) < 0) { + Py_DECREF(mod); + return NULL; + } + if (PyModule_Add(mod, "clear", PyLong_FromVoidPtr(&noop_clear)) < 0) { + Py_DECREF(mod); + return NULL; + } + if (PyModule_Add(mod, "free", PyLong_FromVoidPtr(&noop_free)) < 0) { Py_DECREF(mod); return NULL; } - assert(traverse == &noop_traverse); - assert(clear == &noop_clear); - assert(free == &noop_free); return mod; } diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index c2647d405e25bc..dede05960d78b6 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2418,6 +2418,34 @@ set_vectorcall_nop(PyObject *self, PyObject *func) Py_RETURN_NONE; } +static PyObject * +module_get_gc_hooks(PyObject *self, PyObject *arg) +{ + PyModuleObject *mod = (PyModuleObject *)arg; + PyObject *traverse = NULL; + PyObject *clear = NULL; + PyObject *free = NULL; + PyObject *result = NULL; + traverse = PyLong_FromVoidPtr(mod->md_state_traverse); + if (!traverse) { + goto finally; + } + clear = PyLong_FromVoidPtr(mod->md_state_clear); + if (!clear) { + goto finally; + } + free = PyLong_FromVoidPtr(mod->md_state_free); + if (!free) { + goto finally; + } + result = PyTuple_FromArray((PyObject*[]){ traverse, clear, free }, 3); +finally: + Py_XDECREF(traverse); + Py_XDECREF(clear); + Py_XDECREF(free); + return result; +} + static PyMethodDef module_functions[] = { {"get_configs", get_configs, METH_NOARGS}, {"get_recursion_depth", get_recursion_depth, METH_NOARGS}, @@ -2527,6 +2555,7 @@ static PyMethodDef module_functions[] = { #endif {"simple_pending_call", simple_pending_call, METH_O}, {"set_vectorcall_nop", set_vectorcall_nop, METH_O}, + {"module_get_gc_hooks", module_get_gc_hooks, METH_O}, {NULL, NULL} /* sentinel */ }; diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index c67e84fcfa749c..b174090c3cc799 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -828,21 +828,6 @@ PyModule_GetToken(PyObject *m, void **token_p) return 0; } -int -_PyModule_GetGCHooks(PyObject *m, traverseproc *traverse, - inquiry *clear, freefunc *free) -{ - if (!PyModule_Check(m)) { - PyErr_BadInternalCall(); - return -1; - } - PyModuleObject *mod = (PyModuleObject *)m; - *traverse = mod->md_state_traverse; - *clear = mod->md_state_clear; - *free = mod->md_state_free; - return 0; -} - PyObject* PyModule_GetNameObject(PyObject *mod) { From 1e6b18ccec8e4ce9eb8d0ad2f681274b046cfe64 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 4 Nov 2025 13:15:49 +0100 Subject: [PATCH 80/81] Style fixes --- Include/internal/pycore_moduleobject.h | 2 +- Objects/moduleobject.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h index 1814e3f26726cc..c34e42e826e476 100644 --- a/Include/internal/pycore_moduleobject.h +++ b/Include/internal/pycore_moduleobject.h @@ -46,7 +46,7 @@ typedef struct { static inline PyModuleDef *_PyModule_GetDefOrNull(PyObject *arg) { PyModuleObject *mod = _PyModule_CAST(arg); if (mod->md_token_is_def) { - return (PyModuleDef *)((PyModuleObject *)mod)->md_token; + return (PyModuleDef *)mod->md_token; } return NULL; } diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index b174090c3cc799..9dee03bdb5ee55 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -28,7 +28,8 @@ static PyMemberDef module_members[] = { }; static void -assert_def_missing_or_redundant(PyModuleObject *m) { +assert_def_missing_or_redundant(PyModuleObject *m) +{ /* We copy all relevant info into the module object. * Modules created using a def keep a reference to that (statically * allocated) def; the info there should match what we have in the module. From 4d58846e632a08a17de99029783fb483f73bfe06 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 4 Nov 2025 14:01:54 +0100 Subject: [PATCH 81/81] Remove unused variables --- Modules/_testcapi/module.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index ad6ea2bc17715b..9349445351eaca 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -96,9 +96,6 @@ module_from_slots_gc(PyObject *self, PyObject *spec) if (!mod) { return NULL; } - traverseproc traverse; - inquiry clear; - freefunc free; if (PyModule_Add(mod, "traverse", PyLong_FromVoidPtr(&noop_traverse)) < 0) { Py_DECREF(mod); return NULL;