Skip to content

Commit bc07de6

Browse files
authored
ENH: Make f2py use multiphase init (numpy#31578)
Adapt f2py to use multiphase init (and on Python 3.15+, to use PEP 793 and 820 init). This is mainly useful for future Limited API support (although mutliphase init is generally "preferred" and so arguably better anyway). The main complication was the usercode that gets injected into the module init function. This wasn't tested (so I added a test) but user code with error handling requires return NULL to be valid, so I moved it to a separate function that keeps that interface. One potential issue to flag is if people are doing embedding type things and manually calling the PyInit_ function. This isn't recommended and isn't an insurmountable problem, but will break that case.
1 parent f898c60 commit bc07de6

3 files changed

Lines changed: 93 additions & 28 deletions

File tree

numpy/f2py/common_rules.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ def dadd(line, s=doc):
120120
f'(f2py_setup_{name});')
121121
cadd('}\n')
122122
iadd(f'\ttmp = PyFortranObject_New(f2py_{name}_def,f2py_init_{name});')
123-
iadd('\tif (tmp == NULL) return NULL;')
124-
iadd(f'\tif (F2PyDict_SetItemString(d, "{name}", tmp) == -1) return NULL;')
123+
iadd('\tif (tmp == NULL) return -1;')
124+
iadd(f'\tif (F2PyDict_SetItemString(d, "{name}", tmp) == -1) return -1;')
125125
iadd('\tPy_DECREF(tmp);')
126126
tname = name.replace('_', '\\_')
127127
dadd(f'\\subsection{{Common block \\texttt{{{tname}}}}}\n')

numpy/f2py/rules.py

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -239,26 +239,18 @@
239239
{NULL,NULL}
240240
};
241241
242-
static struct PyModuleDef moduledef = {
243-
PyModuleDef_HEAD_INIT,
244-
"#modulename#",
245-
NULL,
246-
-1,
247-
f2py_module_methods,
248-
NULL,
249-
NULL,
250-
NULL,
251-
NULL
252-
};
242+
static PyObject *f2py_module_interface_usercode(PyObject *m, PyObject *d) {
243+
/* Returns a PyObject* because existing user code will return NULL on exit */
244+
#interface_usercode#
245+
return m;
246+
}
253247
254-
PyMODINIT_FUNC PyInit_#modulename#(void) {
248+
static int f2py_module_exec(PyObject *m) {
255249
int i;
256-
PyObject *m,*d, *s, *tmp;
257-
m = #modulename#_module = PyModule_Create(&moduledef);
258-
Py_SET_TYPE(&PyFortran_Type, &PyType_Type);
259-
import_array();
260-
if (PyErr_Occurred())
261-
{PyErr_SetString(PyExc_ImportError, \"can't initialize module #modulename# (failed to import numpy)\"); return m;}
250+
PyObject *d, *s, *tmp;
251+
#modulename#_module = m;
252+
Py_SET_TYPE((PyObject*)&PyFortran_Type, &PyType_Type);
253+
import_array2(\"can't initialize module #modulename# (failed to import numpy)\", -1);
262254
d = PyModule_GetDict(m);
263255
s = PyUnicode_FromString(\"#f2py_version#\");
264256
PyDict_SetItemString(d, \"__version__\", s);
@@ -282,27 +274,74 @@
282274
PyDict_SetItemString(d, f2py_routine_defs[i].name, tmp);
283275
Py_DECREF(tmp);
284276
}
277+
285278
#initf2pywraphooks#
286279
#initf90modhooks#
287280
#initcommonhooks#
288-
#interface_usercode#
289-
290-
#ifdef Py_GIL_DISABLED
291-
// signal whether this module supports running with the GIL disabled
292-
PyUnstable_Module_SetGIL(m , #gil_used#);
293-
#endif
281+
if (!f2py_module_interface_usercode(m, d)) return -1;
294282
295283
#ifdef F2PY_REPORT_ATEXIT
296284
if (! PyErr_Occurred())
297285
on_exit(f2py_report_on_exit,(void*)\"#modulename#\");
298286
#endif
299287
300288
if (PyType_Ready(&PyFortran_Type) < 0) {
301-
return NULL;
289+
return -1;
302290
}
303291
304-
return m;
292+
return 0;
293+
}
294+
295+
#ifndef Py_TARGET_ABI3T
296+
static PyModuleDef_Slot f2py_module_slots[] = {
297+
{Py_mod_exec, (void*)f2py_module_exec},
298+
#ifdef Py_GIL_DISABLED
299+
{Py_mod_gil, (void*)#gil_used#},
300+
#endif
301+
#if (defined(Py_LIMITED_API) && Py_LIMITED_API >= 0x030C0000) || (!defined(Py_LIMITED_API) && PY_VERSION_HEX >= 0x030C0000)
302+
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
303+
#endif
304+
{0, NULL}
305+
};
306+
307+
static struct PyModuleDef moduledef = {
308+
PyModuleDef_HEAD_INIT,
309+
"#modulename#",
310+
NULL,
311+
0,
312+
f2py_module_methods,
313+
f2py_module_slots,
314+
NULL,
315+
NULL,
316+
NULL
317+
};
318+
319+
PyMODINIT_FUNC PyInit_#modulename#(void) {
320+
return PyModuleDef_Init(&moduledef);
305321
}
322+
#endif
323+
324+
#if (defined(Py_LIMITED_API) && Py_LIMITED_API >= 0x030F0000) || (!defined(Py_LIMITED_API) && PY_VERSION_HEX >= 0x030F0000)
325+
PyABIInfo_VAR(f2py_abi_info);
326+
327+
static PySlot f2py_module_pyslots[] = {
328+
PySlot_PTR_STATIC(Py_mod_abi, &f2py_abi_info),
329+
PySlot_PTR_STATIC(Py_mod_name, "#modulename#"),
330+
#if defined(Py_GIL_DISABLED) || defined(Py_TARGET_ABI3T)
331+
PySlot_PTR(Py_mod_gil, #gil_used#),
332+
#endif
333+
PySlot_PTR_STATIC(Py_mod_methods, f2py_module_methods),
334+
PySlot_PTR(Py_mod_exec, f2py_module_exec),
335+
PySlot_END
336+
};
337+
338+
339+
PyMODEXPORT_FUNC PyModExport_#modulename#(void)
340+
{
341+
return f2py_module_pyslots;
342+
}
343+
#endif
344+
306345
#ifdef __cplusplus
307346
}
308347
#endif

numpy/f2py/tests/test_usercode.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from . import util
2+
3+
4+
class TestUserCode(util.F2PyTest):
5+
suffix = ".pyf"
6+
module_name = "user_code_success"
7+
8+
# The failure path in this code isn't expected to happen (and would be
9+
# untestable if it did, because it would happen in the test setup).
10+
# However, it does need to compile correctly.
11+
code = f"""
12+
python module {module_name}
13+
interface
14+
usercode '''
15+
{{
16+
PyObject *value = PyUnicode_FromString("Hello from the user code");
17+
if (!value) return NULL;
18+
if (PyModule_AddObjectRef(m, "foo", value) < 0) return NULL;
19+
}}
20+
'''
21+
end interface
22+
end python module {module_name}
23+
"""
24+
25+
def test_user_code_success(self):
26+
assert self.module.foo == "Hello from the user code"

0 commit comments

Comments
 (0)