diff -r 32c6cfffbddd Lib/test/test_traceback.py --- a/Lib/test/test_traceback.py Thu Jul 11 15:58:07 2013 -0400 +++ b/Lib/test/test_traceback.py Thu Jul 11 22:02:46 2013 +0100 @@ -83,6 +83,16 @@ err = traceback.format_exception_only(None, None) self.assertEqual(err, ['None\n']) + err = traceback.format_exception(None, None, None) + self.assertEqual(err, ['None\n']) + + summary = traceback.extract_exception(None, None, None) + self.assertIsNone(summary.exc_type) + self.assertEqual(summary.exc_str, '') + self.assertEqual(summary.exc_traceback, []) + err = list(summary.format_exception()) + self.assertEqual(err, ['None\n']) + def test_encoded_file(self): # Test that tracebacks are correctly printed for encoded source files: # - correct line number (Issue2384) @@ -239,41 +249,105 @@ 1/0 # Marker except ZeroDivisionError as _: e = _ - lines = self.get_report(e).splitlines() + report = self.get_report(e) + lines = report.splitlines() self.assertEqual(len(lines), 4) self.assertTrue(lines[0].startswith('Traceback')) self.assertTrue(lines[1].startswith(' File')) self.assertIn('1/0 # Marker', lines[2]) self.assertTrue(lines[3].startswith('ZeroDivisionError')) + summary = traceback.extract_exception(type(e), e, None) + self.assertIs(summary.exc_type, ZeroDivisionError) + self.assertEqual(summary.exc_str, "division by zero") + self.assertEqual(len(summary.exc_traceback), 1) + self.assertEqual(summary.exc_traceback[0][0], __file__) + self.assertEqual(summary.exc_traceback[0][2], 'test_simple') + self.assertEqual(summary.exc_traceback[0][3], '1/0 # Marker') + + self.assertEqual(''.join(summary.format_exception()), report) + + def test_deferred_traceback(self): + def inner_raise(): + try: + self.zero_div() + except ZeroDivisionError as err: + raise KeyError from err + try: + inner_raise() # Marker + except KeyError as _: + e = _ + report = self.get_report(e) + + summary = traceback.extract_exception(type(e), e, None, defer=True) + self.assertEqual(len(summary.exc_traceback), 2) + tb = summary.exc_traceback + self.assertIsInstance(tb[0][3], traceback.DeferredLine) + self.assertEqual(str(tb[0][3]), 'inner_raise() # Marker') + + self.assertEqual(''.join(summary.format_exception()), report) + def test_cause(self): def inner_raise(): try: self.zero_div() - except ZeroDivisionError as e: - raise KeyError from e - def outer_raise(): + except ZeroDivisionError as err: + raise KeyError from err + try: inner_raise() # Marker - blocks = boundaries.split(self.get_report(outer_raise)) + except KeyError as _: + e = _ + report = self.get_report(e) + blocks = boundaries.split(report) self.assertEqual(len(blocks), 3) self.assertEqual(blocks[1], cause_message) self.check_zero_div(blocks[0]) self.assertIn('inner_raise() # Marker', blocks[2]) + summary = traceback.extract_exception(type(e), e, None) + self.assertIs(summary.exc_type, KeyError) + self.assertEqual(summary.exc_str, "") + self.assertEqual(len(summary.exc_traceback), 2) + tb = summary.exc_traceback + self.assertEqual(tb[0][0], __file__) + self.assertEqual(tb[0][2], 'test_cause') + self.assertEqual(tb[0][3], 'inner_raise() # Marker') + self.assertEqual(tb[1][0], __file__) + self.assertEqual(tb[1][2], 'inner_raise') + self.assertEqual(tb[1][3], 'raise KeyError from err') + + self.assertIsNot(summary.context, None) + self.assertIs(summary.cause, summary.context) + cause_summary = summary.cause + self.assertIs(cause_summary.exc_type, ZeroDivisionError) + self.assertEqual(cause_summary.exc_str, "division by zero") + self.assertEqual(len(cause_summary.exc_traceback), 2) + + self.assertEqual(''.join(summary.format_exception()), report) + def test_context(self): def inner_raise(): try: self.zero_div() except ZeroDivisionError: raise KeyError - def outer_raise(): + try: inner_raise() # Marker - blocks = boundaries.split(self.get_report(outer_raise)) + except KeyError as _: + e = _ + report = self.get_report(e) + blocks = boundaries.split(report) self.assertEqual(len(blocks), 3) self.assertEqual(blocks[1], context_message) self.check_zero_div(blocks[0]) self.assertIn('inner_raise() # Marker', blocks[2]) + summary = traceback.extract_exception(type(e), e, None) + self.assertIsNot(summary.context, None) + self.assertIs(summary.cause, None) + + self.assertEqual(''.join(summary.format_exception()), report) + def test_context_suppression(self): try: try: @@ -282,13 +356,21 @@ raise ZeroDivisionError from None except ZeroDivisionError as _: e = _ - lines = self.get_report(e).splitlines() + report = self.get_report(e) + lines = report.splitlines() self.assertEqual(len(lines), 4) self.assertTrue(lines[0].startswith('Traceback')) self.assertTrue(lines[1].startswith(' File')) self.assertIn('ZeroDivisionError from None', lines[2]) self.assertTrue(lines[3].startswith('ZeroDivisionError')) + self.assertIsNotNone(e.__context__) + summary = traceback.extract_exception(type(e), e, None) + self.assertIs(summary.cause, None) + self.assertIs(summary.context, None) + + self.assertEqual(''.join(summary.format_exception()), report) + def test_cause_and_context(self): # When both a cause and a context are set, only the cause should be # displayed and the context should be muted. @@ -301,14 +383,27 @@ xyzzy except NameError: raise KeyError from e - def outer_raise(): + try: inner_raise() # Marker - blocks = boundaries.split(self.get_report(outer_raise)) + except KeyError as _: + err = _ + report = self.get_report(err) + blocks = boundaries.split(report) self.assertEqual(len(blocks), 3) self.assertEqual(blocks[1], cause_message) self.check_zero_div(blocks[0]) self.assertIn('inner_raise() # Marker', blocks[2]) + summary = traceback.extract_exception(type(err), err, None) + self.assertIsNot(summary.context, None) + self.assertIsNot(summary.cause, None) + self.assertIs(summary.exc_type, KeyError) + self.assertIs(summary.cause.exc_type, ZeroDivisionError) + # The context is set even though it will be suppressed in the output + self.assertIs(summary.context.exc_type, NameError) + + self.assertEqual(''.join(summary.format_exception()), report) + def test_cause_recursive(self): def inner_raise(): try: @@ -319,9 +414,12 @@ raise KeyError from e except KeyError as e: raise z from e - def outer_raise(): + try: inner_raise() # Marker - blocks = boundaries.split(self.get_report(outer_raise)) + except ZeroDivisionError as _: + err = _ + report = self.get_report(err) + blocks = boundaries.split(report) self.assertEqual(len(blocks), 3) self.assertEqual(blocks[1], cause_message) # The first block is the KeyError raised from the ZeroDivisionError @@ -332,17 +430,39 @@ self.assertIn('inner_raise() # Marker', blocks[2]) self.check_zero_div(blocks[2]) + summary = traceback.extract_exception(type(err), err, None) + self.assertIsNot(summary.context, None) + self.assertIs(summary.cause, summary.context) + + self.assertIs(summary.cause.cause, summary) + self.assertIs(summary.context.context, None) + + self.assertEqual(''.join(summary.format_exception()), report) + def test_syntax_error_offset_at_eol(self): # See #10186. def e(): raise SyntaxError('', ('', 0, 5, 'hello')) msg = self.get_report(e).splitlines() self.assertEqual(msg[-2], " ^") - def e(): + try: exec("x = 5 | 4 |") - msg = self.get_report(e).splitlines() + except SyntaxError as _: + e = _ + report = self.get_report(e) + msg = report.splitlines() self.assertEqual(msg[-2], ' ^') + summary = traceback.extract_exception(type(e), e, None) + self.assertEqual(len(summary.exc_traceback), 2) + tb = summary.exc_traceback + self.assertEqual(tb[0][0], __file__) + + self.assertEqual(tb[1][0], '') + self.assertEqual(tb[1][2], '') + self.assertEqual(tb[1][3], 'x = 5 | 4 |\n') + + self.assertEqual(''.join(summary.format_exception()), report) class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): # diff -r 32c6cfffbddd Lib/traceback.py --- a/Lib/traceback.py Thu Jul 11 15:58:07 2013 -0400 +++ b/Lib/traceback.py Thu Jul 11 22:02:46 2013 +0100 @@ -13,11 +13,15 @@ # Formatting and printing lists of traceback lines. # -def _format_list_iter(extracted_list): +def _format_traceback(extracted_list): for filename, lineno, name, line in extracted_list: - item = ' File "{}", line {}, in {}\n'.format(filename, lineno, name) - if line: - item = item + ' {}\n'.format(line.strip()) + if name: + item = ' File "{}", line {}, in {}\n'.format(filename, lineno, name) + else: + item = ' File "{}", line {}\n'.format(filename, lineno) + line = str(line) # Evaluate deferred traceback lines + if line and line != 'None': + item += ' {}\n'.format(line.strip()) yield item def print_list(extracted_list, file=None): @@ -25,7 +29,7 @@ extract_stack() as a formatted stack trace to the given file.""" if file is None: file = sys.stderr - for item in _format_list_iter(extracted_list): + for item in _format_traceback(extracted_list): print(item, file=file, end="") def format_list(extracted_list): @@ -38,44 +42,68 @@ the strings may contain internal newlines as well, for those items whose source text line is not None. """ - return list(_format_list_iter(extracted_list)) + return list(_format_traceback(extracted_list)) # # Printing and Extracting Tracebacks. # -# extractor takes curr and needs to return a tuple of: -# - Frame object -# - Line number -# - Next item (same type as curr) -# In practice, curr is either a traceback or a frame. -def _extract_tb_or_stack_iter(curr, limit, extractor): +class DeferredLine: + """Wrap the necessary information for retrieving a line of a traceback + at a later time""" + __slots__ = ('filename', 'lineno', 'module_globals') + + def __init__(self, filename, lineno, module_globals): + self.filename = filename + self.lineno = lineno + if '__loader__' in module_globals: + self.module_globals = { + '__loader__': module_globals['__loader__'], + '__name__': module_globals['__name__']} + + def __str__(self): + if self.filename: + linecache.checkcache(self.filename) + line = linecache.getline(self.filename, self.lineno, + self.module_globals) + + if line: + return line.strip() + return '' + +def _extract_tb_tuples(frames, limit, defer=False): if limit is None: limit = getattr(sys, 'tracebacklimit', None) + checked = set() n = 0 - while curr is not None and (limit is None or n < limit): - f, lineno, next_item = extractor(curr) + for f, lineno in frames: + if limit is not None and n >= limit: + break co = f.f_code filename = co.co_filename name = co.co_name + line = None + if defer: + line = DeferredLine(filename, lineno, f.f_globals) + else: + if filename and filename not in checked: + linecache.checkcache(filename) + checked.add(filename) + line = linecache.getline(filename, lineno, f.f_globals) - linecache.checkcache(filename) - line = linecache.getline(filename, lineno, f.f_globals) - - if line: - line = line.strip() - else: - line = None + if line: + line = line.strip() + else: + line = None yield (filename, lineno, name, line) - curr = next_item n += 1 -def _extract_tb_iter(tb, limit): - return _extract_tb_or_stack_iter( - tb, limit, - operator.attrgetter("tb_frame", "tb_lineno", "tb_next")) +def _iter_tb(tb): + while tb: + yield tb.tb_frame, tb.tb_lineno + tb = tb.tb_next def print_tb(tb, limit=None, file=None): """Print up to 'limit' stack trace entries from the traceback 'tb'. @@ -91,7 +119,7 @@ """A shorthand for 'format_list(extract_tb(tb, limit)).""" return format_list(extract_tb(tb, limit=limit)) -def extract_tb(tb, limit=None): +def extract_tb(tb, limit=None, defer=False): """Return list of up to limit pre-processed entries from traceback. This is useful for alternate formatting of stack traces. If @@ -100,9 +128,10 @@ number, function name, text) representing the information that is usually printed for a stack trace. The text is a string with leading and trailing whitespace stripped; if the source is not - available it is None. + available it is None. If defer is set to True, line information + will not be extracted until it is needed. """ - return list(_extract_tb_iter(tb, limit=limit)) + return list(_extract_tb_tuples(_iter_tb(tb), limit=limit, defer=defer)) # # Exception formatting and output. @@ -110,11 +139,14 @@ _cause_message = ( "\nThe above exception was the direct cause " - "of the following exception:\n") + "of the following exception:\n\n") _context_message = ( "\nDuring handling of the above exception, " - "another exception occurred:\n") + "another exception occurred:\n\n") + +_traceback_message = ( + 'Traceback (most recent call last):\n') def _iter_chain(exc, custom_tb=None, seen=None): if seen is None: @@ -136,8 +168,8 @@ for it in its: yield from it -def _format_exception_iter(etype, value, tb, limit, chain): - if chain: +def _format_exception_iter(etype, value, tb, limit=None, chain=True): + if chain and value is not None: values = _iter_chain(value, tb) else: values = [(value, tb)] @@ -145,13 +177,58 @@ for value, tb in values: if isinstance(value, str): # This is a cause/context message line - yield value + '\n' + yield value continue if tb: - yield 'Traceback (most recent call last):\n' - yield from _format_list_iter(_extract_tb_iter(tb, limit=limit)) + yield _traceback_message + yield from _format_traceback(_extract_tb_tuples(_iter_tb(tb), limit=limit)) yield from _format_exception_only_iter(type(value), value) +def _format_exception_only_iter(etype, value): + if etype is None or etype is type(None): + # Gracefully handle (the way Python 2.4 and earlier did) the case of + # being called with (None, None). + stype = None + else: + stype = etype.__name__ + smod = etype.__module__ + if smod not in ("__main__", "builtins"): + stype = smod + '.' + stype + + if etype is None or not issubclass(etype, SyntaxError): + valuestr = _format_value(value) + if value is None or not valuestr: + yield "%s\n" % stype + else: + yield "%s: %s\n" % (stype, valuestr) + return + + # It was a syntax error; show exactly where the problem was found. + filename = value.filename or "" + lineno = str(value.lineno) or '?' + yield ' File "{}", line {}\n'.format(filename, lineno) + + badline = value.text + offset = value.offset + if badline is not None: + yield ' {}\n'.format(badline.strip()) + if offset is not None: + caretspace = badline.rstrip('\n')[:offset].lstrip() + # non-space whitespace (likes tabs) must be kept for alignment + caretspace = ((c.isspace() and c or ' ') for c in caretspace) + # only three spaces to account for offset1 == pos 0 + yield ' {}^\n'.format(''.join(caretspace)) + msg = value.msg or "" + yield "{}: {}\n".format(stype, msg) + +def _format_value(value): + if value is None: + return '' + try: + return str(value) + except: + return '' % type(value).__name__ + def print_exception(etype, value, tb, limit=None, file=None, chain=True): """Print exception up to 'limit' stack trace entries from 'tb' to 'file'. @@ -195,55 +272,7 @@ string in the list. """ - return list(_format_exception_only_iter(etype, value)) - -def _format_exception_only_iter(etype, value): - # Gracefully handle (the way Python 2.4 and earlier did) the case of - # being called with (None, None). - if etype is None: - yield _format_final_exc_line(etype, value) - return - - stype = etype.__name__ - smod = etype.__module__ - if smod not in ("__main__", "builtins"): - stype = smod + '.' + stype - - if not issubclass(etype, SyntaxError): - yield _format_final_exc_line(stype, value) - return - - # It was a syntax error; show exactly where the problem was found. - filename = value.filename or "" - lineno = str(value.lineno) or '?' - yield ' File "{}", line {}\n'.format(filename, lineno) - - badline = value.text - offset = value.offset - if badline is not None: - yield ' {}\n'.format(badline.strip()) - if offset is not None: - caretspace = badline.rstrip('\n')[:offset].lstrip() - # non-space whitespace (likes tabs) must be kept for alignment - caretspace = ((c.isspace() and c or ' ') for c in caretspace) - # only three spaces to account for offset1 == pos 0 - yield ' {}^\n'.format(''.join(caretspace)) - msg = value.msg or "" - yield "{}: {}\n".format(stype, msg) - -def _format_final_exc_line(etype, value): - valuestr = _some_str(value) - if value is None or not valuestr: - line = "%s\n" % etype - else: - line = "%s: %s\n" % (etype, valuestr) - return line - -def _some_str(value): - try: - return str(value) - except: - return '' % type(value).__name__ + return list(_format_exception_iter(etype, value, [], chain=False)) def print_exc(limit=None, file=None, chain=True): """Shorthand for 'print_exception(*sys.exc_info(), limit, file)'.""" @@ -261,13 +290,133 @@ print_exception(sys.last_type, sys.last_value, sys.last_traceback, limit, file, chain) +class ExceptionSummary: + """Summary object containing all relevant information to format an + exception description.""" + __slots__ = ('exc_type', 'exc_str', 'exc_traceback', 'exc_offset', + 'cause', 'context') + + def __init__(self, value, tb, limit=None, defer=False): + #: Exception type. + self.exc_type = type(value) if value is not None else None + #: Exception string representation. + self.exc_str = _format_value(value) + #: A list of tuples as returned by extract_tb. + self.exc_traceback = extract_tb(tb, limit, defer=defer) + + #: For a SyntaxError, the line position where the error occurred. + self.exc_offset = None + if isinstance(value, SyntaxError): + badline = value.text + filename = value.filename or "" + lineno = str(value.lineno) or '?' + + self.exc_traceback.append((filename, lineno, '', badline)) + self.exc_offset = value.offset + self.exc_str = value.msg or "" + + # Cause and context must be set later to prevent recursion errors + #: A summary object describing the exception cause. + self.cause = None + #: A summary object describing the exception context. This will be + #: set even when a cause has been explicitly set to something else, + #: but printing should omit it in that case. If context is explicitly + #: suppressed by 'raise ErrType as None', this will be None. + self.context = None + + def format_exception(self): + """Iterates over the formatted lines for this exception report. + + The yielded values are strings, each ending in a newline and some + containing internal newlines. When these lines are concatenated and + printed, exactly the same text is printed as does print_exception(). + """ + seen = set() + def _format_recursive(exc): + seen.add(exc) + if exc.cause and exc.cause not in seen: + yield from _format_recursive(exc.cause) + yield _cause_message + elif exc.context and exc.context not in seen: + yield from _format_recursive(exc.context) + yield _context_message + + stype = exc.exc_type + if exc.exc_type is not None: + stype = exc.exc_type.__name__ + smod = exc.exc_type.__module__ + if smod not in ("__main__", "builtins"): + stype = smod + '.' + stype + + if exc.exc_traceback: + yield _traceback_message + yield from _format_traceback(exc.exc_traceback) + if exc.exc_offset is not None: + badline = exc.exc_traceback[-1][3] + caretspace = badline.rstrip('\n')[:exc.exc_offset].lstrip() + # non-space whitespace (likes tabs) must be kept + caretspace = ((c.isspace() and c or ' ') + for c in caretspace) + # only three spaces to account for offset1 == pos 0 + yield ' {}^\n'.format(''.join(caretspace)) + if not exc.exc_str: + yield "%s\n" % stype + else: + yield "%s: %s\n" % (stype, exc.exc_str) + + yield from _format_recursive(self) + + def print_exception(self, file=None): + """Print the formatted exception to the given file.""" + if file is None: + file = sys.stderr + for line in self.format_exception(): + print(line, file=file, end='') + +def _exc_chain(exc, custom_tb=None, seen=None): + if seen is None: + seen = set() + seen.add(exc) + context = exc.__context__ + cause = exc.__cause__ + if cause is not None and cause not in seen: + yield from _exc_chain(cause, None, seen) + if context is not None and context not in seen: + yield from _exc_chain(context, None, seen) + yield (exc, custom_tb or exc.__traceback__) + +def extract_exception(etype, value, tb=None, limit=None, chain=True, + defer=False): + """Extract exception and traceback information and return a summary. + + The summary object contains the exception type, string representation and a + list of tuples as returned by extract_tb(). If the optional argument 'chain' + is set to True, the cause and context attributes will be set to a summary + object describing the cause or context exception. If 'chain' is set to + False, cause and context will be None. + """ + if chain and value is not None: + exceptions = _exc_chain(value, tb) + else: + exceptions = [(value, tb)] + + summaries = {exc: ExceptionSummary(exc, tb, limit=limit, defer=defer) + for (exc, tb) in exceptions} + for exc, summary in summaries.items(): + if exc is not None: + summary.cause = summaries.get(exc.__cause__) + if exc.__cause__ is not None or not exc.__suppress_context__: + summary.context = summaries.get(exc.__context__) + return summaries[value] + # # Printing and Extracting Stacks. # -def _extract_stack_iter(f, limit=None): - return _extract_tb_or_stack_iter( - f, limit, lambda f: (f, f.f_lineno, f.f_back)) +def _iter_stack(f): + while f: + yield f, f.f_lineno + f = f.f_back def _get_stack(f): if f is None: @@ -296,6 +445,6 @@ line number, function name, text), and the entries are in order from oldest to newest stack frame. """ - stack = list(_extract_stack_iter(_get_stack(f), limit=limit)) + stack = list(_extract_tb_tuples(_iter_stack(_get_stack(f)), limit=limit)) stack.reverse() return stack