Skip to content

Commit 58720d6

Browse files
committed
Issue #17934: Add a clear() method to frame objects, to help clean up expensive details (local variables) and break reference cycles.
1 parent c53204b commit 58720d6

10 files changed

Lines changed: 80 additions & 10 deletions

File tree

Doc/library/inspect.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,10 @@ index of the current line within that list.
846846
finally:
847847
del frame
848848

849+
If you want to keep the frame around (for example to print a traceback
850+
later), you can also break reference cycles by using the
851+
:meth:`frame.clear` method.
852+
849853
The optional *context* argument supported by most of these functions specifies
850854
the number of lines of context to return, which are centered around the current
851855
line.

Doc/reference/datamodel.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,20 @@ Internal types
934934
frame). A debugger can implement a Jump command (aka Set Next Statement)
935935
by writing to f_lineno.
936936

937+
Frame objects support one method:
938+
939+
.. method:: frame.clear()
940+
941+
This method clears all references to local variables held by the
942+
frame. Also, if the frame belonged to a generator, the generator
943+
is finalized. This helps break reference cycles involving frame
944+
objects (for example when catching an exception and storing its
945+
traceback for later use).
946+
947+
:exc:`RuntimeError` is raised if the frame is currently executing.
948+
949+
.. versionadded:: 3.4
950+
937951
Traceback objects
938952
.. index::
939953
object: traceback

Include/frameobject.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ typedef struct _frame {
3636
non-generator frames. See the save_exc_state and swap_exc_state
3737
functions in ceval.c for details of their use. */
3838
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
39+
/* Borrowed referenced to a generator, or NULL */
40+
PyObject *f_gen;
3941

4042
PyThreadState *f_tstate;
4143
int f_lasti; /* Last instruction if called */
@@ -46,6 +48,7 @@ typedef struct _frame {
4648
bytecode index. */
4749
int f_lineno; /* Current line number */
4850
int f_iblock; /* index in f_blockstack */
51+
char f_executing; /* whether the frame is still executing */
4952
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
5053
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
5154
} PyFrameObject;

Include/genobject.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ PyAPI_FUNC(PyObject *) PyGen_New(struct _frame *);
3636
PyAPI_FUNC(int) PyGen_NeedsFinalizing(PyGenObject *);
3737
PyAPI_FUNC(int) _PyGen_FetchStopIterationValue(PyObject **);
3838
PyObject *_PyGen_Send(PyGenObject *, PyObject *);
39+
PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self);
40+
3941

4042
#ifdef __cplusplus
4143
}

Lib/test/test_sys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,7 @@ class C(object): pass
764764
nfrees = len(x.f_code.co_freevars)
765765
extras = x.f_code.co_stacksize + x.f_code.co_nlocals +\
766766
ncells + nfrees - 1
767-
check(x, vsize('12P3i' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
767+
check(x, vsize('13P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
768768
# function
769769
def func(): pass
770770
check(func, size('12P'))

Lib/test/test_traceback.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,17 @@ def do_test(firstlines, message, charset, lineno):
150150

151151
class TracebackFormatTests(unittest.TestCase):
152152

153-
def test_traceback_format(self):
153+
def some_exception(self):
154+
raise KeyError('blah')
155+
156+
def check_traceback_format(self, cleanup_func=None):
154157
try:
155-
raise KeyError('blah')
158+
self.some_exception()
156159
except KeyError:
157160
type_, value, tb = sys.exc_info()
161+
if cleanup_func is not None:
162+
# Clear the inner frames, not this one
163+
cleanup_func(tb.tb_next)
158164
traceback_fmt = 'Traceback (most recent call last):\n' + \
159165
''.join(traceback.format_tb(tb))
160166
file_ = StringIO()
@@ -183,12 +189,22 @@ def test_traceback_format(self):
183189

184190
# Make sure that the traceback is properly indented.
185191
tb_lines = python_fmt.splitlines()
186-
self.assertEqual(len(tb_lines), 3)
187-
banner, location, source_line = tb_lines
192+
self.assertEqual(len(tb_lines), 5)
193+
banner = tb_lines[0]
194+
location, source_line = tb_lines[-2:]
188195
self.assertTrue(banner.startswith('Traceback'))
189196
self.assertTrue(location.startswith(' File'))
190197
self.assertTrue(source_line.startswith(' raise'))
191198

199+
def test_traceback_format(self):
200+
self.check_traceback_format()
201+
202+
def test_traceback_format_with_cleared_frames(self):
203+
# Check that traceback formatting also works with a clear()ed frame
204+
def cleanup_tb(tb):
205+
tb.tb_frame.clear()
206+
self.check_traceback_format(cleanup_tb)
207+
192208
def test_stack_format(self):
193209
# Verify _stack functions. Note we have to use _getframe(1) to
194210
# compare them without this frame appearing in the output

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Projected Release date: 2013-09-08
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #17934: Add a clear() method to frame objects, to help clean up
14+
expensive details (local variables) and break reference cycles.
15+
1316
Library
1417
-------
1518

Objects/frameobject.c

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ frame_traverse(PyFrameObject *f, visitproc visit, void *arg)
488488
}
489489

490490
static void
491-
frame_clear(PyFrameObject *f)
491+
frame_tp_clear(PyFrameObject *f)
492492
{
493493
PyObject **fastlocals, **p, **oldtop;
494494
Py_ssize_t i, slots;
@@ -500,6 +500,7 @@ frame_clear(PyFrameObject *f)
500500
*/
501501
oldtop = f->f_stacktop;
502502
f->f_stacktop = NULL;
503+
f->f_executing = 0;
503504

504505
Py_CLEAR(f->f_exc_type);
505506
Py_CLEAR(f->f_exc_value);
@@ -519,6 +520,25 @@ frame_clear(PyFrameObject *f)
519520
}
520521
}
521522

523+
static PyObject *
524+
frame_clear(PyFrameObject *f)
525+
{
526+
if (f->f_executing) {
527+
PyErr_SetString(PyExc_RuntimeError,
528+
"cannot clear an executing frame");
529+
return NULL;
530+
}
531+
if (f->f_gen) {
532+
_PyGen_Finalize(f->f_gen);
533+
assert(f->f_gen == NULL);
534+
}
535+
frame_tp_clear(f);
536+
Py_RETURN_NONE;
537+
}
538+
539+
PyDoc_STRVAR(clear__doc__,
540+
"F.clear(): clear most references held by the frame");
541+
522542
static PyObject *
523543
frame_sizeof(PyFrameObject *f)
524544
{
@@ -538,6 +558,8 @@ PyDoc_STRVAR(sizeof__doc__,
538558
"F.__sizeof__() -> size of F in memory, in bytes");
539559

540560
static PyMethodDef frame_methods[] = {
561+
{"clear", (PyCFunction)frame_clear, METH_NOARGS,
562+
clear__doc__},
541563
{"__sizeof__", (PyCFunction)frame_sizeof, METH_NOARGS,
542564
sizeof__doc__},
543565
{NULL, NULL} /* sentinel */
@@ -566,7 +588,7 @@ PyTypeObject PyFrame_Type = {
566588
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
567589
0, /* tp_doc */
568590
(traverseproc)frame_traverse, /* tp_traverse */
569-
(inquiry)frame_clear, /* tp_clear */
591+
(inquiry)frame_tp_clear, /* tp_clear */
570592
0, /* tp_richcompare */
571593
0, /* tp_weaklistoffset */
572594
0, /* tp_iter */
@@ -708,6 +730,8 @@ PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
708730
f->f_lasti = -1;
709731
f->f_lineno = code->co_firstlineno;
710732
f->f_iblock = 0;
733+
f->f_executing = 0;
734+
f->f_gen = NULL;
711735

712736
_PyObject_GC_TRACK(f);
713737
return f;

Objects/genobject.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ gen_traverse(PyGenObject *gen, visitproc visit, void *arg)
1515
return 0;
1616
}
1717

18-
static void
19-
gen_finalize(PyObject *self)
18+
void
19+
_PyGen_Finalize(PyObject *self)
2020
{
2121
PyGenObject *gen = (PyGenObject *)self;
2222
PyObject *res;
@@ -140,6 +140,7 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
140140
Py_XDECREF(t);
141141
Py_XDECREF(v);
142142
Py_XDECREF(tb);
143+
gen->gi_frame->f_gen = NULL;
143144
gen->gi_frame = NULL;
144145
Py_DECREF(f);
145146
}
@@ -505,7 +506,7 @@ PyTypeObject PyGen_Type = {
505506
0, /* tp_weaklist */
506507
0, /* tp_del */
507508
0, /* tp_version_tag */
508-
gen_finalize, /* tp_finalize */
509+
_PyGen_Finalize, /* tp_finalize */
509510
};
510511

511512
PyObject *
@@ -517,6 +518,7 @@ PyGen_New(PyFrameObject *f)
517518
return NULL;
518519
}
519520
gen->gi_frame = f;
521+
f->f_gen = (PyObject *) gen;
520522
Py_INCREF(f->f_code);
521523
gen->gi_code = (PyObject *)(f->f_code);
522524
gen->gi_running = 0;

Python/ceval.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,7 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
11821182
stack_pointer = f->f_stacktop;
11831183
assert(stack_pointer != NULL);
11841184
f->f_stacktop = NULL; /* remains NULL unless yield suspends frame */
1185+
f->f_executing = 1;
11851186

11861187
if (co->co_flags & CO_GENERATOR && !throwflag) {
11871188
if (f->f_exc_type != NULL && f->f_exc_type != Py_None) {
@@ -3206,6 +3207,7 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
32063207
/* pop frame */
32073208
exit_eval_frame:
32083209
Py_LeaveRecursiveCall();
3210+
f->f_executing = 0;
32093211
tstate->frame = f->f_back;
32103212

32113213
return retval;

0 commit comments

Comments
 (0)