Skip to content

Adopt PEP 793 module export hooks for C extensions (keep PyInit for compatibility)#19482

Draft
davin-core wants to merge 7 commits intoastropy:mainfrom
davin-core:RFC-PEP-793-compliance
Draft

Adopt PEP 793 module export hooks for C extensions (keep PyInit for compatibility)#19482
davin-core wants to merge 7 commits intoastropy:mainfrom
davin-core:RFC-PEP-793-compliance

Conversation

@davin-core
Copy link
Copy Markdown

@davin-core davin-core commented Mar 23, 2026

This PR adds PEP 793-style module export hooks (PyModExport_*) to Astropy C extensions that currently define modules via PyModuleDef_HEAD_INIT.

What changed
Added guarded slot-based module export definitions in:

  • io/votable/src/tablewriter.c
  • stats/src/fast_sigma_clip.c
  • time/src/parse_times.c
  • utils/xml/src/iterparse.c
  • wcs/src/astropy_wcs.c

fixes #19478

do let's me know if I am misssing something. I am quite new to the code base

@github-actions
Copy link
Copy Markdown
Contributor

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.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

@davin-core
Copy link
Copy Markdown
Author

@neutrinoceros I am seeing repeated failures in py311-test-image-mpldev-cov on TestBasic.test_noncelestial_angular in test_images.py with only a hash mismatch and no related code changes. Since this env tracks Matplotlib dev, is this considered expected baseline drift? Should we update py311-test-image-mpldev-cov.json for this case, or is there concern this indicates a real WCSAxes regression?

@neutrinoceros
Copy link
Copy Markdown
Contributor

I've been aware of this problem for a couple days but have not taken the time to open an issue yet (I will now). In any case it's definitely unrelated to this PR and not blocking for either review or merge.

@neutrinoceros
Copy link
Copy Markdown
Contributor

(for posterity this unrelated error is now discussed at #19483)

@neutrinoceros neutrinoceros self-assigned this Mar 23, 2026
@neutrinoceros
Copy link
Copy Markdown
Contributor

Thank you for opening this. I need to read through the PEP to review this carefully. I'll plan to do it tomorrow. Meanwhile, I have an observation: I would expect that the existing declarations using PyMODINIT_FUNC would also become conditional and live between #else and #endif. Am I missing something ?

@davin-core
Copy link
Copy Markdown
Author

davin-core commented Mar 23, 2026

@neutrinoceros Thank you for the feedback! I think your suggestion makes sense. I can improve my PR by wrapping the new PEP 793 slots-style module init in #if PY_VERSION_HEX >= 0x030C0000 and putting the old PyMODINIT_FUNC init inside the corresponding #else. I can also use a macro wrapper for 0x030C0000 to make the code cleaner and easier to maintain across Python versions. This what you means right?

@neutrinoceros
Copy link
Copy Markdown
Contributor

Yes, that is what I mean. I don't think defining a macro is necessary, especially since I can't think of a natural place for it to live (though I'm open to suggestions)

0x030C0000 is probably not quite right; IIUC the reference issue is python/cpython#140550, and it's not quite finished. It'd be great to specify the exact hex for the first 3.15 alpha version that has the relevant API, assuming it already exists.

Signed-off-by: Davin <dev.davicheanin@gmail.com>
Copy link
Copy Markdown
Contributor

@neutrinoceros neutrinoceros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is better, but now I'm seeing a handful of definitions that are only ever used in the <=3.14 path (including, but not necessarily limited to a couple static struct PyModuleDef). Could you move these to the appropriate region so they're simply not included unless they're needed ?

@davin-core
Copy link
Copy Markdown
Author

@neutrinoceros I will

  1. Remove the top-level static struct PyModuleDef moduledef declaration.
  2. Re-declare moduledef inside the #else branch, immediately before the PyInit_* function.
  3. Leave the PyMODEXPORT/module-slots path unchanged.

Does that sound right to you?

@neutrinoceros
Copy link
Copy Markdown
Contributor

yes

Copy link
Copy Markdown
Contributor

@neutrinoceros neutrinoceros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm seeing a couple #if !defined(...) directives. I would prefer we keep the style as uniform as possible to ease cleanup and followup refactors in the future; can we normalize the style to #if defined(...) / #else, ideally using a single if/else pair per module ?

davin-core and others added 2 commits March 27, 2026 10:15
Co-authored-by: Clément Robert <cr52@protonmail.com>
Signed-off-by: Davin <dev.davicheanin@gmail.com>
@davin-core
Copy link
Copy Markdown
Author

@neutrinoceros Thanks for the review and the style suggestion. I updated the module-init conditionals to use a uniform #if defined(...) / #else pattern, and removed the #if !defined(...) . I see that it is only in fast_sigma_clip.c

@neutrinoceros
Copy link
Copy Markdown
Contributor

Thank you !
This now looks good to me, however we don't have systematic testing against Python 3.15 at the moment, so could you explain how this can be tested locally ?

@davin-core
Copy link
Copy Markdown
Author

From my understanding, Python 3.15 is currently in its alpha stage. I could create a virtual environment, pull this PR branch, and try to trigger the relevant functions. I can then report back on any issues I encounter. I could consider introducing a targeted test suite to help ensure compatibility with Python 3.15.

@neutrinoceros
Copy link
Copy Markdown
Contributor

yes, it's in alpha stage right now. The last alpha is expected in about 10 days.

I could consider introducing a targeted test suite to help ensure compatibility with Python 3.15.

that wouldn't work for us, as numpy doesn't have compatible binaries yet, and we don't want to spend compute time on building numpy. This is why I'm asking for a plan to test locally.

@davin-core
Copy link
Copy Markdown
Author

then I am not sure too, perhaps we wait until we get more support before we test and mergred it then?

@neutrinoceros
Copy link
Copy Markdown
Contributor

hum, so you mean you have not checked the validity of your own patch before submitting this PR ? This is a bit unsettling.

For reference, one way to check your patch compiles correctly, using uv, is1

uv build -p 3.15 --no-config

I could confirm that it builds correctly.

Then, I attempted to run tests with

uv venv -p 3.15.0a7 -c
uv pip install --no-config -e '.[test]'
uv run --no-sync pytest

and hit an exception at collection time:

AttributeError: module 'astropy.time._parse_times' has no attribute 'dt_pars'

In order to make make sure the error was specific to your branch I also did the same thing on main and this time, collection ran fine. One test did fail (which I'll report and possibly fix independently), but that's still different enough of a story that I know something needs fixing in your branch.
I suggest you iterate locally using the method I show cased. Feel free to push incomplete patches and report back if you get stuck. I'll mark this as a draft for the time being (do feel free to undraft if you get to a stable state !)

Footnotes

  1. I'm using --no-config because we normally prevent uv from building numpy from source, but it's absolutely required for building astropy itself here

@neutrinoceros neutrinoceros marked this pull request as draft March 28, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFC: PEP 793 compliance (stable init API for C extensions)

3 participants