[Python-checkins] cpython: Issue #17934: Add a clear() method to frame objects, to help clean up expensive

antoine.pitrou python-checkins at python.org
Mon Aug 5 23:28:02 CEST 2013


http://hg.python.org/cpython/rev/862ab99ab570
changeset:   85046:862ab99ab570
user:        Antoine Pitrou <solipsis at pitrou.net>
date:        Mon Aug 05 23:26:40 2013 +0200
summary:
  Issue #17934: Add a clear() method to frame objects, to help clean up expensive details (local variables) and break reference cycles.

files:
  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
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -846,6 +846,10 @@
           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
--- a/Doc/reference/datamodel.rst
+++ b/Doc/reference/datamodel.rst
@@ -934,6 +934,20 @@
       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
--- a/Include/frameobject.h
+++ b/Include/frameobject.h
@@ -36,6 +36,8 @@
            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 @@
        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
--- a/Include/genobject.h
+++ b/Include/genobject.h
@@ -36,6 +36,8 @@
 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
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -764,7 +764,7 @@
         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
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -150,11 +150,17 @@
 
 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 @@
 
         # 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
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -10,6 +10,9 @@
 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
--- a/Objects/frameobject.c
+++ b/Objects/frameobject.c
@@ -488,7 +488,7 @@
 }
 
 static void
-frame_clear(PyFrameObject *f)
+frame_tp_clear(PyFrameObject *f)
 {
     PyObject **fastlocals, **p, **oldtop;
     Py_ssize_t i, slots;
@@ -500,6 +500,7 @@
      */
     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 @@
 }
 
 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 @@
 "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 @@
     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 @@
     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
--- a/Objects/genobject.c
+++ b/Objects/genobject.c
@@ -15,8 +15,8 @@
     return 0;
 }
 
-static void
-gen_finalize(PyObject *self)
+void
+_PyGen_Finalize(PyObject *self)
 {
     PyGenObject *gen = (PyGenObject *)self;
     PyObject *res;
@@ -140,6 +140,7 @@
         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 @@
     0,                                          /* tp_weaklist */
     0,                                          /* tp_del */
     0,                                          /* tp_version_tag */
-    gen_finalize,                               /* tp_finalize */
+    _PyGen_Finalize,                            /* tp_finalize */
 };
 
 PyObject *
@@ -517,6 +518,7 @@
         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
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -1182,6 +1182,7 @@
     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 @@
     /* pop frame */
 exit_eval_frame:
     Py_LeaveRecursiveCall();
+    f->f_executing = 0;
     tstate->frame = f->f_back;
 
     return retval;

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list