Skip to content

gh-79932: raise exception if frame.clear() is called on a suspended frame #111792

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 9 additions & 1 deletion Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand Down
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------------------
Expand Down
19 changes: 17 additions & 2 deletions Lib/test/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import threading
import types
import unittest
import warnings
import weakref
try:
import _testcapi
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
19 changes: 19 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecate clearing a suspended frame.
23 changes: 21 additions & 2 deletions Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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__},
Expand Down