Skip to content
Open
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 76 additions & 22 deletions src/sage/cpython/atexit.pyx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# sage_setup: distribution = sagemath-objects
# distutils: define_macros=Py_BUILD_CORE=1

"""Utilities for interfacing with the standard library's atexit module."""

Expand Down Expand Up @@ -144,51 +145,101 @@ cdef class restore_atexit:
_set_exithandlers(self._exithandlers)

from cpython.ref cimport PyObject
import sys

# Implement "_atexit_callbacks()" for each supported python version
# Implement a uniform interface for getting atexit callbacks
cdef extern from *:
"""
#ifndef Py_BUILD_CORE
#define Py_BUILD_CORE
#endif
#undef _PyGC_FINALIZED
#include "internal/pycore_interp.h"
#include "internal/pycore_pystate.h"

// Always define this struct for Cython's use
typedef struct {
PyObject *func;
PyObject *args;
PyObject *kwargs;
} atexit_callback_struct;

#if PY_VERSION_HEX >= 0x030e0000
// Python 3.14+: atexit uses a PyList
static PyObject* get_atexit_callbacks_list(PyObject *self) {
PyInterpreterState *interp = _PyInterpreterState_GET();
struct atexit_state state = interp->atexit;
return state.callbacks;
}

// Dummy function for Python 3.14+ (never called)
static atexit_callback_struct** get_atexit_callbacks_array(PyObject *self) {
return NULL;
}
#else
// Python < 3.14: atexit uses C array
#if PY_VERSION_HEX >= 0x030c0000
// struct atexit_callback was renamed in 3.12 to atexit_py_callback
#define atexit_callback atexit_py_callback
#endif
static atexit_callback ** _atexit_callbacks(PyObject *self) {

static atexit_callback_struct** get_atexit_callbacks_array(PyObject *self) {
PyInterpreterState *interp = _PyInterpreterState_GET();
struct atexit_state state = interp->atexit;
return state.callbacks;
// Cast from atexit_callback** to our struct type
return (atexit_callback_struct**)state.callbacks;
}

// Dummy function for Python < 3.14 (never called)
static PyObject* get_atexit_callbacks_list(PyObject *self) {
return NULL;
}
#endif
"""
ctypedef struct atexit_callback:
# Declare both functions - they exist in all Python versions (one is dummy)
PyObject* get_atexit_callbacks_list(object module)

ctypedef struct atexit_callback_struct:
PyObject* func
PyObject* args
PyObject* kwargs
atexit_callback** _atexit_callbacks(object module)
atexit_callback_struct** get_atexit_callbacks_array(object module)


def _get_exithandlers():
"""Return list of exit handlers registered with the atexit module."""
cdef atexit_callback ** callbacks
cdef atexit_callback callback
cdef list exithandlers
cdef list exithandlers = []
cdef atexit_callback_struct ** callbacks
cdef atexit_callback_struct callback
cdef int idx
cdef object kwargs

exithandlers = []
callbacks = _atexit_callbacks(atexit)

for idx in range(atexit._ncallbacks()):
callback = callbacks[idx][0]
if callback.kwargs:
kwargs = <object>callback.kwargs
else:
kwargs = {}
exithandlers.append((<object>callback.func,
<object>callback.args,
kwargs))

# Python 3.14+ uses a PyList directly
if sys.version_info >= (3, 14):
callbacks_list = <object>get_atexit_callbacks_list(atexit)
if callbacks_list is None:
return exithandlers
# callbacks is a list of tuples: [(func, args, kwargs), ...]
# Normalize kwargs to ensure it's always a dict (not None)
# Note: In Python 3.14+, atexit stores callbacks in LIFO order
# (most recently registered first), but we return them in FIFO
# order (registration order) for consistency with earlier versions
for item in reversed(callbacks_list):
func, args, kwargs = item
if kwargs is None:
kwargs = {}
exithandlers.append((func, args, kwargs))
else:
# Python < 3.14 uses C array
callbacks = get_atexit_callbacks_array(atexit)
for idx in range(atexit._ncallbacks()):
callback = callbacks[idx][0]
if callback.kwargs:
kwargs = <object>callback.kwargs
else:
kwargs = {}
exithandlers.append((<object>callback.func,
<object>callback.args,
kwargs))
return exithandlers


Expand All @@ -203,6 +254,9 @@ def _set_exithandlers(exithandlers):

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

Expand Down
Loading