From 0e322e49be64d56c39f4c383117ad62879645aa5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 17 Nov 2025 16:00:35 +0100 Subject: [PATCH 1/7] gh-141518: Add PyUnstable_InterpreterState_SetEvalFrameFunc() Add PyUnstable API for PEP 523: * PyUnstable_FrameEvalFunction type * PyUnstable_InterpreterState_GetEvalFrameFunc() * PyUnstable_InterpreterState_SetEvalFrameFunc() Keep the old names as deprecated aliases to new names: * _PyFrameEvalFunction * _PyInterpreterState_GetEvalFrameFunc * _PyInterpreterState_SetEvalFrameFunc --- Doc/c-api/init.rst | 10 +++++----- Doc/whatsnew/3.15.rst | 10 ++++++++++ Include/cpython/pystate.h | 13 +++++++++---- Include/internal/pycore_interp_structs.h | 4 ++-- .../2025-11-17-16-08-11.gh-issue-141518.Z8ig9U.rst | 7 +++++++ Modules/_testinternalcapi.c | 6 ++++-- Python/perf_trampoline.c | 8 ++++---- Python/pystate.c | 8 ++++---- 8 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2025-11-17-16-08-11.gh-issue-141518.Z8ig9U.rst diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index 18ee16118070eb..ac514ac3cad96d 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -1438,7 +1438,7 @@ All of the following functions must be called after :c:func:`Py_Initialize`. .. versionadded:: 3.8 -.. c:type:: PyObject* (*_PyFrameEvalFunction)(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag) +.. c:type:: PyObject* (*PyUnstable_FrameEvalFunction)(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag) Type of a frame evaluation function. @@ -1451,21 +1451,21 @@ All of the following functions must be called after :c:func:`Py_Initialize`. .. versionchanged:: 3.11 The *frame* parameter changed from ``PyFrameObject*`` to ``_PyInterpreterFrame*``. -.. c:function:: _PyFrameEvalFunction _PyInterpreterState_GetEvalFrameFunc(PyInterpreterState *interp) +.. c:function:: PyUnstable_FrameEvalFunction PyUnstable_InterpreterState_GetEvalFrameFunc(PyInterpreterState *interp) Get the frame evaluation function. See the :pep:`523` "Adding a frame evaluation API to CPython". - .. versionadded:: 3.9 + .. versionadded:: next -.. c:function:: void _PyInterpreterState_SetEvalFrameFunc(PyInterpreterState *interp, _PyFrameEvalFunction eval_frame) +.. c:function:: void PyUnstable_InterpreterState_SetEvalFrameFunc(PyInterpreterState *interp, PyUnstable_FrameEvalFunction eval_frame) Set the frame evaluation function. See the :pep:`523` "Adding a frame evaluation API to CPython". - .. versionadded:: 3.9 + .. versionadded:: next .. c:function:: PyObject* PyThreadState_GetDict() diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index cf5bef15203b23..81c968f8cfdc5d 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1087,6 +1087,16 @@ New features * Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array. (Contributed by Victor Stinner in :gh:`111489`.) +* Add a PyUnstable API for :pep:`523`: + + * :c:type:`PyUnstable_FrameEvalFunction` type + * :c:func:`PyUnstable_InterpreterState_GetEvalFrameFunc` + * :c:func:`PyUnstable_InterpreterState_SetEvalFrameFunc` + + (Contributed by Victor Stinner in :gh:`141518`.) + +Patch by Victor Stinner. + * Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and :c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set the stack protection base address and stack protection size of a Python diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 1e1e46ea4c0bcd..f22760d7c21cc8 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -302,10 +302,15 @@ PyAPI_FUNC(void) PyThreadState_DeleteCurrent(void); /* Frame evaluation API */ -typedef PyObject* (*_PyFrameEvalFunction)(PyThreadState *tstate, struct _PyInterpreterFrame *, int); +typedef PyObject* (*PyUnstable_FrameEvalFunction)(PyThreadState *tstate, struct _PyInterpreterFrame *, int); -PyAPI_FUNC(_PyFrameEvalFunction) _PyInterpreterState_GetEvalFrameFunc( +PyAPI_FUNC(PyUnstable_FrameEvalFunction) PyUnstable_InterpreterState_GetEvalFrameFunc( PyInterpreterState *interp); -PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameFunc( +PyAPI_FUNC(void) PyUnstable_InterpreterState_SetEvalFrameFunc( PyInterpreterState *interp, - _PyFrameEvalFunction eval_frame); + PyUnstable_FrameEvalFunction eval_frame); + +// Deprecated aliases kept for backward compatibility +#define _PyFrameEvalFunction PyUnstable_FrameEvalFunction +#define _PyInterpreterState_GetEvalFrameFunc PyUnstable_InterpreterState_GetEvalFrameFunc +#define _PyInterpreterState_SetEvalFrameFunc PyUnstable_InterpreterState_SetEvalFrameFunc diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index f861d3abd96d48..04a77966fecc89 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -87,7 +87,7 @@ struct _ceval_runtime_state { struct trampoline_api_st trampoline_api; FILE *map_file; Py_ssize_t persist_after_fork; - _PyFrameEvalFunction prev_eval_frame; + PyUnstable_FrameEvalFunction prev_eval_frame; #else int _not_used; #endif @@ -860,7 +860,7 @@ struct _is { PyObject *sysdict_copy; PyObject *builtins_copy; // Initialized to _PyEval_EvalFrameDefault(). - _PyFrameEvalFunction eval_frame; + PyUnstable_FrameEvalFunction eval_frame; PyFunction_WatchCallback func_watchers[FUNC_MAX_WATCHERS]; // One bit is set for each non-NULL entry in func_watchers diff --git a/Misc/NEWS.d/next/C_API/2025-11-17-16-08-11.gh-issue-141518.Z8ig9U.rst b/Misc/NEWS.d/next/C_API/2025-11-17-16-08-11.gh-issue-141518.Z8ig9U.rst new file mode 100644 index 00000000000000..c53c3252e8648c --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-11-17-16-08-11.gh-issue-141518.Z8ig9U.rst @@ -0,0 +1,7 @@ +Add a PyUnstable API for :pep:`523`: + +* :c:type:`PyUnstable_FrameEvalFunction` type +* :c:func:`PyUnstable_InterpreterState_GetEvalFrameFunc` +* :c:func:`PyUnstable_InterpreterState_SetEvalFrameFunc` + +Patch by Victor Stinner. diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 89e558b0fe8933..4f6abb372585b6 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -657,7 +657,8 @@ static PyObject * set_eval_frame_default(PyObject *self, PyObject *Py_UNUSED(args)) { module_state *state = get_module_state(self); - _PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), _PyEval_EvalFrameDefault); + PyUnstable_InterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), + _PyEval_EvalFrameDefault); Py_CLEAR(state->record_list); Py_RETURN_NONE; } @@ -689,7 +690,8 @@ set_eval_frame_record(PyObject *self, PyObject *list) return NULL; } Py_XSETREF(state->record_list, Py_NewRef(list)); - _PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), record_eval); + PyUnstable_InterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), + record_eval); Py_RETURN_NONE; } diff --git a/Python/perf_trampoline.c b/Python/perf_trampoline.c index 987e8d2a11a659..003905b4439ff2 100644 --- a/Python/perf_trampoline.c +++ b/Python/perf_trampoline.c @@ -486,12 +486,12 @@ _PyPerfTrampoline_Init(int activate) #ifdef PY_HAVE_PERF_TRAMPOLINE PyThreadState *tstate = _PyThreadState_GET(); if (!activate) { - _PyInterpreterState_SetEvalFrameFunc(tstate->interp, prev_eval_frame); + PyUnstable_InterpreterState_SetEvalFrameFunc(tstate->interp, prev_eval_frame); perf_status = PERF_STATUS_NO_INIT; } else if (tstate->interp->eval_frame != py_trampoline_evaluator) { - prev_eval_frame = _PyInterpreterState_GetEvalFrameFunc(tstate->interp); - _PyInterpreterState_SetEvalFrameFunc(tstate->interp, py_trampoline_evaluator); + prev_eval_frame = PyUnstable_InterpreterState_GetEvalFrameFunc(tstate->interp); + PyUnstable_InterpreterState_SetEvalFrameFunc(tstate->interp, py_trampoline_evaluator); extra_code_index = _PyEval_RequestCodeExtraIndex(NULL); if (extra_code_index == -1) { return -1; @@ -517,7 +517,7 @@ _PyPerfTrampoline_Fini(void) } PyThreadState *tstate = _PyThreadState_GET(); if (tstate->interp->eval_frame == py_trampoline_evaluator) { - _PyInterpreterState_SetEvalFrameFunc(tstate->interp, NULL); + PyUnstable_InterpreterState_SetEvalFrameFunc(tstate->interp, NULL); } if (perf_status == PERF_STATUS_OK) { trampoline_api.free_state(trampoline_api.state); diff --git a/Python/pystate.c b/Python/pystate.c index c12a1418e74309..2e3c40d2be8957 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -2896,8 +2896,8 @@ PyGILState_Release(PyGILState_STATE oldstate) /* Other API */ /*************/ -_PyFrameEvalFunction -_PyInterpreterState_GetEvalFrameFunc(PyInterpreterState *interp) +PyUnstable_FrameEvalFunction +PyUnstable_InterpreterState_GetEvalFrameFunc(PyInterpreterState *interp) { if (interp->eval_frame == NULL) { return _PyEval_EvalFrameDefault; @@ -2907,8 +2907,8 @@ _PyInterpreterState_GetEvalFrameFunc(PyInterpreterState *interp) void -_PyInterpreterState_SetEvalFrameFunc(PyInterpreterState *interp, - _PyFrameEvalFunction eval_frame) +PyUnstable_InterpreterState_SetEvalFrameFunc(PyInterpreterState *interp, + PyUnstable_FrameEvalFunction eval_frame) { if (eval_frame == _PyEval_EvalFrameDefault) { eval_frame = NULL; From 757eada4d4e4940808a888fd6a2623caa2262555 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 17 Nov 2025 16:13:49 +0100 Subject: [PATCH 2/7] Deprecate the old C API --- .../c-api-pending-removal-in-3.20.rst | 7 +++++++ Doc/whatsnew/3.11.rst | 2 +- Doc/whatsnew/3.15.rst | 7 +++++++ Include/cpython/pystate.h | 15 +++++++++++++-- Misc/NEWS.d/3.9.0a5.rst | 6 +++--- 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/Doc/deprecations/c-api-pending-removal-in-3.20.rst b/Doc/deprecations/c-api-pending-removal-in-3.20.rst index 18623b19a2ab8d..076ef76108b722 100644 --- a/Doc/deprecations/c-api-pending-removal-in-3.20.rst +++ b/Doc/deprecations/c-api-pending-removal-in-3.20.rst @@ -7,3 +7,10 @@ Pending removal in Python 3.20 representation. * Macros :c:macro:`!Py_MATH_PIl` and :c:macro:`!Py_MATH_El`. + +* :c:func:`!_PyInterpreterState_GetEvalFrameFunc` and + :c:func:`!_PyInterpreterState_SetEvalFrameFunc` functions are deprecated + and will be removed in Python 3.20. Use + :c:func:`PyUnstable_InterpreterState_GetEvalFrameFunc` and + :c:func:`PyUnstable_InterpreterState_SetEvalFrameFunc` instead. + (Contributed by Victor Stinner in :gh:`141518`.) diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index a095d887352127..14e998aae26f28 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -2305,7 +2305,7 @@ Porting to Python 3.11 be used for ``size``. (Contributed by Kumar Aditya in :issue:`46608`.) -* :c:func:`_PyFrameEvalFunction` now takes ``_PyInterpreterFrame*`` +* :c:func:`!_PyFrameEvalFunction` now takes ``_PyInterpreterFrame*`` as its second parameter, instead of ``PyFrameObject*``. See :pep:`523` for more details of how to use this function pointer type. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 81c968f8cfdc5d..aee73938a5d0cf 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1233,6 +1233,13 @@ Deprecated C APIs since 3.15 and will be removed in 3.20. (Contributed by Sergey B Kirpichev in :gh:`141004`.) +* :c:func:`!_PyInterpreterState_GetEvalFrameFunc` and + :c:func:`!_PyInterpreterState_SetEvalFrameFunc` functions are deprecated + and will be removed in Python 3.20. Use + :c:func:`PyUnstable_InterpreterState_GetEvalFrameFunc` and + :c:func:`PyUnstable_InterpreterState_SetEvalFrameFunc` instead. + (Contributed by Victor Stinner in :gh:`141518`.) + .. Add C API deprecations above alphabetically, not here at the end. diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index f22760d7c21cc8..7e3f2e02141f60 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -312,5 +312,16 @@ PyAPI_FUNC(void) PyUnstable_InterpreterState_SetEvalFrameFunc( // Deprecated aliases kept for backward compatibility #define _PyFrameEvalFunction PyUnstable_FrameEvalFunction -#define _PyInterpreterState_GetEvalFrameFunc PyUnstable_InterpreterState_GetEvalFrameFunc -#define _PyInterpreterState_SetEvalFrameFunc PyUnstable_InterpreterState_SetEvalFrameFunc + +Py_DEPRECATED(3.15) static inline PyUnstable_FrameEvalFunction +_PyInterpreterState_GetEvalFrameFunc(PyInterpreterState *interp) +{ + return PyUnstable_InterpreterState_GetEvalFrameFunc(interp); +} + +Py_DEPRECATED(3.15) static inline void +_PyInterpreterState_SetEvalFrameFunc(PyInterpreterState *interp, + PyUnstable_FrameEvalFunction eval_frame) +{ + PyUnstable_InterpreterState_SetEvalFrameFunc(interp, eval_frame); +} diff --git a/Misc/NEWS.d/3.9.0a5.rst b/Misc/NEWS.d/3.9.0a5.rst index 9402e5077c2e77..f22a87ba356efd 100644 --- a/Misc/NEWS.d/3.9.0a5.rst +++ b/Misc/NEWS.d/3.9.0a5.rst @@ -1305,6 +1305,6 @@ and undefined PY_SSIZE_T_CLEAN whwn an exception is set. .. section: C API Add a private API to get and set the frame evaluation function: add -:c:func:`_PyInterpreterState_GetEvalFrameFunc` and -:c:func:`_PyInterpreterState_SetEvalFrameFunc` C functions. The -:c:type:`_PyFrameEvalFunction` function type now takes a *tstate* parameter. +:c:func:`!_PyInterpreterState_GetEvalFrameFunc` and +:c:func:`!_PyInterpreterState_SetEvalFrameFunc` C functions. The +:c:type:`!_PyFrameEvalFunction` function type now takes a *tstate* parameter. From 4ceff72af2c994d8964bd79ff80122c1ac68c2d4 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 17 Nov 2025 23:22:33 +0100 Subject: [PATCH 3/7] Fix PyUnstable_FrameEvalFunction doc --- Doc/c-api/init.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index ac514ac3cad96d..1e3333fd5fac40 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -1445,11 +1445,7 @@ All of the following functions must be called after :c:func:`Py_Initialize`. The *throwflag* parameter is used by the ``throw()`` method of generators: if non-zero, handle the current exception. - .. versionchanged:: 3.9 - The function now takes a *tstate* parameter. - - .. versionchanged:: 3.11 - The *frame* parameter changed from ``PyFrameObject*`` to ``_PyInterpreterFrame*``. + .. versionadded:: next .. c:function:: PyUnstable_FrameEvalFunction PyUnstable_InterpreterState_GetEvalFrameFunc(PyInterpreterState *interp) From 0a8d7cdb4de88396b0bf768b36d107c5e34c107f Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 17 Nov 2025 23:31:52 +0100 Subject: [PATCH 4/7] Add tests --- Modules/_testcapimodule.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index c14f925b4e7632..dc7e57dcfc7303 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2595,6 +2595,29 @@ create_managed_weakref_nogc_type(PyObject *self, PyObject *Py_UNUSED(args)) } +static PyObject * +noop_eval(PyThreadState *tstate, struct _PyInterpreterFrame *f, int exc) +{ + return NULL; +} + +static PyObject * +test_interpreter_setevalframefunc(PyObject *self, PyObject *Py_UNUSED(args)) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + PyUnstable_FrameEvalFunction eval_func; + + eval_func = PyUnstable_InterpreterState_GetEvalFrameFunc(interp); + + PyUnstable_InterpreterState_SetEvalFrameFunc(interp, noop_eval); + assert(PyUnstable_InterpreterState_GetEvalFrameFunc(interp) == noop_eval); + + PyUnstable_InterpreterState_SetEvalFrameFunc(interp, eval_func); + + Py_RETURN_NONE; +} + + static PyMethodDef TestMethods[] = { {"set_errno", set_errno, METH_VARARGS}, {"test_config", test_config, METH_NOARGS}, @@ -2691,6 +2714,8 @@ static PyMethodDef TestMethods[] = { {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, {"create_managed_weakref_nogc_type", create_managed_weakref_nogc_type, METH_NOARGS}, + {"test_interpreter_setevalframefunc", + test_interpreter_setevalframefunc, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; From f69454279c494aad0ef7513cd5451cf4ffb126bf Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 18 Nov 2025 13:38:45 +0100 Subject: [PATCH 5/7] Update Modules/_testcapimodule.c Co-authored-by: Mikhail Efimov --- Modules/_testcapimodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index dc7e57dcfc7303..353cbd8097623e 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2604,7 +2604,7 @@ noop_eval(PyThreadState *tstate, struct _PyInterpreterFrame *f, int exc) static PyObject * test_interpreter_setevalframefunc(PyObject *self, PyObject *Py_UNUSED(args)) { - PyInterpreterState *interp = PyInterpreterState_Get(); + PyInterpreterState *interp = PyInterpreterState_Get(); PyUnstable_FrameEvalFunction eval_func; eval_func = PyUnstable_InterpreterState_GetEvalFrameFunc(interp); From 3e48d9129a66ce0b9e1b135038988a8169423984 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 19 Nov 2025 11:35:15 +0100 Subject: [PATCH 6/7] Fix copy/paste mistake --- Doc/whatsnew/3.15.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index aee73938a5d0cf..1a50387c4c15f5 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1095,8 +1095,6 @@ New features (Contributed by Victor Stinner in :gh:`141518`.) -Patch by Victor Stinner. - * Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and :c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set the stack protection base address and stack protection size of a Python From c624f744548d0d0f2ffcb841adf5e02dae56adfa Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 19 Nov 2025 14:31:06 +0100 Subject: [PATCH 7/7] _PyFrameEvalFunction changelog --- Doc/c-api/init.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index 5b6396e889ec86..990bba3a9139c8 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -1445,7 +1445,18 @@ All of the following functions must be called after :c:func:`Py_Initialize`. The *throwflag* parameter is used by the ``throw()`` method of generators: if non-zero, handle the current exception. - .. versionadded:: next + .. versionadded:: 3.6 + as ``_PyFrameEvalFunction``. + + .. versionchanged:: 3.9 + The function now takes a *tstate* parameter. + + .. versionchanged:: 3.11 + The *frame* parameter changed from ``PyFrameObject*`` to ``_PyInterpreterFrame*``. + + .. versionchanged:: next + Renamed to :c:type:`!PyUnstable_FrameEvalFunction`. The old private name + is deprecated, but will be available until the API changes. .. c:function:: PyUnstable_FrameEvalFunction PyUnstable_InterpreterState_GetEvalFrameFunc(PyInterpreterState *interp)