summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorAmmar Askar <ammar@ammaraskar.com>2021-07-04 23:14:33 (GMT)
committerGitHub <noreply@github.com>2021-07-04 23:14:33 (GMT)
commit5644c7b3ffd49bed58dc095be6e6148e0bb4431e (patch)
treec53063e2f78685d1f284f6e333e05f44e49ea5ca /Lib
parent693cec0e2dcafa393ed5cdaa606f8dc8e3876adf (diff)
downloadcpython-5644c7b3ffd49bed58dc095be6e6148e0bb4431e.zip
cpython-5644c7b3ffd49bed58dc095be6e6148e0bb4431e.tar.gz
cpython-5644c7b3ffd49bed58dc095be6e6148e0bb4431e.tar.bz2
bpo-43950: Print columns in tracebacks (PEP 657) (GH-26958)
The traceback.c and traceback.py mechanisms now utilize the newly added code.co_positions and PyCode_Addr2Location to print carets on the specific expressions involved in a traceback. Co-authored-by: Pablo Galindo <Pablogsal@gmail.com> Co-authored-by: Ammar Askar <ammar@ammaraskar.com> Co-authored-by: Batuhan Taskaya <batuhanosmantaskaya@gmail.com>
Diffstat (limited to 'Lib')
-rw-r--r--Lib/idlelib/idle_test/test_run.py6
-rw-r--r--Lib/test/test_cmd_line_script.py4
-rw-r--r--Lib/test/test_doctest.py1
-rw-r--r--Lib/test/test_traceback.py176
-rw-r--r--Lib/test/test_zipimport.py5
-rw-r--r--Lib/traceback.py89
6 files changed, 244 insertions, 37 deletions
diff --git a/Lib/idlelib/idle_test/test_run.py b/Lib/idlelib/idle_test/test_run.py
index ec4637c..b289fa7 100644
--- a/Lib/idlelib/idle_test/test_run.py
+++ b/Lib/idlelib/idle_test/test_run.py
@@ -33,9 +33,9 @@ class ExceptionTest(unittest.TestCase):
run.print_exception()
tb = output.getvalue().strip().splitlines()
- self.assertEqual(11, len(tb))
- self.assertIn('UnhashableException: ex2', tb[3])
- self.assertIn('UnhashableException: ex1', tb[10])
+ self.assertEqual(13, len(tb))
+ self.assertIn('UnhashableException: ex2', tb[4])
+ self.assertIn('UnhashableException: ex1', tb[12])
data = (('1/0', ZeroDivisionError, "division by zero\n"),
('abc', NameError, "name 'abc' is not defined. "
diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py
index 6ffec91..e50c992 100644
--- a/Lib/test/test_cmd_line_script.py
+++ b/Lib/test/test_cmd_line_script.py
@@ -548,10 +548,10 @@ class CmdLineTest(unittest.TestCase):
script_name = _make_test_script(script_dir, 'script', script)
exitcode, stdout, stderr = assert_python_failure(script_name)
text = stderr.decode('ascii').split('\n')
- self.assertEqual(len(text), 5)
+ self.assertEqual(len(text), 6)
self.assertTrue(text[0].startswith('Traceback'))
self.assertTrue(text[1].startswith(' File '))
- self.assertTrue(text[3].startswith('NameError'))
+ self.assertTrue(text[4].startswith('NameError'))
def test_non_ascii(self):
# Mac OS X denies the creation of a file with an invalid UTF-8 name.
diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py
index 828a0ff..06d9d5d 100644
--- a/Lib/test/test_doctest.py
+++ b/Lib/test/test_doctest.py
@@ -2835,6 +2835,7 @@ Check doctest with a non-ascii filename:
exec(compile(example.source, filename, "single",
File "<doctest foo-bär@baz[0]>", line 1, in <module>
raise Exception('clé')
+ ^^^^^^^^^^^^^^^^^^^^^^
Exception: clé
TestResults(failed=1, attempted=1)
"""
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 78b2851..54f592a 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -17,8 +17,9 @@ import traceback
test_code = namedtuple('code', ['co_filename', 'co_name'])
+test_code.co_positions = lambda _: iter([(6, 6, 0, 0)])
test_frame = namedtuple('frame', ['f_code', 'f_globals', 'f_locals'])
-test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next'])
+test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next', 'tb_lasti'])
class TracebackCases(unittest.TestCase):
@@ -154,9 +155,9 @@ class TracebackCases(unittest.TestCase):
self.assertTrue(stdout[2].endswith(err_line),
"Invalid traceback line: {0!r} instead of {1!r}".format(
stdout[2], err_line))
- self.assertTrue(stdout[3] == err_msg,
+ self.assertTrue(stdout[4] == err_msg,
"Invalid error message: {0!r} instead of {1!r}".format(
- stdout[3], err_msg))
+ stdout[4], err_msg))
do_test("", "foo", "ascii", 3)
for charset in ("ascii", "iso-8859-1", "utf-8", "GBK"):
@@ -272,6 +273,114 @@ class TracebackCases(unittest.TestCase):
'(exc, /, value=<implicit>)')
+class TracebackErrorLocationCaretTests(unittest.TestCase):
+ """
+ Tests for printing code error expressions as part of PEP 657
+ """
+ def get_exception(self, callable):
+ try:
+ callable()
+ self.fail("No exception thrown.")
+ except:
+ return traceback.format_exc().splitlines()[:-1]
+
+ callable_line = get_exception.__code__.co_firstlineno + 2
+
+ def test_basic_caret(self):
+ def f():
+ raise ValueError("basic caret tests")
+
+ lineno_f = f.__code__.co_firstlineno
+ expected_f = (
+ 'Traceback (most recent call last):\n'
+ f' File "{__file__}", line {self.callable_line}, in get_exception\n'
+ ' callable()\n'
+ ' ^^^^^^^^^^\n'
+ f' File "{__file__}", line {lineno_f+1}, in f\n'
+ ' raise ValueError("basic caret tests")\n'
+ ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
+ )
+ result_lines = self.get_exception(f)
+ self.assertEqual(result_lines, expected_f.splitlines())
+
+ def test_line_with_unicode(self):
+ # Make sure that even if a line contains multi-byte unicode characters
+ # the correct carets are printed.
+ def f_with_unicode():
+ raise ValueError("Ĥellö Wörld")
+
+ lineno_f = f_with_unicode.__code__.co_firstlineno
+ expected_f = (
+ 'Traceback (most recent call last):\n'
+ f' File "{__file__}", line {self.callable_line}, in get_exception\n'
+ ' callable()\n'
+ ' ^^^^^^^^^^\n'
+ f' File "{__file__}", line {lineno_f+1}, in f_with_unicode\n'
+ ' raise ValueError("Ĥellö Wörld")\n'
+ ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
+ )
+ result_lines = self.get_exception(f_with_unicode)
+ self.assertEqual(result_lines, expected_f.splitlines())
+
+ def test_caret_in_type_annotation(self):
+ def f_with_type():
+ def foo(a: THIS_DOES_NOT_EXIST ) -> int:
+ return 0
+
+ lineno_f = f_with_type.__code__.co_firstlineno
+ expected_f = (
+ 'Traceback (most recent call last):\n'
+ f' File "{__file__}", line {self.callable_line}, in get_exception\n'
+ ' callable()\n'
+ ' ^^^^^^^^^^\n'
+ f' File "{__file__}", line {lineno_f+1}, in f_with_type\n'
+ ' def foo(a: THIS_DOES_NOT_EXIST ) -> int:\n'
+ ' ^^^^^^^^^^^^^^^^^^^\n'
+ )
+ result_lines = self.get_exception(f_with_type)
+ self.assertEqual(result_lines, expected_f.splitlines())
+
+ def test_caret_multiline_expression(self):
+ # Make sure no carets are printed for expressions spanning multiple
+ # lines.
+ def f_with_multiline():
+ raise ValueError(
+ "error over multiple lines"
+ )
+
+ lineno_f = f_with_multiline.__code__.co_firstlineno
+ expected_f = (
+ 'Traceback (most recent call last):\n'
+ f' File "{__file__}", line {self.callable_line}, in get_exception\n'
+ ' callable()\n'
+ ' ^^^^^^^^^^\n'
+ f' File "{__file__}", line {lineno_f+1}, in f_with_multiline\n'
+ ' raise ValueError(\n'
+ )
+ result_lines = self.get_exception(f_with_multiline)
+ self.assertEqual(result_lines, expected_f.splitlines())
+
+
+@cpython_only
+class CPythonTracebackErrorCaretTests(TracebackErrorLocationCaretTests):
+ """
+ Same set of tests as above but with Python's internal traceback printing.
+ """
+ def get_exception(self, callable):
+ from _testcapi import traceback_print
+ try:
+ callable()
+ self.fail("No exception thrown.")
+ except:
+ type_, value, tb = sys.exc_info()
+
+ file_ = StringIO()
+ traceback_print(tb, file_)
+ return file_.getvalue().splitlines()
+
+ callable_line = get_exception.__code__.co_firstlineno + 3
+
+
class TracebackFormatTests(unittest.TestCase):
def some_exception(self):
@@ -315,9 +424,9 @@ class TracebackFormatTests(unittest.TestCase):
# Make sure that the traceback is properly indented.
tb_lines = python_fmt.splitlines()
- self.assertEqual(len(tb_lines), 5)
+ self.assertEqual(len(tb_lines), 7)
banner = tb_lines[0]
- location, source_line = tb_lines[-2:]
+ location, source_line = tb_lines[-3], tb_lines[-2]
self.assertTrue(banner.startswith('Traceback'))
self.assertTrue(location.startswith(' File'))
self.assertTrue(source_line.startswith(' raise'))
@@ -381,12 +490,16 @@ class TracebackFormatTests(unittest.TestCase):
'Traceback (most recent call last):\n'
f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n'
' f()\n'
+ ' ^^^\n'
f' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
+ ' ^^^\n'
f' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
+ ' ^^^\n'
f' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n'
+ ' ^^^\n'
# XXX: The following line changes depending on whether the tests
# are run through the interactive interpreter or with -m
# It also varies depending on the platform (stack size)
@@ -427,19 +540,24 @@ class TracebackFormatTests(unittest.TestCase):
result_g = (
f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n'
+ ' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n'
+ ' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n'
+ ' ^^^^^^^^^^\n'
' [Previous line repeated 7 more times]\n'
f' File "{__file__}", line {lineno_g+3}, in g\n'
' raise ValueError\n'
+ ' ^^^^^^^^^^^^^^^^\n'
'ValueError\n'
)
tb_line = (
'Traceback (most recent call last):\n'
f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n'
' g()\n'
+ ' ^^^\n'
)
expected = (tb_line + result_g).splitlines()
actual = stderr_g.getvalue().splitlines()
@@ -464,15 +582,20 @@ class TracebackFormatTests(unittest.TestCase):
'Traceback (most recent call last):\n'
f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display\n'
' h()\n'
+ ' ^^^\n'
f' File "{__file__}", line {lineno_h+2}, in h\n'
' return h(count-1)\n'
+ ' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_h+2}, in h\n'
' return h(count-1)\n'
+ ' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_h+2}, in h\n'
' return h(count-1)\n'
+ ' ^^^^^^^^^^\n'
' [Previous line repeated 7 more times]\n'
f' File "{__file__}", line {lineno_h+3}, in h\n'
' g()\n'
+ ' ^^^\n'
)
expected = (result_h + result_g).splitlines()
actual = stderr_h.getvalue().splitlines()
@@ -489,18 +612,23 @@ class TracebackFormatTests(unittest.TestCase):
result_g = (
f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n'
+ ' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n'
+ ' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n'
+ ' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+3}, in g\n'
' raise ValueError\n'
+ ' ^^^^^^^^^^^^^^^^\n'
'ValueError\n'
)
tb_line = (
'Traceback (most recent call last):\n'
- f' File "{__file__}", line {lineno_g+71}, in _check_recursive_traceback_display\n'
+ f' File "{__file__}", line {lineno_g+81}, in _check_recursive_traceback_display\n'
' g(traceback._RECURSIVE_CUTOFF)\n'
+ ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
)
expected = (tb_line + result_g).splitlines()
actual = stderr_g.getvalue().splitlines()
@@ -517,19 +645,24 @@ class TracebackFormatTests(unittest.TestCase):
result_g = (
f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n'
+ ' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n'
+ ' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n'
+ ' ^^^^^^^^^^\n'
' [Previous line repeated 1 more time]\n'
f' File "{__file__}", line {lineno_g+3}, in g\n'
' raise ValueError\n'
+ ' ^^^^^^^^^^^^^^^^\n'
'ValueError\n'
)
tb_line = (
'Traceback (most recent call last):\n'
- f' File "{__file__}", line {lineno_g+99}, in _check_recursive_traceback_display\n'
+ f' File "{__file__}", line {lineno_g+114}, in _check_recursive_traceback_display\n'
' g(traceback._RECURSIVE_CUTOFF + 1)\n'
+ ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
)
expected = (tb_line + result_g).splitlines()
actual = stderr_g.getvalue().splitlines()
@@ -580,10 +713,10 @@ class TracebackFormatTests(unittest.TestCase):
exception_print(exc_val)
tb = stderr_f.getvalue().strip().splitlines()
- self.assertEqual(11, len(tb))
- self.assertEqual(context_message.strip(), tb[5])
- self.assertIn('UnhashableException: ex2', tb[3])
- self.assertIn('UnhashableException: ex1', tb[10])
+ self.assertEqual(13, len(tb))
+ self.assertEqual(context_message.strip(), tb[6])
+ self.assertIn('UnhashableException: ex2', tb[4])
+ self.assertIn('UnhashableException: ex1', tb[12])
cause_message = (
@@ -613,8 +746,8 @@ class BaseExceptionReportingTests:
def check_zero_div(self, msg):
lines = msg.splitlines()
- self.assertTrue(lines[-3].startswith(' File'))
- self.assertIn('1/0 # In zero_div', lines[-2])
+ self.assertTrue(lines[-4].startswith(' File'))
+ self.assertIn('1/0 # In zero_div', lines[-3])
self.assertTrue(lines[-1].startswith('ZeroDivisionError'), lines[-1])
def test_simple(self):
@@ -623,11 +756,11 @@ class BaseExceptionReportingTests:
except ZeroDivisionError as _:
e = _
lines = self.get_report(e).splitlines()
- self.assertEqual(len(lines), 4)
+ self.assertEqual(len(lines), 5)
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'))
+ self.assertTrue(lines[4].startswith('ZeroDivisionError'))
def test_cause(self):
def inner_raise():
@@ -666,11 +799,11 @@ class BaseExceptionReportingTests:
except ZeroDivisionError as _:
e = _
lines = self.get_report(e).splitlines()
- self.assertEqual(len(lines), 4)
+ self.assertEqual(len(lines), 5)
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.assertTrue(lines[4].startswith('ZeroDivisionError'))
def test_cause_and_context(self):
# When both a cause and a context are set, only the cause should be
@@ -1362,7 +1495,7 @@ class TestTracebackException(unittest.TestCase):
e = Exception("uh oh")
c = test_code('/foo.py', 'method')
f = test_frame(c, None, None)
- tb = test_tb(f, 6, None)
+ tb = test_tb(f, 6, None, 0)
exc = traceback.TracebackException(Exception, e, tb, lookup_lines=False)
self.assertEqual(linecache.cache, {})
linecache.updatecache('/foo.py', globals())
@@ -1373,7 +1506,7 @@ class TestTracebackException(unittest.TestCase):
e = Exception("uh oh")
c = test_code('/foo.py', 'method')
f = test_frame(c, globals(), {'something': 1, 'other': 'string'})
- tb = test_tb(f, 6, None)
+ tb = test_tb(f, 6, None, 0)
exc = traceback.TracebackException(
Exception, e, tb, capture_locals=True)
self.assertEqual(
@@ -1384,7 +1517,7 @@ class TestTracebackException(unittest.TestCase):
e = Exception("uh oh")
c = test_code('/foo.py', 'method')
f = test_frame(c, globals(), {'something': 1})
- tb = test_tb(f, 6, None)
+ tb = test_tb(f, 6, None, 0)
exc = traceback.TracebackException(Exception, e, tb)
self.assertEqual(exc.stack[0].locals, None)
@@ -1405,8 +1538,9 @@ class TestTracebackException(unittest.TestCase):
output = StringIO()
exc.print(file=output)
self.assertEqual(
- output.getvalue().split('\n')[-4:],
+ output.getvalue().split('\n')[-5:],
[' x/0',
+ ' ^^^',
' x = 12',
'ZeroDivisionError: division by zero',
''])
diff --git a/Lib/test/test_zipimport.py b/Lib/test/test_zipimport.py
index dfc4a06..861ebe3 100644
--- a/Lib/test/test_zipimport.py
+++ b/Lib/test/test_zipimport.py
@@ -716,7 +716,10 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase):
s = io.StringIO()
print_tb(tb, 1, s)
- self.assertTrue(s.getvalue().endswith(raise_src))
+ self.assertTrue(s.getvalue().endswith(
+ ' def do_raise(): raise TypeError\n'
+ ' ^^^^^^^^^^^^^^^\n'
+ ))
else:
raise AssertionError("This ought to be impossible")
diff --git a/Lib/traceback.py b/Lib/traceback.py
index b4c7641..cf1ba2a 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -69,7 +69,8 @@ def extract_tb(tb, limit=None):
trace. The line is a string with leading and trailing
whitespace stripped; if the source is not available it is None.
"""
- return StackSummary.extract(walk_tb(tb), limit=limit)
+ return StackSummary._extract_from_extended_frame_gen(
+ _walk_tb_with_full_positions(tb), limit=limit)
#
# Exception formatting and output.
@@ -251,10 +252,12 @@ class FrameSummary:
mapping the name to the repr() of the variable.
"""
- __slots__ = ('filename', 'lineno', 'name', '_line', 'locals')
+ __slots__ = ('filename', 'lineno', 'end_lineno', 'colno', 'end_colno',
+ 'name', '_line', 'locals')
def __init__(self, filename, lineno, name, *, lookup_line=True,
- locals=None, line=None):
+ locals=None, line=None,
+ end_lineno=None, colno=None, end_colno=None):
"""Construct a FrameSummary.
:param lookup_line: If True, `linecache` is consulted for the source
@@ -271,6 +274,9 @@ class FrameSummary:
if lookup_line:
self.line
self.locals = {k: repr(v) for k, v in locals.items()} if locals else None
+ self.end_lineno = end_lineno
+ self.colno = colno
+ self.end_colno = end_colno
def __eq__(self, other):
if isinstance(other, FrameSummary):
@@ -296,10 +302,16 @@ class FrameSummary:
return 4
@property
+ def _original_line(self):
+ # Returns the line as-is from the source, without modifying whitespace.
+ self.line
+ return self._line
+
+ @property
def line(self):
if self._line is None:
- self._line = linecache.getline(self.filename, self.lineno).strip()
- return self._line
+ self._line = linecache.getline(self.filename, self.lineno)
+ return self._line.strip()
def walk_stack(f):
@@ -309,7 +321,7 @@ def walk_stack(f):
current stack is used. Usually used with StackSummary.extract.
"""
if f is None:
- f = sys._getframe().f_back.f_back
+ f = sys._getframe().f_back.f_back.f_back.f_back
while f is not None:
yield f, f.f_lineno
f = f.f_back
@@ -326,6 +338,27 @@ def walk_tb(tb):
tb = tb.tb_next
+def _walk_tb_with_full_positions(tb):
+ # Internal version of walk_tb that yields full code positions including
+ # end line and column information.
+ while tb is not None:
+ positions = _get_code_position(tb.tb_frame.f_code, tb.tb_lasti)
+ # Yield tb_lineno when co_positions does not have a line number to
+ # maintain behavior with walk_tb.
+ if positions[0] is None:
+ yield tb.tb_frame, (tb.tb_lineno, ) + positions[1:]
+ else:
+ yield tb.tb_frame, positions
+ tb = tb.tb_next
+
+
+def _get_code_position(code, instruction_index):
+ if instruction_index < 0:
+ return (None, None, None, None)
+ positions_gen = code.co_positions()
+ return next(itertools.islice(positions_gen, instruction_index // 2, None))
+
+
_RECURSIVE_CUTOFF = 3 # Also hardcoded in traceback.c.
class StackSummary(list):
@@ -345,6 +378,21 @@ class StackSummary(list):
:param capture_locals: If True, the local variables from each frame will
be captured as object representations into the FrameSummary.
"""
+ def extended_frame_gen():
+ for f, lineno in frame_gen:
+ yield f, (lineno, None, None, None)
+
+ return klass._extract_from_extended_frame_gen(
+ extended_frame_gen(), limit=limit, lookup_lines=lookup_lines,
+ capture_locals=capture_locals)
+
+ @classmethod
+ def _extract_from_extended_frame_gen(klass, frame_gen, *, limit=None,
+ lookup_lines=True, capture_locals=False):
+ # Same as extract but operates on a frame generator that yields
+ # (frame, (lineno, end_lineno, colno, end_colno)) in the stack.
+ # Only lineno is required, the remaining fields can be empty if the
+ # information is not available.
if limit is None:
limit = getattr(sys, 'tracebacklimit', None)
if limit is not None and limit < 0:
@@ -357,7 +405,7 @@ class StackSummary(list):
result = klass()
fnames = set()
- for f, lineno in frame_gen:
+ for f, (lineno, end_lineno, colno, end_colno) in frame_gen:
co = f.f_code
filename = co.co_filename
name = co.co_name
@@ -370,7 +418,8 @@ class StackSummary(list):
else:
f_locals = None
result.append(FrameSummary(
- filename, lineno, name, lookup_line=False, locals=f_locals))
+ filename, lineno, name, lookup_line=False, locals=f_locals,
+ end_lineno=end_lineno, colno=colno, end_colno=end_colno))
for filename in fnames:
linecache.checkcache(filename)
# If immediate lookup was desired, trigger lookups now.
@@ -437,6 +486,17 @@ class StackSummary(list):
frame.filename, frame.lineno, frame.name))
if frame.line:
row.append(' {}\n'.format(frame.line.strip()))
+
+ stripped_characters = len(frame._original_line) - len(frame.line.lstrip())
+ if frame.end_lineno == frame.lineno and frame.end_colno != 0:
+ colno = _byte_offset_to_character_offset(frame._original_line, frame.colno)
+ end_colno = _byte_offset_to_character_offset(frame._original_line, frame.end_colno)
+
+ row.append(' ')
+ row.append(' ' * (colno - stripped_characters))
+ row.append('^' * (end_colno - colno))
+ row.append('\n')
+
if frame.locals:
for name, value in sorted(frame.locals.items()):
row.append(' {name} = {value}\n'.format(name=name, value=value))
@@ -450,6 +510,14 @@ class StackSummary(list):
return result
+def _byte_offset_to_character_offset(str, offset):
+ as_utf8 = str.encode('utf-8')
+ if offset > len(as_utf8):
+ offset = len(as_utf8)
+
+ return len(as_utf8[:offset + 1].decode("utf-8"))
+
+
class TracebackException:
"""An exception ready for rendering.
@@ -491,8 +559,9 @@ class TracebackException:
_seen.add(id(exc_value))
# TODO: locals.
- self.stack = StackSummary.extract(
- walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines,
+ self.stack = StackSummary._extract_from_extended_frame_gen(
+ _walk_tb_with_full_positions(exc_traceback),
+ limit=limit, lookup_lines=lookup_lines,
capture_locals=capture_locals)
self.exc_type = exc_type
# Capture now to permit freeing resources: only complication is in the