From af0b965b62dbae3afc760606982b0649ea2dbbf4 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 6 Nov 2023 19:03:09 +0000 Subject: [PATCH] gh-79932: deprecate clearing a suspended frame and add option to get exception. --- Doc/reference/datamodel.rst | 10 +++++++- Doc/whatsnew/3.13.rst | 3 +++ Lib/test/test_frame.py | 19 +++++++++++++-- ...3-11-06-16-44-09.gh-issue-79932.2qv7uD.rst | 1 + Objects/frameobject.c | 23 +++++++++++++++++-- 5 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-11-06-16-44-09.gh-issue-79932.2qv7uD.rst diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 9e9fe831f4a647..fea5e6ccd48e49 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -1206,7 +1206,7 @@ by writing to f_lineno. Frame objects support one method: -.. method:: frame.clear() +.. method:: frame.clear([raise_if_suspended]) This method clears all references to local variables held by the frame. Also, if the frame belonged to a generator, the generator @@ -1216,8 +1216,16 @@ Frame objects support one method: :exc:`RuntimeError` is raised if the frame is currently executing. + Clearing a suspended frame is deprecated. + The optional argument *raise_if_suspended* can be passed ``True`` to + make this function raise a :exc:`RuntimeError` instead of issuing a + deprecation warning if the frame is suspended. + .. versionadded:: 3.4 + .. versionchanged:: 3.13 + Clearing a suspended frame is deprecated. Added the *raise_if_suspended* + argument. .. _traceback-objects: diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index e12c2a1b0454fd..e137e5479fb6aa 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -397,6 +397,9 @@ Deprecated and methods that consider plural forms even if the translation was not found. (Contributed by Serhiy Storchaka in :gh:`88434`.) +* Calling :meth:`frame.clear` on a suspended frame is deprecated. + (Contributed by Irit Katriel in :gh:`79932`.) + Pending Removal in Python 3.14 ------------------------------ diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py index 9491c7facdf077..e6d4067727e0ac 100644 --- a/Lib/test/test_frame.py +++ b/Lib/test/test_frame.py @@ -6,6 +6,7 @@ import threading import types import unittest +import warnings import weakref try: import _testcapi @@ -80,8 +81,19 @@ def g(): gen = g() next(gen) self.assertFalse(endly) + + # Raise exception when attempting to clear a suspended frame + with self.assertRaisesRegex(RuntimeError, r'suspended frame'): + gen.gi_frame.clear(True) + self.assertFalse(endly) + # Clearing the frame closes the generator - gen.gi_frame.clear() + try: + with self.assertWarnsRegex(DeprecationWarning, r'suspended frame'): + gen.gi_frame.clear() + except DeprecationWarning: + # Suppress the warning when running with -We + pass self.assertTrue(endly) def test_clear_executing(self): @@ -115,7 +127,10 @@ def g(): f = next(gen) self.assertFalse(endly) # Clearing the frame closes the generator - f.clear() + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', category=DeprecationWarning) + f.clear() + self.assertTrue(endly) def test_lineno_with_tracing(self): diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-11-06-16-44-09.gh-issue-79932.2qv7uD.rst b/Misc/NEWS.d/next/Core and Builtins/2023-11-06-16-44-09.gh-issue-79932.2qv7uD.rst new file mode 100644 index 00000000000000..67071ee60b8a11 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-11-06-16-44-09.gh-issue-79932.2qv7uD.rst @@ -0,0 +1 @@ +Deprecate clearing a suspended frame. diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 3b72651a1c0f74..bd265ec2e07f23 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -933,13 +933,32 @@ frame_tp_clear(PyFrameObject *f) } static PyObject * -frame_clear(PyFrameObject *f, PyObject *Py_UNUSED(ignored)) +frame_clear(PyFrameObject *f, PyObject *args, PyObject *kwds) { + bool raise_if_suspended = false; + PyObject *v = NULL; + if (!PyArg_UnpackTuple(args, "clear", 0, 1, &v)) { + return NULL; + } + if (v != NULL && PyObject_IsTrue(v)) { + raise_if_suspended = true; + } + if (f->f_frame->owner == FRAME_OWNED_BY_GENERATOR) { PyGenObject *gen = _PyFrame_GetGenerator(f->f_frame); if (gen->gi_frame_state == FRAME_EXECUTING) { goto running; } + if (FRAME_STATE_SUSPENDED(gen->gi_frame_state)) { + if (raise_if_suspended) { + PyErr_SetString(PyExc_RuntimeError, "cannot clear a suspended frame"); + return NULL; + } + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "clearing a suspended frame is deprecated", 1) < 0) { + return NULL; + } + } _PyGen_Finalize((PyObject *)gen); } else if (f->f_frame->owner == FRAME_OWNED_BY_THREAD) { @@ -983,7 +1002,7 @@ frame_repr(PyFrameObject *f) } static PyMethodDef frame_methods[] = { - {"clear", (PyCFunction)frame_clear, METH_NOARGS, + {"clear", (PyCFunction)frame_clear, METH_VARARGS, clear__doc__}, {"__sizeof__", (PyCFunction)frame_sizeof, METH_NOARGS, sizeof__doc__},