diff -r c143699d8dee Lib/test/test_raise.py --- a/Lib/test/test_raise.py Wed Jun 18 20:01:29 2008 +0200 +++ b/Lib/test/test_raise.py Thu Jun 19 00:22:43 2008 +0200 @@ -121,6 +121,17 @@ class TestRaise(unittest.TestCase): else: self.fail("No exception raised") + def test_explicit_reraise(self): + # Check the most likely case of reference cycle through __context__ + # is avoided. + try: + try: + 1/0 + except ZeroDivisionError as e: + raise e + except ZeroDivisionError as e: + self.failUnless(e.__context__ is None, e.__context__) + class TestCause(unittest.TestCase): def test_invalid_cause(self): diff -r c143699d8dee Lib/test/test_traceback.py --- a/Lib/test/test_traceback.py Wed Jun 18 20:01:29 2008 +0200 +++ b/Lib/test/test_traceback.py Thu Jun 19 00:22:43 2008 +0200 @@ -1,10 +1,11 @@ """Test cases for traceback module""" -from _testcapi import traceback_print +from _testcapi import traceback_print, exception_print from io import StringIO import sys import unittest -from test.support import run_unittest, is_jython, Error +import re +from test.support import run_unittest, is_jython, Error, captured_output import traceback @@ -19,7 +20,7 @@ else: raise Error("unable to create test traceback string") -class TracebackCases(unittest.TestCase): +class SyntaxTracebackCases(unittest.TestCase): # For now, a very minimal set of tests. I want to be sure that # formatting of SyntaxErrors works based on changes for 2.1. @@ -102,9 +103,132 @@ class TracebackFormatTests(unittest.Test self.assert_(source_line.startswith('raise')) +cause_message = ( + "\nThe above exception was the direct cause " + "of the following exception:\n\n") + +context_message = ( + "\nDuring handling of the above exception, " + "another exception occurred:\n\n") + +boundaries = re.compile( + '(%s|%s)' % (re.escape(cause_message), re.escape(context_message))) + + +class BaseExceptionReportingTests: + + def get_exception(self, exception_or_callable): + if isinstance(exception_or_callable, Exception): + return exception_or_callable + try: + exception_or_callable() + except Exception as e: + return e + + def zero_div(self): + 1/0 # In zero_div + + def check_zero_div(self, msg): + lines = msg.splitlines() + self.assert_(lines[-3].startswith(' File')) + self.assert_('1/0 # In zero_div' in lines[-2], lines[-2]) + self.assert_(lines[-1].startswith('ZeroDivisionError'), lines[-1]) + + def test_simple(self): + try: + 1/0 # Marker + except ZeroDivisionError as _: + e = _ + lines = self.get_report(e).splitlines() + self.assertEquals(len(lines), 4) + self.assert_(lines[0].startswith('Traceback')) + self.assert_(lines[1].startswith(' File')) + self.assert_('1/0 # Marker' in lines[2]) + self.assert_(lines[3].startswith('ZeroDivisionError')) + + def test_cause(self): + def inner_raise(): + try: + self.zero_div() + except ZeroDivisionError as e: + raise KeyError from e + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEquals(len(blocks), 3) + self.assertEquals(blocks[1], cause_message) + self.check_zero_div(blocks[0]) + self.assert_('inner_raise() # Marker' in blocks[2]) + + def test_context(self): + def inner_raise(): + try: + self.zero_div() + except ZeroDivisionError: + raise KeyError + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEquals(len(blocks), 3) + self.assertEquals(blocks[1], context_message) + self.check_zero_div(blocks[0]) + self.assert_('inner_raise() # Marker' in blocks[2]) + + def test_cause_recursive(self): + def inner_raise(): + try: + try: + self.zero_div() + except ZeroDivisionError as e: + z = e + raise KeyError from e + except KeyError as e: + raise z from e + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEquals(len(blocks), 3) + self.assertEquals(blocks[1], cause_message) + # The first block is the KeyError raised from the ZeroDivisionError + self.assert_('raise KeyError from e' in blocks[0]) + self.assert_('1/0' not in blocks[0]) + # The second block (apart from the boundary) is the ZeroDivisionError + # re-raised from the KeyError + self.assert_('inner_raise() # Marker' in blocks[2]) + self.check_zero_div(blocks[2]) + + + +class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): + # + # This checks reporting through the 'traceback' module, with both + # format_exception() and print_exception(). + # + + def get_report(self, e): + e = self.get_exception(e) + s = ''.join( + traceback.format_exception(type(e), e, e.__traceback__)) + with captured_output("stderr") as sio: + traceback.print_exception(type(e), e, e.__traceback__) + self.assertEquals(sio.getvalue(), s) + return s + + +class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): + # + # This checks built-in reporting by the interpreter. + # + + def get_report(self, e): + e = self.get_exception(e) + with captured_output("stderr") as s: + exception_print(e) + return s.getvalue() + + def test_main(): - run_unittest(TracebackCases, TracebackFormatTests) - + run_unittest(__name__) if __name__ == "__main__": test_main() diff -r c143699d8dee Lib/traceback.py --- a/Lib/traceback.py Wed Jun 18 20:01:29 2008 +0200 +++ b/Lib/traceback.py Thu Jun 19 00:22:43 2008 +0200 @@ -3,6 +3,7 @@ import linecache import linecache import sys import types +import itertools __all__ = ['extract_stack', 'extract_tb', 'format_exception', 'format_exception_only', 'format_list', 'format_stack', @@ -107,7 +108,32 @@ def extract_tb(tb, limit = None): return list -def print_exception(etype, value, tb, limit=None, file=None): +_cause_message = ( + "\nThe above exception was the direct cause " + "of the following exception:\n") + +_context_message = ( + "\nDuring handling of the above exception, " + "another exception occurred:\n") + +def _iter_chain(exc, custom_tb=None, seen=None): + if seen is None: + seen = set() + seen.add(exc) + its = [] + cause = exc.__cause__ + context = exc.__context__ + if cause is not None and cause not in seen: + its.append(_iter_chain(cause, None, seen)) + its.append([(_cause_message, None)]) + if context is not None and context is not cause and context not in seen: + its.append(_iter_chain(context, None, seen)) + its.append([(_context_message, None)]) + its.append([(exc, custom_tb or exc.__traceback__)]) + return itertools.chain(*its) + + +def print_exception(etype, value, tb, limit=None, file=None, chain=True): """Print exception up to 'limit' stack trace entries from 'tb' to 'file'. This differs from print_tb() in the following ways: (1) if @@ -120,15 +146,23 @@ def print_exception(etype, value, tb, li """ if file is None: file = sys.stderr - if tb: - _print(file, 'Traceback (most recent call last):') - print_tb(tb, limit, file) - lines = format_exception_only(etype, value) - for line in lines[:-1]: - _print(file, line, ' ') - _print(file, lines[-1], '') + if chain: + values = _iter_chain(value, tb) + else: + values = [(value, tb)] + for value, tb in values: + if isinstance(value, str): + _print(file, value) + continue + if tb: + _print(file, 'Traceback (most recent call last):') + print_tb(tb, limit, file) + lines = format_exception_only(type(value), value) + for line in lines[:-1]: + _print(file, line, ' ') + _print(file, lines[-1], '') -def format_exception(etype, value, tb, limit = None): +def format_exception(etype, value, tb, limit=None, chain=True): """Format a stack trace and the exception information. The arguments have the same meaning as the corresponding arguments @@ -137,12 +171,19 @@ def format_exception(etype, value, tb, l these lines are concatenated and printed, exactly the same text is printed as does print_exception(). """ - if tb: - list = ['Traceback (most recent call last):\n'] - list = list + format_tb(tb, limit) + list = [] + if chain: + values = _iter_chain(value, tb) else: - list = [] - list = list + format_exception_only(etype, value) + values = [(value, tb)] + for value, tb in values: + if isinstance(value, str): + list.append(value + '\n') + continue + if tb: + list.append('Traceback (most recent call last):\n') + list.extend(format_tb(tb, limit)) + list.extend(format_exception_only(type(value), value)) return list def format_exception_only(etype, value): @@ -208,33 +249,34 @@ def _some_str(value): return '' % type(value).__name__ -def print_exc(limit=None, file=None): +def print_exc(limit=None, file=None, chain=True): """Shorthand for 'print_exception(*sys.exc_info(), limit, file)'.""" if file is None: file = sys.stderr try: etype, value, tb = sys.exc_info() - print_exception(etype, value, tb, limit, file) + print_exception(etype, value, tb, limit, file, chain) finally: etype = value = tb = None -def format_exc(limit=None): +def format_exc(limit=None, chain=True): """Like print_exc() but return a string.""" try: etype, value, tb = sys.exc_info() - return ''.join(format_exception(etype, value, tb, limit)) + return ''.join( + format_exception(etype, value, tb, limit, chain)) finally: etype = value = tb = None -def print_last(limit=None, file=None): +def print_last(limit=None, file=None, chain=True): """This is a shorthand for 'print_exception(sys.last_type, sys.last_value, sys.last_traceback, limit, file)'.""" if file is None: file = sys.stderr print_exception(sys.last_type, sys.last_value, sys.last_traceback, - limit, file) + limit, file, chain) def print_stack(f=None, limit=None, file=None): diff -r c143699d8dee Modules/_testcapimodule.c --- a/Modules/_testcapimodule.c Wed Jun 18 20:01:29 2008 +0200 +++ b/Modules/_testcapimodule.c Thu Jun 19 00:22:43 2008 +0200 @@ -950,6 +950,26 @@ traceback_print(PyObject *self, PyObject return NULL; Py_RETURN_NONE; } + +/* To test the format of exceptions as printed out. */ +static PyObject * +exception_print(PyObject *self, PyObject *args) +{ + PyObject *value; + PyObject *tb; + + if (!PyArg_ParseTuple(args, "O:exception_print", + &value)) + return NULL; + + tb = PyException_GetTraceback(value); + PyErr_Display((PyObject *) Py_TYPE(value), value, tb); + Py_XDECREF(tb); + + Py_RETURN_NONE; +} + + static PyMethodDef TestMethods[] = { {"raise_exception", raise_exception, METH_VARARGS}, @@ -995,6 +1015,7 @@ static PyMethodDef TestMethods[] = { {"profile_int", profile_int, METH_NOARGS}, #endif {"traceback_print", traceback_print, METH_VARARGS}, + {"exception_print", exception_print, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff -r c143699d8dee Python/errors.c --- a/Python/errors.c Wed Jun 18 20:01:29 2008 +0200 +++ b/Python/errors.c Thu Jun 19 00:22:43 2008 +0200 @@ -84,8 +84,18 @@ PyErr_SetObject(PyObject *exception, PyO return; value = fixed_value; } - Py_INCREF(tstate->exc_value); - PyException_SetContext(value, tstate->exc_value); + /* The most common source of a reference cycle through + the context attribute is when explicitly re-raising + an exception we just caught. We want to avoid garbage + collection of the exception object to be delayed. + Other kinds of circularities are possible, but only + with much more complex code (e.g. raise A, catch A + then raise B, then catch B and re-raise A). + */ + if (tstate->exc_value != value) { + Py_INCREF(tstate->exc_value); + PyException_SetContext(value, tstate->exc_value); + } } if (value != NULL && PyExceptionInstance_Check(value)) tb = PyException_GetTraceback(value); @@ -160,6 +170,9 @@ PyErr_ExceptionMatches(PyObject *exc) /* Used in many places to normalize a raised exception, including in eval_code2(), do_raise(), and PyErr_Print() + + XXX: should PyErr_NormalizeException() also call + PyException_SetTraceback() with the resulting value and tb? */ void PyErr_NormalizeException(PyObject **exc, PyObject **val, PyObject **tb) diff -r c143699d8dee Python/pythonrun.c --- a/Python/pythonrun.c Wed Jun 18 20:01:29 2008 +0200 +++ b/Python/pythonrun.c Thu Jun 19 00:22:43 2008 +0200 @@ -1242,18 +1242,19 @@ PyErr_PrintEx(int set_sys_last_vars) if (exception == NULL) return; PyErr_NormalizeException(&exception, &v, &tb); + tb = tb ? tb : Py_None; + PyException_SetTraceback(v, tb); if (exception == NULL) return; /* Now we know v != NULL too */ if (set_sys_last_vars) { PySys_SetObject("last_type", exception); PySys_SetObject("last_value", v); - PySys_SetObject("last_traceback", tb ? tb : Py_None); + PySys_SetObject("last_traceback", tb); } hook = PySys_GetObject("excepthook"); if (hook) { - PyObject *args = PyTuple_Pack(3, - exception, v, tb ? tb : Py_None); + PyObject *args = PyTuple_Pack(3, exception, v, tb); PyObject *result = PyEval_CallObject(hook, args); if (result == NULL) { PyObject *exception2, *v2, *tb2; @@ -1293,8 +1294,8 @@ PyErr_PrintEx(int set_sys_last_vars) Py_XDECREF(tb); } -void -PyErr_Display(PyObject *exception, PyObject *value, PyObject *tb) +int +PyErr_DisplaySingle(PyObject *exception, PyObject *value, PyObject *tb) { int err = 0; PyObject *f = PySys_GetObject("stderr"); @@ -1383,8 +1384,8 @@ PyErr_Display(PyObject *exception, PyObj */ if (s == NULL) err = -1; - else if (!PyUnicode_Check(s) || - PyUnicode_GetSize(s) != 0) + else if (PyUnicode_Check(s) && + PyUnicode_GetSize(s) != 0) err = PyFile_WriteString(": ", f); if (err == 0) err = PyFile_WriteObject(s, f, Py_PRINT_RAW); @@ -1398,6 +1399,86 @@ PyErr_Display(PyObject *exception, PyObj XXX This is wrong, but too many callers rely on this behavior. */ if (err != 0) PyErr_Clear(); + return err; +} + +static const char *cause_message = + "\nThe above exception was the direct cause " + "of the following exception:\n\n"; + +static const char *context_message = + "\nDuring handling of the above exception, " + "another exception occurred:\n\n"; + +static int +print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen) +{ + int err = 0, res; + PyObject *tb, *cause, *context; + + if (seen != NULL) { + /* Exception chaining */ + if (PySet_Add(seen, value) == -1) + PyErr_Clear(); + else if (PyExceptionInstance_Check(value)) { + cause = PyException_GetCause(value); + context = PyException_GetContext(value); + if (cause) { + res = PySet_Contains(seen, cause); + if (res == -1) + PyErr_Clear(); + if (res == 0) { + err |= print_exception_recursive(f, cause, seen); + err |= PyFile_WriteString(cause_message, f); + } + } + if (context) { + res = PySet_Contains(seen, context); + if (res == -1) + PyErr_Clear(); + if (res == 0) { + err |= print_exception_recursive(f, context, seen); + err |= PyFile_WriteString(context_message, f); + } + } + Py_XDECREF(context); + Py_XDECREF(cause); + } + } + tb = PyException_GetTraceback(value); + err |= PyErr_DisplaySingle((PyObject *) Py_TYPE(value), value, tb); + Py_XDECREF(tb); + return err; +} + +void +PyErr_Display(PyObject *exception, PyObject *value, PyObject *tb) +{ + int err = 0; + PyObject *seen; + PyObject *f = PySys_GetObject("stderr"); + if (f == Py_None) { + /* pass */ + } + else if (f == NULL) { + _PyObject_Dump(value); + fprintf(stderr, "lost sys.stderr\n"); + } + else { + /* We choose to ignore seen being possibly NULL, and report + at least the main exception (it could be a MemoryError). + */ + seen = PySet_New(NULL); + if (seen == NULL) + PyErr_Clear(); + err = print_exception_recursive(f, value, seen); + Py_XDECREF(seen); + } + /* If an error happened here, don't show it. + XXX This is wrong, but too many callers rely on this behavior. */ + if (err != 0) + PyErr_Clear(); + return err; } PyObject *