From af0b965b62dbae3afc760606982b0649ea2dbbf4 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 6 Nov 2023 19:03:09 +0000 Subject: [PATCH 1/7] 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__}, From 0e570d069f7d87b56f67fce27019a8a19dbb0768 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 6 Nov 2023 19:32:28 +0000 Subject: [PATCH 2/7] update traceback module to use the new arg --- Lib/test/test_traceback.py | 19 +++++++++++++++++++ Lib/traceback.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index b43dca6f640b9a..1e4fec8cf2a718 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -2518,6 +2518,25 @@ def inner(): # Local variable dict should now be empty. self.assertEqual(len(inner_frame.f_locals), 0) + def test_do_not_clear_frame_of_suspended_generator(self): + # See gh-79932 + + def f(): + try: + raise TypeError + except Exception as e: + yield e + yield 42 + + def g(): + yield from f() + + gen = g() + e = next(gen) + self.assertIsInstance(e, TypeError) + traceback.clear_frames(e.__traceback__) + self.assertEqual(next(gen), 42) + def test_extract_stack(self): def extract(): return traceback.extract_stack() diff --git a/Lib/traceback.py b/Lib/traceback.py index b25a7291f6be51..69291eec91faa7 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -251,7 +251,7 @@ def clear_frames(tb): "Clear all references to local variables in the frames of a traceback." while tb is not None: try: - tb.tb_frame.clear() + tb.tb_frame.clear(True) except RuntimeError: # Ignore the exception raised if the frame is still executing. pass From d23dfb643b80b816dd06ccd0be74a268d913a474 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 6 Nov 2023 19:36:19 +0000 Subject: [PATCH 3/7] update docs for traceback --- Doc/library/traceback.rst | 3 +++ Doc/whatsnew/3.13.rst | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 408da7fc5f0645..ce830ed8a520c0 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -202,6 +202,9 @@ The module defines the following functions: .. versionadded:: 3.4 + .. versionchanged:: 3.13 + Suspended frames are no cleared. + .. function:: walk_stack(f) Walk a stack following ``f.f_back`` from the given frame, yielding the frame diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index e137e5479fb6aa..bd2ccf7acb729b 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -277,6 +277,9 @@ traceback to format the nested exceptions of a :exc:`BaseExceptionGroup` instance, recursively. (Contributed by Irit Katriel in :gh:`105292`.) +* :meth:`traceback.clear_frames` no longer clears suspended frames. + (Contributed by Irit Katriel in :gh:`111792`.) + typing ------ From 765e013796f73a0c7f5a12fb15656ffd3f81a879 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 6 Nov 2023 21:55:49 +0000 Subject: [PATCH 4/7] Revert "update docs for traceback" This reverts commit d23dfb643b80b816dd06ccd0be74a268d913a474. --- Doc/library/traceback.rst | 3 --- Doc/whatsnew/3.13.rst | 3 --- 2 files changed, 6 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index ce830ed8a520c0..408da7fc5f0645 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -202,9 +202,6 @@ The module defines the following functions: .. versionadded:: 3.4 - .. versionchanged:: 3.13 - Suspended frames are no cleared. - .. function:: walk_stack(f) Walk a stack following ``f.f_back`` from the given frame, yielding the frame diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index bd2ccf7acb729b..e137e5479fb6aa 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -277,9 +277,6 @@ traceback to format the nested exceptions of a :exc:`BaseExceptionGroup` instance, recursively. (Contributed by Irit Katriel in :gh:`105292`.) -* :meth:`traceback.clear_frames` no longer clears suspended frames. - (Contributed by Irit Katriel in :gh:`111792`.) - typing ------ From 07b480a84a497ccf5c11d0840accf5fc4bb200fb Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 6 Nov 2023 21:56:02 +0000 Subject: [PATCH 5/7] Revert "update traceback module to use the new arg" This reverts commit 0e570d069f7d87b56f67fce27019a8a19dbb0768. --- Lib/test/test_traceback.py | 19 ------------------- Lib/traceback.py | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 1e4fec8cf2a718..b43dca6f640b9a 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -2518,25 +2518,6 @@ def inner(): # Local variable dict should now be empty. self.assertEqual(len(inner_frame.f_locals), 0) - def test_do_not_clear_frame_of_suspended_generator(self): - # See gh-79932 - - def f(): - try: - raise TypeError - except Exception as e: - yield e - yield 42 - - def g(): - yield from f() - - gen = g() - e = next(gen) - self.assertIsInstance(e, TypeError) - traceback.clear_frames(e.__traceback__) - self.assertEqual(next(gen), 42) - def test_extract_stack(self): def extract(): return traceback.extract_stack() diff --git a/Lib/traceback.py b/Lib/traceback.py index 69291eec91faa7..b25a7291f6be51 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -251,7 +251,7 @@ def clear_frames(tb): "Clear all references to local variables in the frames of a traceback." while tb is not None: try: - tb.tb_frame.clear(True) + tb.tb_frame.clear() except RuntimeError: # Ignore the exception raised if the frame is still executing. pass From 4196b0a1fe0d21254e7f67e27aad5ca5f35af2fe Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 6 Nov 2023 22:48:49 +0000 Subject: [PATCH 6/7] revert deprecation, treat as bugfix --- Doc/reference/datamodel.rst | 14 ++++------ Doc/whatsnew/3.13.rst | 3 ++- Lib/test/test_frame.py | 23 ++++++---------- ...3-11-06-16-44-09.gh-issue-79932.2qv7uD.rst | 2 +- Objects/frameobject.c | 26 +++++-------------- 5 files changed, 23 insertions(+), 45 deletions(-) diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index fea5e6ccd48e49..67c5c2e38d0df9 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([raise_if_suspended]) +.. method:: frame.clear() This method clears all references to local variables held by the frame. Also, if the frame belonged to a generator, the generator @@ -1214,18 +1214,14 @@ Frame objects support one method: objects (for example when catching an exception and storing its traceback for later use). - :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. + :exc:`RuntimeError` is raised if the frame is currently executing + or suspended. .. versionadded:: 3.4 .. versionchanged:: 3.13 - Clearing a suspended frame is deprecated. Added the *raise_if_suspended* - argument. + Attempting to clear a suspended frame raises :exc:`RuntimeError`. + .. _traceback-objects: diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index e137e5479fb6aa..0d1de78d07abbb 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -397,7 +397,8 @@ 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. +* Calling :meth:`frame.clear` on a suspended frame raises :exc:`RuntimeError`, + as has always been the case for an executing frame. (Contributed by Irit Katriel in :gh:`79932`.) diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py index e6d4067727e0ac..32ea619ec018c7 100644 --- a/Lib/test/test_frame.py +++ b/Lib/test/test_frame.py @@ -6,7 +6,6 @@ import threading import types import unittest -import warnings import weakref try: import _testcapi @@ -84,17 +83,13 @@ def g(): # Raise exception when attempting to clear a suspended frame with self.assertRaisesRegex(RuntimeError, r'suspended frame'): - gen.gi_frame.clear(True) + gen.gi_frame.clear() self.assertFalse(endly) - # Clearing the frame closes the generator - 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) + # Cannot clear a suspended frame + with self.assertRaisesRegex(RuntimeError, r'suspended frame'): + gen.gi_frame.clear() + self.assertFalse(endly) def test_clear_executing(self): # Attempting to clear an executing frame is forbidden. @@ -126,12 +121,10 @@ def g(): gen = g() f = next(gen) self.assertFalse(endly) - # Clearing the frame closes the generator - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', category=DeprecationWarning) + # Cannot clear a suspended frame + with self.assertRaisesRegex(RuntimeError, 'suspended frame'): f.clear() - - self.assertTrue(endly) + self.assertFalse(endly) def test_lineno_with_tracing(self): def record_line(): 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 index 67071ee60b8a11..543dbe4413027a 100644 --- 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 @@ -1 +1 @@ -Deprecate clearing a suspended frame. +Raise exception if :meth:`frame.clear` is called on a suspended frame. diff --git a/Objects/frameobject.c b/Objects/frameobject.c index bd265ec2e07f23..fc82ba0f0f566c 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -933,31 +933,15 @@ frame_tp_clear(PyFrameObject *f) } static PyObject * -frame_clear(PyFrameObject *f, PyObject *args, PyObject *kwds) +frame_clear(PyFrameObject *f, PyObject *Py_UNUSED(ignored)) { - 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; - } + goto suspended; } _PyGen_Finalize((PyObject *)gen); } @@ -973,6 +957,10 @@ frame_clear(PyFrameObject *f, PyObject *args, PyObject *kwds) PyErr_SetString(PyExc_RuntimeError, "cannot clear an executing frame"); return NULL; +suspended: + PyErr_SetString(PyExc_RuntimeError, + "cannot clear a suspended frame"); + return NULL; } PyDoc_STRVAR(clear__doc__, @@ -1002,7 +990,7 @@ frame_repr(PyFrameObject *f) } static PyMethodDef frame_methods[] = { - {"clear", (PyCFunction)frame_clear, METH_VARARGS, + {"clear", (PyCFunction)frame_clear, METH_NOARGS, clear__doc__}, {"__sizeof__", (PyCFunction)frame_sizeof, METH_NOARGS, sizeof__doc__}, From 82c6341a043a807dff6ad03ef938572336185d27 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 6 Nov 2023 22:56:13 +0000 Subject: [PATCH 7/7] tweak docs, remove repetition in test --- Doc/reference/datamodel.rst | 3 ++- Doc/whatsnew/3.13.rst | 4 ++-- Lib/test/test_frame.py | 5 ----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 67c5c2e38d0df9..f7d3d2d0bbec23 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -1220,7 +1220,8 @@ Frame objects support one method: .. versionadded:: 3.4 .. versionchanged:: 3.13 - Attempting to clear a suspended frame raises :exc:`RuntimeError`. + Attempting to clear a suspended frame raises :exc:`RuntimeError` + (as has always been the case for executing frames). .. _traceback-objects: diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 0d1de78d07abbb..ef83f662788fe4 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -397,8 +397,8 @@ 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 raises :exc:`RuntimeError`, - as has always been the case for an executing frame. +* Calling :meth:`frame.clear` on a suspended frame raises :exc:`RuntimeError` + (as has always been the case for an executing frame). (Contributed by Irit Katriel in :gh:`79932`.) diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py index 32ea619ec018c7..7f17666a8d9697 100644 --- a/Lib/test/test_frame.py +++ b/Lib/test/test_frame.py @@ -81,11 +81,6 @@ def 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() - self.assertFalse(endly) - # Cannot clear a suspended frame with self.assertRaisesRegex(RuntimeError, r'suspended frame'): gen.gi_frame.clear()