From 58720d6145eca69b9aa45b720cb3c1376b1ddaea Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Mon, 5 Aug 2013 23:26:40 +0200 Subject: Issue #17934: Add a clear() method to frame objects, to help clean up expensive details (local variables) and break reference cycles. --- Doc/library/inspect.rst | 4 ++++ Doc/reference/datamodel.rst | 14 ++++++++++++++ Include/frameobject.h | 3 +++ Include/genobject.h | 2 ++ Lib/test/test_sys.py | 2 +- Lib/test/test_traceback.py | 24 ++++++++++++++++++++---- Misc/NEWS | 3 +++ Objects/frameobject.c | 28 ++++++++++++++++++++++++++-- Objects/genobject.c | 8 +++++--- Python/ceval.c | 2 ++ 10 files changed, 80 insertions(+), 10 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index af6c96b..9f784fd 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -846,6 +846,10 @@ index of the current line within that list. finally: del frame + If you want to keep the frame around (for example to print a traceback + later), you can also break reference cycles by using the + :meth:`frame.clear` method. + The optional *context* argument supported by most of these functions specifies the number of lines of context to return, which are centered around the current line. diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 95028c2..a88d562 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -934,6 +934,20 @@ Internal types frame). A debugger can implement a Jump command (aka Set Next Statement) by writing to f_lineno. + Frame objects support one method: + + .. 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 + is finalized. This helps break reference cycles involving frame + objects (for example when catching an exception and storing its + traceback for later use). + + :exc:`RuntimeError` is raised if the frame is currently executing. + + .. versionadded:: 3.4 + Traceback objects .. index:: object: traceback diff --git a/Include/frameobject.h b/Include/frameobject.h index 33f73af..10ba06f 100644 --- a/Include/frameobject.h +++ b/Include/frameobject.h @@ -36,6 +36,8 @@ typedef struct _frame { non-generator frames. See the save_exc_state and swap_exc_state functions in ceval.c for details of their use. */ PyObject *f_exc_type, *f_exc_value, *f_exc_traceback; + /* Borrowed referenced to a generator, or NULL */ + PyObject *f_gen; PyThreadState *f_tstate; int f_lasti; /* Last instruction if called */ @@ -46,6 +48,7 @@ typedef struct _frame { bytecode index. */ int f_lineno; /* Current line number */ int f_iblock; /* index in f_blockstack */ + char f_executing; /* whether the frame is still executing */ PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */ PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */ } PyFrameObject; diff --git a/Include/genobject.h b/Include/genobject.h index ed451ba..65f1ecf 100644 --- a/Include/genobject.h +++ b/Include/genobject.h @@ -36,6 +36,8 @@ PyAPI_FUNC(PyObject *) PyGen_New(struct _frame *); PyAPI_FUNC(int) PyGen_NeedsFinalizing(PyGenObject *); PyAPI_FUNC(int) _PyGen_FetchStopIterationValue(PyObject **); PyObject *_PyGen_Send(PyGenObject *, PyObject *); +PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self); + #ifdef __cplusplus } diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 26c7ae7..e31bbc2 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -764,7 +764,7 @@ class SizeofTest(unittest.TestCase): nfrees = len(x.f_code.co_freevars) extras = x.f_code.co_stacksize + x.f_code.co_nlocals +\ ncells + nfrees - 1 - check(x, vsize('12P3i' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P')) + check(x, vsize('13P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P')) # function def func(): pass check(func, size('12P')) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 24753a8..66a12bf 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -150,11 +150,17 @@ class SyntaxTracebackCases(unittest.TestCase): class TracebackFormatTests(unittest.TestCase): - def test_traceback_format(self): + def some_exception(self): + raise KeyError('blah') + + def check_traceback_format(self, cleanup_func=None): try: - raise KeyError('blah') + self.some_exception() except KeyError: type_, value, tb = sys.exc_info() + if cleanup_func is not None: + # Clear the inner frames, not this one + cleanup_func(tb.tb_next) traceback_fmt = 'Traceback (most recent call last):\n' + \ ''.join(traceback.format_tb(tb)) file_ = StringIO() @@ -183,12 +189,22 @@ class TracebackFormatTests(unittest.TestCase): # Make sure that the traceback is properly indented. tb_lines = python_fmt.splitlines() - self.assertEqual(len(tb_lines), 3) - banner, location, source_line = tb_lines + self.assertEqual(len(tb_lines), 5) + banner = tb_lines[0] + location, source_line = tb_lines[-2:] self.assertTrue(banner.startswith('Traceback')) self.assertTrue(location.startswith(' File')) self.assertTrue(source_line.startswith(' raise')) + def test_traceback_format(self): + self.check_traceback_format() + + def test_traceback_format_with_cleared_frames(self): + # Check that traceback formatting also works with a clear()ed frame + def cleanup_tb(tb): + tb.tb_frame.clear() + self.check_traceback_format(cleanup_tb) + def test_stack_format(self): # Verify _stack functions. Note we have to use _getframe(1) to # compare them without this frame appearing in the output diff --git a/Misc/NEWS b/Misc/NEWS index 9f2c2dd..b33b94e 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,9 @@ Projected Release date: 2013-09-08 Core and Builtins ----------------- +- Issue #17934: Add a clear() method to frame objects, to help clean up + expensive details (local variables) and break reference cycles. + Library ------- diff --git a/Objects/frameobject.c b/Objects/frameobject.c index d3b59f1..a62a45e 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -488,7 +488,7 @@ frame_traverse(PyFrameObject *f, visitproc visit, void *arg) } static void -frame_clear(PyFrameObject *f) +frame_tp_clear(PyFrameObject *f) { PyObject **fastlocals, **p, **oldtop; Py_ssize_t i, slots; @@ -500,6 +500,7 @@ frame_clear(PyFrameObject *f) */ oldtop = f->f_stacktop; f->f_stacktop = NULL; + f->f_executing = 0; Py_CLEAR(f->f_exc_type); Py_CLEAR(f->f_exc_value); @@ -520,6 +521,25 @@ frame_clear(PyFrameObject *f) } static PyObject * +frame_clear(PyFrameObject *f) +{ + if (f->f_executing) { + PyErr_SetString(PyExc_RuntimeError, + "cannot clear an executing frame"); + return NULL; + } + if (f->f_gen) { + _PyGen_Finalize(f->f_gen); + assert(f->f_gen == NULL); + } + frame_tp_clear(f); + Py_RETURN_NONE; +} + +PyDoc_STRVAR(clear__doc__, +"F.clear(): clear most references held by the frame"); + +static PyObject * frame_sizeof(PyFrameObject *f) { Py_ssize_t res, extras, ncells, nfrees; @@ -538,6 +558,8 @@ PyDoc_STRVAR(sizeof__doc__, "F.__sizeof__() -> size of F in memory, in bytes"); static PyMethodDef frame_methods[] = { + {"clear", (PyCFunction)frame_clear, METH_NOARGS, + clear__doc__}, {"__sizeof__", (PyCFunction)frame_sizeof, METH_NOARGS, sizeof__doc__}, {NULL, NULL} /* sentinel */ @@ -566,7 +588,7 @@ PyTypeObject PyFrame_Type = { Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */ 0, /* tp_doc */ (traverseproc)frame_traverse, /* tp_traverse */ - (inquiry)frame_clear, /* tp_clear */ + (inquiry)frame_tp_clear, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ @@ -708,6 +730,8 @@ PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals, f->f_lasti = -1; f->f_lineno = code->co_firstlineno; f->f_iblock = 0; + f->f_executing = 0; + f->f_gen = NULL; _PyObject_GC_TRACK(f); return f; diff --git a/Objects/genobject.c b/Objects/genobject.c index dfd90aa..08d30bf 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -15,8 +15,8 @@ gen_traverse(PyGenObject *gen, visitproc visit, void *arg) return 0; } -static void -gen_finalize(PyObject *self) +void +_PyGen_Finalize(PyObject *self) { PyGenObject *gen = (PyGenObject *)self; PyObject *res; @@ -140,6 +140,7 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc) Py_XDECREF(t); Py_XDECREF(v); Py_XDECREF(tb); + gen->gi_frame->f_gen = NULL; gen->gi_frame = NULL; Py_DECREF(f); } @@ -505,7 +506,7 @@ PyTypeObject PyGen_Type = { 0, /* tp_weaklist */ 0, /* tp_del */ 0, /* tp_version_tag */ - gen_finalize, /* tp_finalize */ + _PyGen_Finalize, /* tp_finalize */ }; PyObject * @@ -517,6 +518,7 @@ PyGen_New(PyFrameObject *f) return NULL; } gen->gi_frame = f; + f->f_gen = (PyObject *) gen; Py_INCREF(f->f_code); gen->gi_code = (PyObject *)(f->f_code); gen->gi_running = 0; diff --git a/Python/ceval.c b/Python/ceval.c index 837b7c1..465876b 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1182,6 +1182,7 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag) stack_pointer = f->f_stacktop; assert(stack_pointer != NULL); f->f_stacktop = NULL; /* remains NULL unless yield suspends frame */ + f->f_executing = 1; if (co->co_flags & CO_GENERATOR && !throwflag) { if (f->f_exc_type != NULL && f->f_exc_type != Py_None) { @@ -3206,6 +3207,7 @@ fast_yield: /* pop frame */ exit_eval_frame: Py_LeaveRecursiveCall(); + f->f_executing = 0; tstate->frame = f->f_back; return retval; -- cgit v0.12