Skip to content

Commit 4845989

Browse files
author
Release Manager
committed
gh-41021: Refactor ``atexit.pyx`` <!-- ^ Please provide a concise and informative title. --> <!-- ^ Don't put issue numbers in the title, do this in the PR description below. --> <!-- ^ For example, instead of "Fixes #12345" use "Introduce new method to calculate 1 + 2". --> <!-- v Describe your changes below in detail. --> <!-- v Why is this change required? What problem does it solve? --> <!-- v If this PR resolves an open issue, please link to it here. For example, "Fixes #12345". --> I have refactor the ``atexit.pyx`` since for python 3.14, the changes of ``atexit`` is so much for supporting the nogil version. ### Python<=3.13 Implementation: - The ``atexit`` module stores callbacks in a C array of structs (``atexit_callback``). - The structure is defined in C as: ``` typedef struct {  PyObject *func;    PyObject *args;    PyObject *kwargs ;} atexit_callback; ``` - Callbacks are stored in the interp->atexit.callbacks field as a C array - The array can be directly accessed from Cython code using pointer arithmetic ### Python 3.14 Implementation: - The ``atexit`` module was refactored to use a Python ``PyList`` object instead of a C array. - The structure is now: ``` state.callbacks = [(func, args, kwargs), ...] # A Python list of tuples ``` - In the C implementation, callbacks are managed with: ``` PyObject *callbacks; // This is now a PyList ``` - Callbacks are inserted at the beginning (LIFO order) using ``PyList_Insert(state->callbacks, 0, callback)`` ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. - [ ] I have created tests covering the changes. - [ ] I have updated the documentation and checked the documentation preview. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on. For example, --> <!-- - #12345: short description why this is a dependency --> <!-- - #34567: ... --> python/cpython@3b76682 84d4461af python/cpython#127935 URL: #41021 Reported by: Chenxin Zhong Reviewer(s): Copilot, da-woods
2 parents 7b6f21f + 5f363a8 commit 4845989

File tree

1 file changed

+75
-24
lines changed

1 file changed

+75
-24
lines changed

src/sage/cpython/atexit.pyx

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -143,51 +143,99 @@ cdef class restore_atexit:
143143
_set_exithandlers(self._exithandlers)
144144

145145
from cpython.ref cimport PyObject
146+
import sys
146147

147-
# Implement "_atexit_callbacks()" for each supported python version
148+
# Implement a uniform interface for getting atexit callbacks
148149
cdef extern from *:
149150
"""
151+
#ifndef Py_BUILD_CORE
150152
#define Py_BUILD_CORE
153+
#endif
151154
#undef _PyGC_FINALIZED
152155
#include "internal/pycore_interp.h"
153156
#include "internal/pycore_pystate.h"
154-
#if PY_VERSION_HEX >= 0x030c0000
155-
// struct atexit_callback was renamed in 3.12 to atexit_py_callback
156-
#define atexit_callback atexit_py_callback
157-
#endif
158-
static atexit_callback ** _atexit_callbacks(PyObject *self) {
157+
158+
// Always define this struct for Cython's use
159+
typedef struct {
160+
PyObject *func;
161+
PyObject *args;
162+
PyObject *kwargs;
163+
} atexit_callback_struct;
164+
165+
#if PY_VERSION_HEX >= 0x030e0000
166+
// Python 3.14+: atexit uses a PyList
167+
static PyObject* get_atexit_callbacks_list(PyObject *self) {
159168
PyInterpreterState *interp = _PyInterpreterState_GET();
160169
struct atexit_state state = interp->atexit;
161170
return state.callbacks;
162171
}
172+
173+
// Dummy function for Python 3.14+ (never called)
174+
static atexit_callback_struct** get_atexit_callbacks_array(PyObject *self) {
175+
PyErr_SetString(PyExc_RuntimeError, "Python >= 3.14 has no atexit array");
176+
return NULL;
177+
}
178+
#else
179+
// Python < 3.14: atexit uses C array
180+
static atexit_callback_struct** get_atexit_callbacks_array(PyObject *self) {
181+
PyInterpreterState *interp = _PyInterpreterState_GET();
182+
struct atexit_state state = interp->atexit;
183+
// Cast from atexit_callback** to our struct type
184+
return (atexit_callback_struct**)state.callbacks;
185+
}
186+
187+
// Dummy function for Python < 3.14 (never called)
188+
static PyObject* get_atexit_callbacks_list(PyObject *self) {
189+
PyErr_SetString(PyExc_RuntimeError, "Python < 3.14 has no atexit list");
190+
return NULL;
191+
}
192+
#endif
163193
"""
164-
ctypedef struct atexit_callback:
194+
# Declare both functions - they exist in all Python versions (one is dummy)
195+
object get_atexit_callbacks_list(object module)
196+
197+
ctypedef struct atexit_callback_struct:
165198
PyObject* func
166199
PyObject* args
167200
PyObject* kwargs
168-
atexit_callback** _atexit_callbacks(object module)
201+
atexit_callback_struct** get_atexit_callbacks_array(object module) except NULL
169202

170203

171204
def _get_exithandlers():
172205
"""Return list of exit handlers registered with the atexit module."""
173-
cdef atexit_callback ** callbacks
174-
cdef atexit_callback callback
175-
cdef list exithandlers
206+
cdef list exithandlers = []
207+
cdef atexit_callback_struct ** callbacks
208+
cdef atexit_callback_struct callback
176209
cdef int idx
177210
cdef object kwargs
178-
179-
exithandlers = []
180-
callbacks = _atexit_callbacks(atexit)
181-
182-
for idx in range(atexit._ncallbacks()):
183-
callback = callbacks[idx][0]
184-
if callback.kwargs:
185-
kwargs = <object>callback.kwargs
186-
else:
187-
kwargs = {}
188-
exithandlers.append((<object>callback.func,
189-
<object>callback.args,
190-
kwargs))
211+
212+
# Python 3.14+ uses a PyList directly
213+
if sys.version_info >= (3, 14):
214+
callbacks_list = get_atexit_callbacks_list(atexit)
215+
if callbacks_list is None:
216+
return exithandlers
217+
# callbacks is a list of tuples: [(func, args, kwargs), ...]
218+
# Normalize kwargs to ensure it's always a dict (not None)
219+
# Note: In Python 3.14+, atexit stores callbacks in LIFO order
220+
# (most recently registered first), but we return them in FIFO
221+
# order (registration order) for consistency with earlier versions
222+
for item in reversed(callbacks_list):
223+
func, args, kwargs = item
224+
if kwargs is None:
225+
kwargs = {}
226+
exithandlers.append((func, args, kwargs))
227+
else:
228+
# Python < 3.14 uses C array
229+
callbacks = get_atexit_callbacks_array(atexit)
230+
for idx in range(atexit._ncallbacks()):
231+
callback = callbacks[idx][0]
232+
if callback.kwargs:
233+
kwargs = <object>callback.kwargs
234+
else:
235+
kwargs = {}
236+
exithandlers.append((<object>callback.func,
237+
<object>callback.args,
238+
kwargs))
191239
return exithandlers
192240

193241

@@ -202,6 +250,9 @@ def _set_exithandlers(exithandlers):
202250

203251
# We could do this more efficiently by directly rebuilding the array
204252
# of atexit_callbacks, but this is much simpler
253+
# Note: exithandlers is in registration order (FIFO).
254+
# In Python 3.14+, atexit.register prepends to the list (LIFO),
255+
# so registering in forward order gives us the correct execution order.
205256
for callback in exithandlers:
206257
atexit.register(callback[0], *callback[1], **callback[2])
207258

0 commit comments

Comments
 (0)