diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 351c7723b0..448fe2605c 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4,10 +4,11 @@ from io import StringIO import linecache import sys +import inspect import unittest import re from test import support -from test.support import Error, captured_output, cpython_only +from test.support import Error, captured_output, cpython_only, ALWAYS_EQ from test.support.os_helper import TESTFN, unlink from test.support.script_helper import assert_python_ok import textwrap @@ -38,6 +39,9 @@ def syntax_error_with_caret(self): def syntax_error_with_caret_2(self): compile("1 +\n", "?", "exec") + def syntax_error_with_caret_range(self): + compile("f(x, y for y in range(30), z)", "?", "exec") + def syntax_error_bad_indentation(self): compile("def spam():\n print(1)\n print(2)", "?", "exec") @@ -47,6 +51,11 @@ def syntax_error_with_caret_non_ascii(self): def syntax_error_bad_indentation2(self): compile(" print(2)", "?", "exec") + def tokenizer_error_with_caret_range(self): + compile("blech ( ", "?", "exec") + + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_caret(self): err = self.get_exception_format(self.syntax_error_with_caret, SyntaxError) @@ -54,18 +63,35 @@ def test_caret(self): self.assertTrue(err[1].strip() == "return x!") self.assertIn("^", err[2]) # third line has caret self.assertEqual(err[1].find("!"), err[2].find("^")) # in the right place + self.assertEqual(err[2].count("^"), 1) err = self.get_exception_format(self.syntax_error_with_caret_2, SyntaxError) self.assertIn("^", err[2]) # third line has caret self.assertEqual(err[2].count('\n'), 1) # and no additional newline - self.assertEqual(err[1].find("+"), err[2].find("^")) # in the right place + self.assertEqual(err[1].find("+") + 1, err[2].find("^")) # in the right place + self.assertEqual(err[2].count("^"), 1) err = self.get_exception_format(self.syntax_error_with_caret_non_ascii, SyntaxError) self.assertIn("^", err[2]) # third line has caret self.assertEqual(err[2].count('\n'), 1) # and no additional newline - self.assertEqual(err[1].find("+"), err[2].find("^")) # in the right place + self.assertEqual(err[1].find("+") + 1, err[2].find("^")) # in the right place + self.assertEqual(err[2].count("^"), 1) + + err = self.get_exception_format(self.syntax_error_with_caret_range, + SyntaxError) + self.assertIn("^", err[2]) # third line has caret + self.assertEqual(err[2].count('\n'), 1) # and no additional newline + self.assertEqual(err[1].find("y"), err[2].find("^")) # in the right place + self.assertEqual(err[2].count("^"), len("y for y in range(30)")) + + err = self.get_exception_format(self.tokenizer_error_with_caret_range, + SyntaxError) + self.assertIn("^", err[2]) # third line has caret + self.assertEqual(err[2].count('\n'), 1) # and no additional newline + self.assertEqual(err[1].find("("), err[2].find("^")) # in the right place + self.assertEqual(err[2].count("^"), 1) # TODO: RUSTPYTHON @unittest.expectedFailure @@ -83,14 +109,13 @@ def test_bad_indentation(self): self.assertEqual(len(err), 4) self.assertEqual(err[1].strip(), "print(2)") self.assertIn("^", err[2]) - self.assertEqual(err[1].find(")"), err[2].find("^")) + self.assertEqual(err[1].find(")") + 1, err[2].find("^")) + # No caret for "unexpected indent" err = self.get_exception_format(self.syntax_error_bad_indentation2, IndentationError) - self.assertEqual(len(err), 4) + self.assertEqual(len(err), 3) self.assertEqual(err[1].strip(), "print(2)") - self.assertIn("^", err[2]) - self.assertEqual(err[1].find("p"), err[2].find("^")) def test_base_exception(self): # Test that exceptions derived from BaseException are formatted right @@ -222,6 +247,64 @@ def test_print_exception(self): ) self.assertEqual(output.getvalue(), "Exception: projector\n") + def test_print_exception_exc(self): + output = StringIO() + traceback.print_exception(Exception("projector"), file=output) + self.assertEqual(output.getvalue(), "Exception: projector\n") + + def test_format_exception_exc(self): + e = Exception("projector") + output = traceback.format_exception(e) + self.assertEqual(output, ["Exception: projector\n"]) + with self.assertRaisesRegex(ValueError, 'Both or neither'): + traceback.format_exception(e.__class__, e) + with self.assertRaisesRegex(ValueError, 'Both or neither'): + traceback.format_exception(e.__class__, tb=e.__traceback__) + with self.assertRaisesRegex(TypeError, 'positional-only'): + traceback.format_exception(exc=e) + + def test_format_exception_only_exc(self): + output = traceback.format_exception_only(Exception("projector")) + self.assertEqual(output, ["Exception: projector\n"]) + + def test_exception_is_None(self): + NONE_EXC_STRING = 'NoneType: None\n' + excfile = StringIO() + traceback.print_exception(None, file=excfile) + self.assertEqual(excfile.getvalue(), NONE_EXC_STRING) + + excfile = StringIO() + traceback.print_exception(None, None, None, file=excfile) + self.assertEqual(excfile.getvalue(), NONE_EXC_STRING) + + excfile = StringIO() + traceback.print_exc(None, file=excfile) + self.assertEqual(excfile.getvalue(), NONE_EXC_STRING) + + self.assertEqual(traceback.format_exc(None), NONE_EXC_STRING) + self.assertEqual(traceback.format_exception(None), [NONE_EXC_STRING]) + self.assertEqual( + traceback.format_exception(None, None, None), [NONE_EXC_STRING]) + self.assertEqual( + traceback.format_exception_only(None), [NONE_EXC_STRING]) + self.assertEqual( + traceback.format_exception_only(None, None), [NONE_EXC_STRING]) + + def test_signatures(self): + self.assertEqual( + str(inspect.signature(traceback.print_exception)), + ('(exc, /, value=, tb=, ' + 'limit=None, file=None, chain=True)')) + + self.assertEqual( + str(inspect.signature(traceback.format_exception)), + ('(exc, /, value=, tb=, limit=None, ' + 'chain=True)')) + + self.assertEqual( + str(inspect.signature(traceback.format_exception_only)), + '(exc, /, value=)') + class TracebackFormatTests(unittest.TestCase): @@ -322,7 +405,7 @@ def f(): with captured_output("stderr") as stderr_f: try: f() - except RecursionError as exc: + except RecursionError: render_exc() else: self.fail("no recursion occurred") @@ -369,7 +452,7 @@ def g(count=10): with captured_output("stderr") as stderr_g: try: g() - except ValueError as exc: + except ValueError: render_exc() else: self.fail("no value error was raised") @@ -405,7 +488,7 @@ def h(count=10): with captured_output("stderr") as stderr_h: try: h() - except ValueError as exc: + except ValueError: render_exc() else: self.fail("no value error was raised") @@ -433,7 +516,7 @@ def h(count=10): with captured_output("stderr") as stderr_g: try: g(traceback._RECURSIVE_CUTOFF) - except ValueError as exc: + except ValueError: render_exc() else: self.fail("no error raised") @@ -461,7 +544,7 @@ def h(count=10): with captured_output("stderr") as stderr_g: try: g(traceback._RECURSIVE_CUTOFF + 1) - except ValueError as exc: + except ValueError: render_exc() else: self.fail("no error raised") @@ -678,7 +761,32 @@ def e(): def e(): exec("x = 5 | 4 |") msg = self.get_report(e).splitlines() - self.assertEqual(msg[-2], ' ^') + self.assertEqual(msg[-2], ' ^') + + def test_syntax_error_no_lineno(self): + # See #34463. + + # Without filename + e = SyntaxError('bad syntax') + msg = self.get_report(e).splitlines() + self.assertEqual(msg, + ['SyntaxError: bad syntax']) + e.lineno = 100 + msg = self.get_report(e).splitlines() + self.assertEqual(msg, + [' File "", line 100', 'SyntaxError: bad syntax']) + + # With filename + e = SyntaxError('bad syntax') + e.filename = 'myfile.py' + + msg = self.get_report(e).splitlines() + self.assertEqual(msg, + ['SyntaxError: bad syntax (myfile.py)']) + e.lineno = 100 + msg = self.get_report(e).splitlines() + self.assertEqual(msg, + [' File "myfile.py", line 100', 'SyntaxError: bad syntax']) def test_message_none(self): # A message that looks like "None" should not be treated specially @@ -691,32 +799,51 @@ def test_message_none(self): err = self.get_report(Exception('')) self.assertIn('Exception\n', err) + def test_exception_modulename_not_unicode(self): + class X(Exception): + def __str__(self): + return "I am X" + + X.__module__ = 42 + + err = self.get_report(X()) + exp = f'.{X.__qualname__}: I am X\n' + self.assertEqual(exp, err) + # TODO: RUSTPYTHON @unittest.expectedFailure - def test_syntax_error_no_lineno(self): - # See #34463. - - # Without filename - e = SyntaxError('bad syntax') - msg = self.get_report(e).splitlines() - self.assertEqual(msg, - ['SyntaxError: bad syntax']) - e.lineno = 100 - msg = self.get_report(e).splitlines() - self.assertEqual(msg, - [' File "", line 100', 'SyntaxError: bad syntax']) - - # With filename - e = SyntaxError('bad syntax') - e.filename = 'myfile.py' - - msg = self.get_report(e).splitlines() - self.assertEqual(msg, - ['SyntaxError: bad syntax (myfile.py)']) - e.lineno = 100 - msg = self.get_report(e).splitlines() - self.assertEqual(msg, - [' File "myfile.py", line 100', 'SyntaxError: bad syntax']) + def test_syntax_error_various_offsets(self): + for offset in range(-5, 10): + for add in [0, 2]: + text = " "*add + "text%d" % offset + expected = [' File "file.py", line 1'] + if offset < 1: + expected.append(" %s" % text.lstrip()) + elif offset <= 6: + expected.append(" %s" % text.lstrip()) + expected.append(" %s^" % (" "*(offset-1))) + else: + expected.append(" %s" % text.lstrip()) + expected.append(" %s^" % (" "*5)) + expected.append("SyntaxError: msg") + expected.append("") + err = self.get_report(SyntaxError("msg", ("file.py", 1, offset+add, text))) + exp = "\n".join(expected) + self.assertEqual(exp, err) + + def test_format_exception_only_qualname(self): + class A: + class B: + class X(Exception): + def __str__(self): + return "I am X" + pass + err = self.get_report(A.B.X()) + str_value = 'I am X' + str_name = '.'.join([A.B.X.__module__, A.B.X.__qualname__]) + exp = "%s: %s\n" % (str_name, str_value) + self.assertEqual(exp, err) + class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): # @@ -901,8 +1028,6 @@ def inner(): # Local variable dict should now be empty. self.assertEqual(len(inner_frame.f_locals), 0) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_extract_stack(self): def extract(): return traceback.extract_stack() @@ -933,6 +1058,8 @@ def test_basics(self): # operator fallbacks to FrameSummary.__eq__. self.assertEqual(tuple(f), f) self.assertIsNone(f.locals) + self.assertNotEqual(f, object()) + self.assertEqual(f, ALWAYS_EQ) # TODO: RUSTPYTHON @unittest.expectedFailure @@ -945,12 +1072,14 @@ def test_lazy_lines(self): '"""Test cases for traceback module"""', f.line) + def test_no_line(self): + f = traceback.FrameSummary("f", None, "dummy") + self.assertEqual(f.line, None) + def test_explicit_line(self): f = traceback.FrameSummary("f", 1, "dummy", line="line") self.assertEqual("line", f.line) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_len(self): f = traceback.FrameSummary("f", 1, "dummy", line="line") self.assertEqual(len(f), 4) @@ -1145,6 +1274,71 @@ def test_context(self): # TODO: RUSTPYTHON @unittest.expectedFailure + def test_long_context_chain(self): + def f(): + try: + 1/0 + except: + f() + + try: + f() + except RecursionError: + exc_info = sys.exc_info() + else: + self.fail("Exception not raised") + + te = traceback.TracebackException(*exc_info) + res = list(te.format()) + + # many ZeroDiv errors followed by the RecursionError + self.assertGreater(len(res), sys.getrecursionlimit()) + self.assertGreater( + len([l for l in res if 'ZeroDivisionError:' in l]), + sys.getrecursionlimit() * 0.5) + self.assertIn( + "RecursionError: maximum recursion depth exceeded", res[-1]) + + def test_compact_with_cause(self): + try: + try: + 1/0 + finally: + cause = Exception("cause") + raise Exception("uh oh") from cause + except Exception: + exc_info = sys.exc_info() + exc = traceback.TracebackException(*exc_info, compact=True) + expected_stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_info[2])) + exc_cause = traceback.TracebackException(Exception, cause, None) + self.assertEqual(exc_cause, exc.__cause__) + self.assertEqual(None, exc.__context__) + self.assertEqual(True, exc.__suppress_context__) + self.assertEqual(expected_stack, exc.stack) + self.assertEqual(exc_info[0], exc.exc_type) + self.assertEqual(str(exc_info[1]), str(exc)) + + def test_compact_no_cause(self): + try: + try: + 1/0 + finally: + exc_info_context = sys.exc_info() + exc_context = traceback.TracebackException(*exc_info_context) + raise Exception("uh oh") + except Exception: + exc_info = sys.exc_info() + exc = traceback.TracebackException(*exc_info, compact=True) + expected_stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_info[2])) + self.assertEqual(None, exc.__cause__) + self.assertEqual(exc_context, exc.__context__) + self.assertEqual(False, exc.__suppress_context__) + self.assertEqual(expected_stack, exc.stack) + self.assertEqual(exc_info[0], exc.exc_type) + self.assertEqual(str(exc_info[1]), str(exc)) + def test_no_refs_to_exception_and_traceback_objects(self): try: 1/0 @@ -1166,9 +1360,8 @@ def test_comparison_basic(self): exc2 = traceback.TracebackException(*exc_info) self.assertIsNot(exc, exc2) self.assertEqual(exc, exc2) - class MyObject: - pass - self.assertNotEqual(exc, MyObject()) + self.assertNotEqual(exc, object()) + self.assertEqual(exc, ALWAYS_EQ) # TODO: RUSTPYTHON @unittest.expectedFailure @@ -1209,8 +1402,6 @@ def raise_with_locals(): exc7 = traceback.TracebackException(*exc_info, limit=-2, capture_locals=True) self.assertNotEqual(exc6, exc7) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_comparison_equivalent_exceptions_are_equal(self): excs = [] for _ in range(2): @@ -1303,9 +1494,9 @@ class MiscTest(unittest.TestCase): def test_all(self): expected = set() - blacklist = {'print_list'} + denylist = {'print_list'} for name in dir(traceback): - if name.startswith('_') or name in blacklist: + if name.startswith('_') or name in denylist: continue module_object = getattr(traceback, name) if getattr(module_object, '__module__', None) == 'traceback': diff --git a/Lib/traceback.py b/Lib/traceback.py index 4e7605d15f..d6a010f415 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -84,7 +84,25 @@ def extract_tb(tb, limit=None): "another exception occurred:\n\n") -def print_exception(etype, value, tb, limit=None, file=None, chain=True): +class _Sentinel: + def __repr__(self): + return "" + +_sentinel = _Sentinel() + +def _parse_value_tb(exc, value, tb): + if (value is _sentinel) != (tb is _sentinel): + raise ValueError("Both or neither of value and tb must be given") + if value is tb is _sentinel: + if exc is not None: + return exc, exc.__traceback__ + else: + return None, None + return value, tb + + +def print_exception(exc, /, value=_sentinel, tb=_sentinel, 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 @@ -95,17 +113,16 @@ def print_exception(etype, value, tb, limit=None, file=None, chain=True): occurred with a caret on the next line indicating the approximate position of the error. """ - # format_exception has ignored etype for some time, and code such as cgitb - # passes in bogus values as a result. For compatibility with such code we - # ignore it here (rather than in the new TracebackException API). + value, tb = _parse_value_tb(exc, value, tb) if file is None: file = sys.stderr - for line in TracebackException( - type(value), value, tb, limit=limit).format(chain=chain): + te = TracebackException(type(value), value, tb, limit=limit, compact=True) + for line in te.format(chain=chain): print(line, file=file, end="") -def format_exception(etype, value, tb, limit=None, chain=True): +def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ + chain=True): """Format a stack trace and the exception information. The arguments have the same meaning as the corresponding arguments @@ -114,19 +131,15 @@ def format_exception(etype, value, tb, limit=None, chain=True): these lines are concatenated and printed, exactly the same text is printed as does print_exception(). """ - # format_exception has ignored etype for some time, and code such as cgitb - # passes in bogus values as a result. For compatibility with such code we - # ignore it here (rather than in the new TracebackException API). - return list(TracebackException( - type(value), value, tb, limit=limit).format(chain=chain)) + value, tb = _parse_value_tb(exc, value, tb) + te = TracebackException(type(value), value, tb, limit=limit, compact=True) + return list(te.format(chain=chain)) -def format_exception_only(etype, value): +def format_exception_only(exc, /, value=_sentinel): """Format the exception part of a traceback. - The arguments are the exception type and value such as given by - sys.last_type and sys.last_value. The return value is a list of - strings, each ending in a newline. + The return value is a list of strings, each ending in a newline. Normally, the list contains a single string; however, for SyntaxError exceptions, it contains several lines that (when @@ -137,7 +150,10 @@ def format_exception_only(etype, value): string in the list. """ - return list(TracebackException(etype, value, None).format_exception_only()) + if value is _sentinel: + value = exc + te = TracebackException(type(value), value, None, compact=True) + return list(te.format_exception_only()) # -- not official API but folk probably use these two functions. @@ -279,12 +295,16 @@ def __repr__(self): return "".format( filename=self.filename, lineno=self.lineno, name=self.name) + def __len__(self): + return 4 + @property def line(self): if self._line is None: - self._line = linecache.getline(self.filename, self.lineno).strip() - return self._line - + if self.lineno is None: + return None + self._line = linecache.getline(self.filename, self.lineno) + return self._line.strip() def walk_stack(f): """Walk a stack yielding the frame and line number for each frame. @@ -455,53 +475,29 @@ class TracebackException: occurred. - :attr:`lineno` For syntax errors - the linenumber where the error occurred. + - :attr:`end_lineno` For syntax errors - the end linenumber where the error + occurred. Can be `None` if not present. - :attr:`text` For syntax errors - the text where the error occurred. - :attr:`offset` For syntax errors - the offset into the text where the error occurred. + - :attr:`end_offset` For syntax errors - the offset into the text where the + error occurred. Can be `None` if not present. - :attr:`msg` For syntax errors - the compiler error message. """ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, - lookup_lines=True, capture_locals=False, _seen=None): + lookup_lines=True, capture_locals=False, compact=False, + _seen=None): # NB: we need to accept exc_traceback, exc_value, exc_traceback to # permit backwards compat with the existing API, otherwise we # need stub thunk objects just to glue it together. # Handle loops in __cause__ or __context__. + is_recursive_call = _seen is not None if _seen is None: _seen = set() _seen.add(id(exc_value)) - # Gracefully handle (the way Python 2.4 and earlier did) the case of - # being called with no type or value (None, None, None). - if (exc_value and exc_value.__cause__ is not None - and id(exc_value.__cause__) not in _seen): - cause = TracebackException( - type(exc_value.__cause__), - exc_value.__cause__, - exc_value.__cause__.__traceback__, - limit=limit, - lookup_lines=False, - capture_locals=capture_locals, - _seen=_seen) - else: - cause = None - if (exc_value and exc_value.__context__ is not None - and id(exc_value.__context__) not in _seen): - context = TracebackException( - type(exc_value.__context__), - exc_value.__context__, - exc_value.__context__.__traceback__, - limit=limit, - lookup_lines=False, - capture_locals=capture_locals, - _seen=_seen) - else: - context = None - self.exc_traceback = exc_traceback - self.__cause__ = cause - self.__context__ = context - self.__suppress_context__ = \ - exc_value.__suppress_context__ if exc_value else False + # TODO: locals. self.stack = StackSummary.extract( walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines, @@ -513,12 +509,62 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, if exc_type and issubclass(exc_type, SyntaxError): # Handle SyntaxError's specially self.filename = exc_value.filename - self.lineno = str(exc_value.lineno) + lno = exc_value.lineno + self.lineno = str(lno) if lno is not None else None + end_lno = exc_value.end_lineno + self.end_lineno = str(end_lno) if end_lno is not None else None self.text = exc_value.text self.offset = exc_value.offset + self.end_offset = exc_value.end_offset self.msg = exc_value.msg if lookup_lines: self._load_lines() + self.__suppress_context__ = \ + exc_value.__suppress_context__ if exc_value is not None else False + + # Convert __cause__ and __context__ to `TracebackExceptions`s, use a + # queue to avoid recursion (only the top-level call gets _seen == None) + if not is_recursive_call: + queue = [(self, exc_value)] + while queue: + te, e = queue.pop() + if (e and e.__cause__ is not None + and id(e.__cause__) not in _seen): + cause = TracebackException( + type(e.__cause__), + e.__cause__, + e.__cause__.__traceback__, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + _seen=_seen) + else: + cause = None + + if compact: + need_context = (cause is None and + e is not None and + not e.__suppress_context__) + else: + need_context = True + if (e and e.__context__ is not None + and need_context and id(e.__context__) not in _seen): + context = TracebackException( + type(e.__context__), + e.__context__, + e.__context__.__traceback__, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + _seen=_seen) + else: + context = None + te.__cause__ = cause + te.__context__ = context + if cause: + queue.append((te.__cause__, e.__cause__)) + if context: + queue.append((te.__context__, e.__context__)) @classmethod def from_exception(cls, exc, *args, **kwargs): @@ -529,13 +575,11 @@ def _load_lines(self): """Private API. force all lines in the stack to be loaded.""" for frame in self.stack: frame.line - if self.__context__: - self.__context__._load_lines() - if self.__cause__: - self.__cause__._load_lines() def __eq__(self, other): - return self.__dict__ == other.__dict__ + if isinstance(other, TracebackException): + return self.__dict__ == other.__dict__ + return NotImplemented def __str__(self): return self._str @@ -546,7 +590,7 @@ def format_exception_only(self): The return value is a generator of strings, each ending in a newline. Normally, the generator emits a single string; however, for - SyntaxError exceptions, it emites several lines that (when + SyntaxError exceptions, it emits several lines that (when printed) display detailed information about where the syntax error occurred. @@ -560,30 +604,50 @@ def format_exception_only(self): stype = self.exc_type.__qualname__ smod = self.exc_type.__module__ if smod not in ("__main__", "builtins"): + if not isinstance(smod, str): + smod = "" stype = smod + '.' + stype if not issubclass(self.exc_type, SyntaxError): yield _format_final_exc_line(stype, self._str) - return - - # It was a syntax error; show exactly where the problem was found. - filename = self.filename or "" - lineno = str(self.lineno) or '?' - yield ' File "{}", line {}\n'.format(filename, lineno) - - badline = self.text - offset = self.offset - if badline is not None: - yield ' {}\n'.format(badline.strip()) - if offset is not None: - caretspace = badline.rstrip('\n') - offset = min(len(caretspace), offset) - 1 - caretspace = caretspace[:offset].lstrip() - # non-space whitespace (likes tabs) must be kept for alignment - caretspace = ((c.isspace() and c or ' ') for c in caretspace) - yield ' {}^\n'.format(''.join(caretspace)) + else: + yield from self._format_syntax_error(stype) + + def _format_syntax_error(self, stype): + """Format SyntaxError exceptions (internal helper).""" + # Show exactly where the problem was found. + filename_suffix = '' + if self.lineno is not None: + yield ' File "{}", line {}\n'.format( + self.filename or "", self.lineno) + elif self.filename is not None: + filename_suffix = ' ({})'.format(self.filename) + + text = self.text + if text is not None: + # text = " foo\n" + # rtext = " foo" + # ltext = "foo" + rtext = text.rstrip('\n') + ltext = rtext.lstrip(' \n\f') + spaces = len(rtext) - len(ltext) + yield ' {}\n'.format(ltext) + + if self.offset is not None: + offset = self.offset + end_offset = self.end_offset if self.end_offset not in {None, 0} else offset + if offset == end_offset or end_offset == -1: + end_offset = offset + 1 + + # Convert 1-based column offset to 0-based index into stripped text + colno = offset - 1 - spaces + end_colno = end_offset - 1 - spaces + if colno >= 0: + # non-space whitespace (likes tabs) must be kept for alignment + caretspace = ((c if c.isspace() else ' ') for c in ltext[:colno]) + yield ' {}{}'.format("".join(caretspace), ('^' * (end_colno - colno) + "\n")) msg = self.msg or "" - yield "{}: {}\n".format(stype, msg) + yield "{}: {}{}\n".format(stype, msg, filename_suffix) def format(self, *, chain=True): """Format the exception. @@ -597,15 +661,32 @@ def format(self, *, chain=True): The message indicating which exception occurred is always the last string in the output. """ - if chain: - if self.__cause__ is not None: - yield from self.__cause__.format(chain=chain) - yield _cause_message - elif (self.__context__ is not None and - not self.__suppress_context__): - yield from self.__context__.format(chain=chain) - yield _context_message - if self.exc_traceback is not None: - yield 'Traceback (most recent call last):\n' - yield from self.stack.format() - yield from self.format_exception_only() + + output = [] + exc = self + while exc: + if chain: + if exc.__cause__ is not None: + chained_msg = _cause_message + chained_exc = exc.__cause__ + elif (exc.__context__ is not None and + not exc.__suppress_context__): + chained_msg = _context_message + chained_exc = exc.__context__ + else: + chained_msg = None + chained_exc = None + + output.append((chained_msg, exc)) + exc = chained_exc + else: + output.append((None, exc)) + exc = None + + for msg, exc in reversed(output): + if msg is not None: + yield msg + if exc.stack: + yield 'Traceback (most recent call last):\n' + yield from exc.stack.format() + yield from exc.format_exception_only()