RFC: implement PEP 793/820 multi-phase module init (wcs) - #20494
neutrinoceros wants to merge 1 commit into
Conversation
|
Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.
|
71df779 to
4fb14bb
Compare
|
I will try and review this shortly but just to check, does astropy.wcs import correctly on an abi3t build with these changes? |
|
I'm not sure actually. At this stage I'm trying to resolve build-time errors one by one but as long as I can't build the whole package without error I'm missing runtime validation. It should be possible to build only this one module with |
|
I wonder if it would make more sense to just have one big PR - I know it's normally nice to split thing up, but if we can't know if it will work at runtime it's a bit of guesswork as to whether a change is valid or not. I wouldn't mind reviewing a big PR if I knew it encapsulated all the changes required to get it working. |
|
Ok, then I'll draft it for now |
| #endif | ||
| }; | ||
|
|
||
| #ifdef _Py_OPAQUE_PYOBJECT |
There was a problem hiding this comment.
I'd probably do
| #ifdef _Py_OPAQUE_PYOBJECT | |
| #if (defined(Py_LIMITED_API) && Py_LIMITED_API >= 0x030F0000)) || (!defined(Py_LIMITED_API) && PY_VERSION_HEX >= 0x030F0000) |
since _Py_OPAQUE_PYOBJECT is no longer the recommended way of detecting opaque objects (that's Py_TARGET_ABI3T). But also there's no reason not to do this on any 3.15+ build.
There was a problem hiding this comment.
I don't intend to support targeting a version of the limited API other than the one we're already building against, so, it seems to me that I could even simplify this condition to
#if PY_VERSION_HEX >= 0x030F0000
is that right ?
There was a problem hiding this comment.
That should be fine, but if it were my code I'd probably still go for the longer explicit version.
It's definitely a good rule when preparing your official wheels, but I've found it convenient to be able to build stuff locally on whatever version of Python you're developing on (in general rather than for Astropy specifically).
There was a problem hiding this comment.
fair enough. I encapsulated this condition into a _ASTROPY_MULTI_PHASE_MODULE_INIT pre-proc variable, and added an inline comment based on your explanation
| { | ||
| PyObject* m; | ||
|
|
||
| wcs_errexc[0] = NULL; /* Success */ |
There was a problem hiding this comment.
All this is getting missed in the PEP-793 version. Which likely is important.
It probably wants to be in a Py_mod_exec slot.
There was a problem hiding this comment.
Thanks !
I just pushed a tentative implementation for it.
There was a problem hiding this comment.
An alternative to consider (that I think may be clearer) is to define module_exec unconditionally then define the old init function as:
PyMODINIT_FUNC
PyInit__wcs(void)
{
PyObject* m;
m = PyModule_Create(&moduledef);
if (module_exec(m) != 0) {
// Error
Py_CLEAR(m);
}
return m;
}
There was a problem hiding this comment.
oh, indeed that's a lot better. I just pushed it. Thanks !
| #endif | ||
|
|
||
| #ifdef _Py_OPAQUE_PYOBJECT | ||
| return 0; |
There was a problem hiding this comment.
The return value is guesswork: I'm assuming that 0 means "no exception" in this context, though the documentation is unclear
https://docs.python.org/3/c-api/module.html#c.Py_mod_exec
There was a problem hiding this comment.
Yeah it isn't clearly documented but I'm almost certain that 0 means "good"
wcs)wcs)
44d4103 to
a778bc8
Compare
|
I think I handled every point raised so far. @da-woods, let me know if you're happy with the current state before I proceed with other modules |
a778bc8 to
2cff895
Compare
mhvk
left a comment
There was a problem hiding this comment.
@neutrinoceros - I struggled similarly to you in #20183, but followed some of the CPython developer docs perhaps a little more closely (don't remember which, sadly). Anyway, some comments inline following that. See also #20183 itself.
| return 1; | ||
|
|
||
| import_array(); | ||
| import_array1(1); |
There was a problem hiding this comment.
The newer way to import numpy is
if (PyArray_ImportNumPyAPI() < 0) {
return NULL;
}
though really that should still be in init, not exec.
There was a problem hiding this comment.
pushed. Though, to be clear, are you saying that PyArray_ImportNumPyAPI doesn't need to be called in the multi-phase init mode ?
There was a problem hiding this comment.
It does need to be called, but more easily in PyInit see comment further down: #20494 (comment)
| PyInit__wcs(void) | ||
|
|
||
| { | ||
| PyObject* m; |
There was a problem hiding this comment.
You don't need to create the module in the new version, rather something like,
PyMODINIT_FUNC PyInit__wcs(void)
{
if (PyArray_ImportNumPyAPI() < 0) {
return NULL;
}
return PyModuleDef_Init(&moduledef);
}
There was a problem hiding this comment.
I don't think that's correct: module_exec would not run.
There was a problem hiding this comment.
It does - you need something like what I have in #20183,
static PyModuleDef_Slot scaler_module_slots[] = {
{Py_mod_exec, scaler_module_exec},
{0, NULL},
};
and those slots get added to the module def struct.
There was a problem hiding this comment.
Maybe I'm misunderstanding something but it looks to me like the docs explicitly mention we cannot do that:
https://docs.python.org/3/c-api/module.html#c.PyModuleDef.m_slots
When using legacy single-phase initialization, m_slots must be NULL
There was a problem hiding this comment.
But we're not using legacy single-phase initialization any more! Or, at least, isn't that the point?
There was a problem hiding this comment.
Why not just choose multi-phase? Might as well get it done and it works fine on older python (otherwise #20183 would not pass, nor would all those numpy pieces that have already been converted).
There was a problem hiding this comment.
It is a fairly easy conversion. You add scaler_module_slots as @mhvk suggests, and then you change
PyMODINIT_FUNC
PyInit__wcs(void)
{
// maybe initialize Numpy here too.
return (PyObject*)&moduledef;
}
and you're done.
There was a problem hiding this comment.
Why not just choose multi-phase?
Oh I see what happened here: I read #20494 (comment)
also there's no reason not to do this on any 3.15+ build.
and concluded that there were (implicitly) reasons to keep single-phase init for Python 3.14 and older. Is it the case, really, and do we care ? It sounds like we don't, in which case I could just simplify this by a lot.
There was a problem hiding this comment.
No there wasn't any intended implicit meaning in that comment about Python 3.14 and older. I don't know any reason why you should deliberately keep single-phase init.
There was a problem hiding this comment.
For clarity, I think the init needs return PyModuleDef_Init(&moduledef);
|
p.s. Aside, nice to do this! If you need more examples, see PRs linked under numpy's overall effort to go to multi-phase initialization: numpy/numpy#29021 |
2cff895 to
20a3da5
Compare
| PyMODEXPORT_FUNC | ||
| PyModExport__wcs(void) | ||
| { | ||
| return module_slots; |
There was a problem hiding this comment.
| return module_slots; | |
| if (PyArray_ImportNumPyAPI() < 0) { | |
| return NULL; | |
| } | |
| return module_slots; |
PyArray_ImportNumPyAPI definitely needs calling in both cases. I think @mhvk was proposing to put it there. I don't think it really matters where you put it provided it's called before you try to use Numpy. The documentation says it's fine to call multiple times.
Description
This is the first of a couple modules needing this compatibility layer. Here I'm focusing on getting a single one of them done correctly before migrating all of them, which I expect will be simpler now that I got the gist of it.
Contributes to #19478
AI Disclosure
N/A
Merge method