Skip to content

Commit ab6a2c1

Browse files
author
benjamin.peterson
committed
implement chained exception tracebacks
patch from Antoine Pitrou #3112 git-svn-id: http://svn.python.org/projects/python/branches/py3k@64965 6015fed2-1504-0410-9fe1-9d1591cc4771
1 parent c8f36af commit ab6a2c1

10 files changed

Lines changed: 445 additions & 129 deletions

File tree

Include/traceback.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ typedef struct _traceback {
1919

2020
PyAPI_FUNC(int) PyTraceBack_Here(struct _frame *);
2121
PyAPI_FUNC(int) PyTraceBack_Print(PyObject *, PyObject *);
22-
PyAPI_FUNC(int) Py_DisplaySourceLine(PyObject *, const char *, int);
22+
PyAPI_FUNC(int) Py_DisplaySourceLine(PyObject *, const char *, int, int);
2323

2424
/* Reveal traceback type so we can typecheck traceback objects */
2525
PyAPI_DATA(PyTypeObject) PyTraceBack_Type;

Lib/test/test_raise.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,30 @@ def __exit__(self, t, v, tb):
278278
else:
279279
self.fail("No exception raised")
280280

281+
def test_cycle_broken(self):
282+
# Self-cycles (when re-raising a caught exception) are broken
283+
try:
284+
try:
285+
1/0
286+
except ZeroDivisionError as e:
287+
raise e
288+
except ZeroDivisionError as e:
289+
self.failUnless(e.__context__ is None, e.__context__)
290+
291+
def test_reraise_cycle_broken(self):
292+
# Non-trivial context cycles (through re-raising a previous exception)
293+
# are broken too.
294+
try:
295+
try:
296+
xyzzy
297+
except NameError as a:
298+
try:
299+
1/0
300+
except ZeroDivisionError:
301+
raise a
302+
except NameError as e:
303+
self.failUnless(e.__context__.__context__ is None)
304+
281305

282306
class TestRemovedFunctionality(unittest.TestCase):
283307
def test_tuples(self):

Lib/test/test_traceback.py

Lines changed: 130 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Test cases for traceback module"""
22

3-
from _testcapi import traceback_print
3+
from _testcapi import traceback_print, exception_print
44
from io import StringIO
55
import sys
66
import unittest
7-
from test.support import run_unittest, is_jython, Error
7+
import re
8+
from test.support import run_unittest, is_jython, Error, captured_output
89

910
import traceback
1011

@@ -19,7 +20,7 @@
1920
raise Error("unable to create test traceback string")
2021

2122

22-
class TracebackCases(unittest.TestCase):
23+
class SyntaxTracebackCases(unittest.TestCase):
2324
# For now, a very minimal set of tests. I want to be sure that
2425
# formatting of SyntaxErrors works based on changes for 2.1.
2526

@@ -99,12 +100,135 @@ def test_traceback_indentation(self):
99100
banner, location, source_line = tb_lines
100101
self.assert_(banner.startswith('Traceback'))
101102
self.assert_(location.startswith(' File'))
102-
self.assert_(source_line.startswith('raise'))
103+
self.assert_(source_line.startswith(' raise'))
103104

104105

105-
def test_main():
106-
run_unittest(TracebackCases, TracebackFormatTests)
106+
cause_message = (
107+
"\nThe above exception was the direct cause "
108+
"of the following exception:\n\n")
109+
110+
context_message = (
111+
"\nDuring handling of the above exception, "
112+
"another exception occurred:\n\n")
113+
114+
boundaries = re.compile(
115+
'(%s|%s)' % (re.escape(cause_message), re.escape(context_message)))
116+
117+
118+
class BaseExceptionReportingTests:
119+
120+
def get_exception(self, exception_or_callable):
121+
if isinstance(exception_or_callable, Exception):
122+
return exception_or_callable
123+
try:
124+
exception_or_callable()
125+
except Exception as e:
126+
return e
107127

128+
def zero_div(self):
129+
1/0 # In zero_div
130+
131+
def check_zero_div(self, msg):
132+
lines = msg.splitlines()
133+
self.assert_(lines[-3].startswith(' File'))
134+
self.assert_('1/0 # In zero_div' in lines[-2], lines[-2])
135+
self.assert_(lines[-1].startswith('ZeroDivisionError'), lines[-1])
136+
137+
def test_simple(self):
138+
try:
139+
1/0 # Marker
140+
except ZeroDivisionError as _:
141+
e = _
142+
lines = self.get_report(e).splitlines()
143+
self.assertEquals(len(lines), 4)
144+
self.assert_(lines[0].startswith('Traceback'))
145+
self.assert_(lines[1].startswith(' File'))
146+
self.assert_('1/0 # Marker' in lines[2])
147+
self.assert_(lines[3].startswith('ZeroDivisionError'))
148+
149+
def test_cause(self):
150+
def inner_raise():
151+
try:
152+
self.zero_div()
153+
except ZeroDivisionError as e:
154+
raise KeyError from e
155+
def outer_raise():
156+
inner_raise() # Marker
157+
blocks = boundaries.split(self.get_report(outer_raise))
158+
self.assertEquals(len(blocks), 3)
159+
self.assertEquals(blocks[1], cause_message)
160+
self.check_zero_div(blocks[0])
161+
self.assert_('inner_raise() # Marker' in blocks[2])
162+
163+
def test_context(self):
164+
def inner_raise():
165+
try:
166+
self.zero_div()
167+
except ZeroDivisionError:
168+
raise KeyError
169+
def outer_raise():
170+
inner_raise() # Marker
171+
blocks = boundaries.split(self.get_report(outer_raise))
172+
self.assertEquals(len(blocks), 3)
173+
self.assertEquals(blocks[1], context_message)
174+
self.check_zero_div(blocks[0])
175+
self.assert_('inner_raise() # Marker' in blocks[2])
176+
177+
def test_cause_recursive(self):
178+
def inner_raise():
179+
try:
180+
try:
181+
self.zero_div()
182+
except ZeroDivisionError as e:
183+
z = e
184+
raise KeyError from e
185+
except KeyError as e:
186+
raise z from e
187+
def outer_raise():
188+
inner_raise() # Marker
189+
blocks = boundaries.split(self.get_report(outer_raise))
190+
self.assertEquals(len(blocks), 3)
191+
self.assertEquals(blocks[1], cause_message)
192+
# The first block is the KeyError raised from the ZeroDivisionError
193+
self.assert_('raise KeyError from e' in blocks[0])
194+
self.assert_('1/0' not in blocks[0])
195+
# The second block (apart from the boundary) is the ZeroDivisionError
196+
# re-raised from the KeyError
197+
self.assert_('inner_raise() # Marker' in blocks[2])
198+
self.check_zero_div(blocks[2])
199+
200+
201+
202+
class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
203+
#
204+
# This checks reporting through the 'traceback' module, with both
205+
# format_exception() and print_exception().
206+
#
207+
208+
def get_report(self, e):
209+
e = self.get_exception(e)
210+
s = ''.join(
211+
traceback.format_exception(type(e), e, e.__traceback__))
212+
with captured_output("stderr") as sio:
213+
traceback.print_exception(type(e), e, e.__traceback__)
214+
self.assertEquals(sio.getvalue(), s)
215+
return s
216+
217+
218+
class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
219+
#
220+
# This checks built-in reporting by the interpreter.
221+
#
222+
223+
def get_report(self, e):
224+
e = self.get_exception(e)
225+
with captured_output("stderr") as s:
226+
exception_print(e)
227+
return s.getvalue()
228+
229+
230+
def test_main():
231+
run_unittest(__name__)
108232

109233
if __name__ == "__main__":
110234
test_main()

Lib/traceback.py

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import linecache
44
import sys
55
import types
6+
import itertools
67

78
__all__ = ['extract_stack', 'extract_tb', 'format_exception',
89
'format_exception_only', 'format_list', 'format_stack',
@@ -107,7 +108,32 @@ def extract_tb(tb, limit = None):
107108
return list
108109

109110

110-
def print_exception(etype, value, tb, limit=None, file=None):
111+
_cause_message = (
112+
"\nThe above exception was the direct cause "
113+
"of the following exception:\n")
114+
115+
_context_message = (
116+
"\nDuring handling of the above exception, "
117+
"another exception occurred:\n")
118+
119+
def _iter_chain(exc, custom_tb=None, seen=None):
120+
if seen is None:
121+
seen = set()
122+
seen.add(exc)
123+
its = []
124+
cause = exc.__cause__
125+
context = exc.__context__
126+
if cause is not None and cause not in seen:
127+
its.append(_iter_chain(cause, None, seen))
128+
its.append([(_cause_message, None)])
129+
if context is not None and context is not cause and context not in seen:
130+
its.append(_iter_chain(context, None, seen))
131+
its.append([(_context_message, None)])
132+
its.append([(exc, custom_tb or exc.__traceback__)])
133+
return itertools.chain(*its)
134+
135+
136+
def print_exception(etype, value, tb, limit=None, file=None, chain=True):
111137
"""Print exception up to 'limit' stack trace entries from 'tb' to 'file'.
112138
113139
This differs from print_tb() in the following ways: (1) if
@@ -120,15 +146,23 @@ def print_exception(etype, value, tb, limit=None, file=None):
120146
"""
121147
if file is None:
122148
file = sys.stderr
123-
if tb:
124-
_print(file, 'Traceback (most recent call last):')
125-
print_tb(tb, limit, file)
126-
lines = format_exception_only(etype, value)
127-
for line in lines[:-1]:
128-
_print(file, line, ' ')
129-
_print(file, lines[-1], '')
130-
131-
def format_exception(etype, value, tb, limit = None):
149+
if chain:
150+
values = _iter_chain(value, tb)
151+
else:
152+
values = [(value, tb)]
153+
for value, tb in values:
154+
if isinstance(value, str):
155+
_print(file, value)
156+
continue
157+
if tb:
158+
_print(file, 'Traceback (most recent call last):')
159+
print_tb(tb, limit, file)
160+
lines = format_exception_only(type(value), value)
161+
for line in lines[:-1]:
162+
_print(file, line, ' ')
163+
_print(file, lines[-1], '')
164+
165+
def format_exception(etype, value, tb, limit=None, chain=True):
132166
"""Format a stack trace and the exception information.
133167
134168
The arguments have the same meaning as the corresponding arguments
@@ -137,12 +171,19 @@ def format_exception(etype, value, tb, limit = None):
137171
these lines are concatenated and printed, exactly the same text is
138172
printed as does print_exception().
139173
"""
140-
if tb:
141-
list = ['Traceback (most recent call last):\n']
142-
list = list + format_tb(tb, limit)
174+
list = []
175+
if chain:
176+
values = _iter_chain(value, tb)
143177
else:
144-
list = []
145-
list = list + format_exception_only(etype, value)
178+
values = [(value, tb)]
179+
for value, tb in values:
180+
if isinstance(value, str):
181+
list.append(value + '\n')
182+
continue
183+
if tb:
184+
list.append('Traceback (most recent call last):\n')
185+
list.extend(format_tb(tb, limit))
186+
list.extend(format_exception_only(type(value), value))
146187
return list
147188

148189
def format_exception_only(etype, value):
@@ -208,33 +249,34 @@ def _some_str(value):
208249
return '<unprintable %s object>' % type(value).__name__
209250

210251

211-
def print_exc(limit=None, file=None):
252+
def print_exc(limit=None, file=None, chain=True):
212253
"""Shorthand for 'print_exception(*sys.exc_info(), limit, file)'."""
213254
if file is None:
214255
file = sys.stderr
215256
try:
216257
etype, value, tb = sys.exc_info()
217-
print_exception(etype, value, tb, limit, file)
258+
print_exception(etype, value, tb, limit, file, chain)
218259
finally:
219260
etype = value = tb = None
220261

221262

222-
def format_exc(limit=None):
263+
def format_exc(limit=None, chain=True):
223264
"""Like print_exc() but return a string."""
224265
try:
225266
etype, value, tb = sys.exc_info()
226-
return ''.join(format_exception(etype, value, tb, limit))
267+
return ''.join(
268+
format_exception(etype, value, tb, limit, chain))
227269
finally:
228270
etype = value = tb = None
229271

230272

231-
def print_last(limit=None, file=None):
273+
def print_last(limit=None, file=None, chain=True):
232274
"""This is a shorthand for 'print_exception(sys.last_type,
233275
sys.last_value, sys.last_traceback, limit, file)'."""
234276
if file is None:
235277
file = sys.stderr
236278
print_exception(sys.last_type, sys.last_value, sys.last_traceback,
237-
limit, file)
279+
limit, file, chain)
238280

239281

240282
def print_stack(f=None, limit=None, file=None):

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Core and Builtins
2222

2323
- Issue #3236: Return small longs from PyLong_FromString.
2424

25+
- Exception tracebacks now support exception chaining.
26+
2527
Library
2628
-------
2729

@@ -35,6 +37,8 @@ Library
3537
- All the u* variant functions and methods in gettext have been renamed to their
3638
none u* siblings.
3739

40+
- The traceback module has been expanded to handle chained exceptions.
41+
3842
C API
3943
-----
4044

Modules/_testcapimodule.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,26 @@ traceback_print(PyObject *self, PyObject *args)
951951
Py_RETURN_NONE;
952952
}
953953

954+
/* To test the format of exceptions as printed out. */
955+
static PyObject *
956+
exception_print(PyObject *self, PyObject *args)
957+
{
958+
PyObject *value;
959+
PyObject *tb;
960+
961+
if (!PyArg_ParseTuple(args, "O:exception_print",
962+
&value))
963+
return NULL;
964+
965+
tb = PyException_GetTraceback(value);
966+
PyErr_Display((PyObject *) Py_TYPE(value), value, tb);
967+
Py_XDECREF(tb);
968+
969+
Py_RETURN_NONE;
970+
}
971+
972+
973+
954974
static PyMethodDef TestMethods[] = {
955975
{"raise_exception", raise_exception, METH_VARARGS},
956976
{"test_config", (PyCFunction)test_config, METH_NOARGS},
@@ -995,6 +1015,7 @@ static PyMethodDef TestMethods[] = {
9951015
{"profile_int", profile_int, METH_NOARGS},
9961016
#endif
9971017
{"traceback_print", traceback_print, METH_VARARGS},
1018+
{"exception_print", exception_print, METH_VARARGS},
9981019
{NULL, NULL} /* sentinel */
9991020
};
10001021

0 commit comments

Comments
 (0)