diff --git a/Doc/c-api/frame.rst b/Doc/c-api/frame.rst index 1a52e146a69751..a252f3c5dd7627 100644 --- a/Doc/c-api/frame.rst +++ b/Doc/c-api/frame.rst @@ -140,6 +140,18 @@ See also :ref:`Reflection `. Return the line number that *frame* is currently executing. +.. c:function:: int PyFrame_SetLineNumber(PyFrameObject *frame, int lineno) + + Set a frame line number. + + *lineno* must be greater than or equal to ``1``. + + Do not check if *lineno* is valid for the frame code object. + Do not update the instruction pointer. + + .. versionadded:: next + + Frame Locals Proxies ^^^^^^^^^^^^^^^^^^^^ @@ -191,5 +203,3 @@ Unless using :pep:`523`, you will not need this. Return the currently executing line number, or -1 if there is no line number. .. versionadded:: 3.12 - - diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 65ab57eb821c6c..530fbd7aa42525 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1590,6 +1590,9 @@ New features take a C integer and produce a Python :class:`bool` object. (Contributed by Pablo Galindo in :issue:`45325`.) +* Add :c:func:`PyFrame_SetLineNumber` function to set a frame line number. + (Contributed by Victor Stinner in :gh:`118720`.) + Limited C API changes --------------------- diff --git a/Include/cpython/pyframe.h b/Include/cpython/pyframe.h index 51529763923ec3..98dd51b133023c 100644 --- a/Include/cpython/pyframe.h +++ b/Include/cpython/pyframe.h @@ -18,6 +18,7 @@ PyAPI_FUNC(PyObject *) PyFrame_GetGenerator(PyFrameObject *frame); PyAPI_FUNC(int) PyFrame_GetLasti(PyFrameObject *frame); PyAPI_FUNC(PyObject*) PyFrame_GetVar(PyFrameObject *frame, PyObject *name); PyAPI_FUNC(PyObject*) PyFrame_GetVarString(PyFrameObject *frame, const char *name); +PyAPI_FUNC(int) PyFrame_SetLineNumber(PyFrameObject *frame, int lineno); /* The following functions are for use by debuggers and other tools * implementing custom frame evaluators with PEP 523. */ diff --git a/Lib/test/test_capi/test_frame.py b/Lib/test/test_capi/test_frame.py index 23cb8e3dada9d4..acfb989cd5b865 100644 --- a/Lib/test/test_capi/test_frame.py +++ b/Lib/test/test_capi/test_frame.py @@ -4,6 +4,7 @@ _testcapi = import_helper.import_module('_testcapi') +_testlimitedcapi = import_helper.import_module('_testlimitedcapi') class FrameTest(unittest.TestCase): @@ -51,6 +52,22 @@ def dummy(): # The following line should not cause a segmentation fault. self.assertIsNone(frame.f_back) + def test_lineno(self): + # Test PyFrame_GetLineNumber() and PyFrame_SetLineNumber() + frame_getlinenumber = _testlimitedcapi.frame_getlinenumber + frame_setlinenumber = _testcapi.frame_setlinenumber + + frame = sys._getframe() + frame_setlinenumber(frame, 123) + self.assertEqual(frame_getlinenumber(frame), 123) + frame_setlinenumber(frame, 222) + self.assertEqual(frame_getlinenumber(frame), 222) + + for invalid in (-10, -1, 0): + with self.subTest(invalid): + with self.assertRaises(ValueError): + frame_setlinenumber(frame, invalid) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C_API/2025-03-14-15-02-34.gh-issue-118720.0q9bO7.rst b/Misc/NEWS.d/next/C_API/2025-03-14-15-02-34.gh-issue-118720.0q9bO7.rst new file mode 100644 index 00000000000000..7d0ef94579f6f3 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-03-14-15-02-34.gh-issue-118720.0q9bO7.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyFrame_SetLineNumber` function to set a frame line number. +Patch by Victor Stinner. diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 6bb05a06a3465d..8b51ba12aa61ed 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -163,7 +163,7 @@ @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c @MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c @MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c -@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c +@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/frame.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c @MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c diff --git a/Modules/_testcapi/frame.c b/Modules/_testcapi/frame.c index 5748dca948ea94..8546b479d0ff53 100644 --- a/Modules/_testcapi/frame.c +++ b/Modules/_testcapi/frame.c @@ -114,6 +114,26 @@ frame_getvarstring(PyObject *self, PyObject *args) } +static PyObject * +frame_setlinenumber(PyObject *self, PyObject *args) +{ + PyObject *frame; + int lineno; + if (!PyArg_ParseTuple(args, "Oi", &frame, &lineno)) { + return NULL; + } + if (!PyFrame_Check(frame)) { + PyErr_SetString(PyExc_TypeError, "argument must be a frame"); + return NULL; + } + + if (PyFrame_SetLineNumber((PyFrameObject*)frame, lineno) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + + static PyMethodDef test_methods[] = { {"frame_getlocals", frame_getlocals, METH_O, NULL}, {"frame_getglobals", frame_getglobals, METH_O, NULL}, @@ -123,6 +143,7 @@ static PyMethodDef test_methods[] = { {"frame_new", frame_new, METH_VARARGS, NULL}, {"frame_getvar", frame_getvar, METH_VARARGS, NULL}, {"frame_getvarstring", frame_getvarstring, METH_VARARGS, NULL}, + {"frame_setlinenumber", frame_setlinenumber, METH_VARARGS, NULL}, {NULL}, }; @@ -131,4 +152,3 @@ _PyTestCapi_Init_Frame(PyObject *m) { return PyModule_AddFunctions(m, test_methods); } - diff --git a/Modules/_testlimitedcapi.c b/Modules/_testlimitedcapi.c index 4dae99ec92a085..e897d32fe2ab48 100644 --- a/Modules/_testlimitedcapi.c +++ b/Modules/_testlimitedcapi.c @@ -92,5 +92,8 @@ PyInit__testlimitedcapi(void) if (_PyTestLimitedCAPI_Init_File(mod) < 0) { return NULL; } + if (_PyTestLimitedCAPI_Init_Frame(mod) < 0) { + return NULL; + } return mod; } diff --git a/Modules/_testlimitedcapi/frame.c b/Modules/_testlimitedcapi/frame.c new file mode 100644 index 00000000000000..aff235be9051cb --- /dev/null +++ b/Modules/_testlimitedcapi/frame.c @@ -0,0 +1,22 @@ +#include "parts.h" +#include "util.h" + + +static PyObject * +frame_getlinenumber(PyObject *self, PyObject *frame) +{ + int lineno = PyFrame_GetLineNumber((PyFrameObject*)frame); + return PyLong_FromLong(lineno); +} + + +static PyMethodDef test_methods[] = { + {"frame_getlinenumber", frame_getlinenumber, METH_O, NULL}, + {NULL}, +}; + +int +_PyTestLimitedCAPI_Init_Frame(PyObject *m) +{ + return PyModule_AddFunctions(m, test_methods); +} diff --git a/Modules/_testlimitedcapi/parts.h b/Modules/_testlimitedcapi/parts.h index 60f6f03011a65c..6dc3d60372ecb7 100644 --- a/Modules/_testlimitedcapi/parts.h +++ b/Modules/_testlimitedcapi/parts.h @@ -43,5 +43,6 @@ int _PyTestLimitedCAPI_Init_Unicode(PyObject *module); int _PyTestLimitedCAPI_Init_VectorcallLimited(PyObject *module); int _PyTestLimitedCAPI_Init_Version(PyObject *module); int _PyTestLimitedCAPI_Init_File(PyObject *module); +int _PyTestLimitedCAPI_Init_Frame(PyObject *module); #endif // Py_TESTLIMITEDCAPI_PARTS_H diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 001b58dc052416..9b2a2e7ce59b33 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -971,6 +971,21 @@ frame_getlineno(PyObject *op, void *Py_UNUSED(closure)) } } + +int +PyFrame_SetLineNumber(PyFrameObject *frame, int lineno) +{ + if (lineno < 1) { + PyErr_SetString(PyExc_ValueError, + "lineno must be greater than or equal to 1"); + return -1; + } + + frame->f_lineno = lineno; + return 0; +} + + static PyObject * frame_getlasti(PyObject *op, void *Py_UNUSED(closure)) { diff --git a/PCbuild/_testlimitedcapi.vcxproj b/PCbuild/_testlimitedcapi.vcxproj index 36c41fc9824fda..4ee6a45efc6412 100644 --- a/PCbuild/_testlimitedcapi.vcxproj +++ b/PCbuild/_testlimitedcapi.vcxproj @@ -115,6 +115,7 @@ + diff --git a/PCbuild/_testlimitedcapi.vcxproj.filters b/PCbuild/_testlimitedcapi.vcxproj.filters index 62ecb2f70ffa2d..1f2dae439733ed 100644 --- a/PCbuild/_testlimitedcapi.vcxproj.filters +++ b/PCbuild/_testlimitedcapi.vcxproj.filters @@ -31,6 +31,7 @@ +