Update traceback from v3.14.3

This commit is contained in:
CPython Developers authored and Jeong, YunWon committed 2026-02-06 07:45:17 +09:00
1 parent c3212e7cd0
commit 7ccab4a4c8
2 files changed
+341 -156

No files matched your search

+177 -97
View File
@@ -18,8 +18,8 @@ import shutil
from test.support import (Error, captured_output, cpython_only, ALWAYS_EQ,
requires_debug_ranges, has_no_debug_ranges,
requires_subprocess)
from test.support.os_helper import TESTFN, unlink
from test.support.script_helper import assert_python_ok, assert_python_failure
from test.support.os_helper import TESTFN, temp_dir, unlink
from test.support.script_helper import assert_python_ok, assert_python_failure, make_script
from test.support.import_helper import forget
from test.support import force_not_colorized, force_not_colorized_test_class
@@ -37,6 +37,12 @@ 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', 'tb_lasti'])
color_overrides = {"reset": "z", "filename": "fn", "error_highlight": "E"}
colors = {
color_overrides.get(k, k[0].lower()): v
for k, v in _colorize.default_theme.traceback.items()
}
LEVENSHTEIN_DATA_FILE = Path(__file__).parent / 'levenshtein_examples.json'
@@ -87,7 +93,7 @@ class TracebackCases(unittest.TestCase):
err = self.get_exception_format(self.syntax_error_with_caret,
SyntaxError)
self.assertEqual(len(err), 4)
self.assertTrue(err[1].strip() == "return x!")
self.assertEqual(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)
@@ -402,8 +408,7 @@ class TracebackCases(unittest.TestCase):
self.assertEqual(len(err), 1)
self.assertEqual(err[-1], 'SyntaxError: error\n')
# TODO: RUSTPYTHON; IndexError: index out of range
@unittest.expectedFailure
@unittest.expectedFailure # TODO: RUSTPYTHON; IndexError: index out of range
@requires_subprocess()
@force_not_colorized
def test_encoded_file(self):
@@ -447,16 +452,10 @@ class TracebackCases(unittest.TestCase):
err_line = "raise RuntimeError('{0}')".format(message_ascii)
err_msg = "RuntimeError: {0}".format(message_ascii)
self.assertIn(("line %s" % lineno), stdout[1],
"Invalid line number: {0!r} instead of {1}".format(
stdout[1], lineno))
self.assertTrue(stdout[2].endswith(err_line),
"Invalid traceback line: {0!r} instead of {1!r}".format(
stdout[2], err_line))
self.assertIn("line %s" % lineno, stdout[1])
self.assertEndsWith(stdout[2], err_line)
actual_err_msg = stdout[3]
self.assertTrue(actual_err_msg == err_msg,
"Invalid error message: {0!r} instead of {1!r}".format(
actual_err_msg, err_msg))
self.assertEqual(actual_err_msg, err_msg)
do_test("", "foo", "ascii", 3)
for charset in ("ascii", "iso-8859-1", "utf-8", "GBK"):
@@ -509,6 +508,33 @@ class TracebackCases(unittest.TestCase):
b'ZeroDivisionError: division by zero']
self.assertEqual(stderr.splitlines(), expected)
@cpython_only
def test_lost_io_open(self):
# GH-142737: Display the traceback even if io.open is lost
crasher = textwrap.dedent("""\
import io
import traceback
# Trigger fallback mode
traceback._print_exception_bltin = None
del io.open
raise RuntimeError("should not crash")
""")
# Create a temporary script to exercise _Py_FindSourceFile
with temp_dir() as script_dir:
script = make_script(
script_dir=script_dir,
script_basename='tb_test_no_io_open',
source=crasher)
rc, stdout, stderr = assert_python_failure(script)
self.assertEqual(rc, 1) # Make sure it's not a crash
expected = [b'Traceback (most recent call last):',
f' File "{script}", line 6, in <module>'.encode(),
b'RuntimeError: should not crash']
self.assertEqual(stderr.splitlines(), expected)
def test_print_exception(self):
output = StringIO()
traceback.print_exception(
@@ -611,7 +637,7 @@ class CAPIExceptionFormattingMixin:
class CAPIExceptionFormattingLegacyMixin(CAPIExceptionFormattingMixin):
LEGACY = 1
# @requires_debug_ranges() # XXX: RUSTPYTHON patch
@requires_debug_ranges()
class TracebackErrorLocationCaretTestBase:
"""
Tests for printing code error expressions as part of PEP 657
@@ -658,6 +684,7 @@ class TracebackErrorLocationCaretTestBase:
def f_with_type():
def foo(a: THIS_DOES_NOT_EXIST ) -> int:
return 0
foo.__annotations__
lineno_f = f_with_type.__code__.co_firstlineno
expected_f = (
@@ -665,7 +692,9 @@ class TracebackErrorLocationCaretTestBase:
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'
f' File "{__file__}", line {lineno_f+3}, in f_with_type\n'
' foo.__annotations__\n'
f' File "{__file__}", line {lineno_f+1}, in __annotate__\n'
' def foo(a: THIS_DOES_NOT_EXIST ) -> int:\n'
' ^^^^^^^^^^^^^^^^^^^\n'
)
@@ -1742,8 +1771,53 @@ class TracebackErrorLocationCaretTestBase:
]
self.assertEqual(result_lines, expected)
class TestKeywordTypoSuggestions(unittest.TestCase):
TYPO_CASES = [
("with block ad something:\n pass", "and"),
("fur a in b:\n pass", "for"),
("for a in b:\n pass\nelso:\n pass", "else"),
("whille True:\n pass", "while"),
("iff x > 5:\n pass", "if"),
("if x:\n pass\nelseif y:\n pass", "elif"),
("tyo:\n pass\nexcept y:\n pass", "try"),
("classe MyClass:\n pass", "class"),
("impor math", "import"),
("form x import y", "from"),
("defn calculate_sum(a, b):\n return a + b", "def"),
("def foo():\n returm result", "return"),
("lamda x: x ** 2", "lambda"),
("def foo():\n yeld i", "yield"),
("def foo():\n globel counter", "global"),
("frum math import sqrt", "from"),
("asynch def fetch_data():\n pass", "async"),
("async def foo():\n awaid fetch_data()", "await"),
('raisee ValueError("Error")', "raise"),
("[x for x\nin range(3)\nof x]", "if"),
("[123 fur x\nin range(3)\nif x]", "for"),
("for x im n:\n pass", "in"),
]
# @requires_debug_ranges() # XXX: RUSTPYTHON patch
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_keyword_suggestions_from_file(self):
with tempfile.TemporaryDirectory() as script_dir:
for i, (code, expected_kw) in enumerate(self.TYPO_CASES):
with self.subTest(typo=expected_kw):
source = textwrap.dedent(code).strip()
script_name = make_script(script_dir, f"script_{i}", source)
rc, stdout, stderr = assert_python_failure(script_name)
stderr_text = stderr.decode('utf-8')
self.assertIn(f"Did you mean '{expected_kw}'", stderr_text)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_keyword_suggestions_from_command_string(self):
for code, expected_kw in self.TYPO_CASES:
with self.subTest(typo=expected_kw):
source = textwrap.dedent(code).strip()
rc, stdout, stderr = assert_python_failure('-c', source)
stderr_text = stderr.decode('utf-8')
self.assertIn(f"Did you mean '{expected_kw}'", stderr_text)
@requires_debug_ranges()
@force_not_colorized_test_class
class PurePythonTracebackErrorCaretTests(
PurePythonExceptionFormattingMixin,
@@ -1869,7 +1943,7 @@ class PurePythonTracebackErrorCaretTests(
@cpython_only
# @requires_debug_ranges() # XXX: RUSTPYTHON patch
@requires_debug_ranges()
@force_not_colorized_test_class
class CPythonTracebackErrorCaretTests(
CAPIExceptionFormattingMixin,
@@ -1881,7 +1955,7 @@ class CPythonTracebackErrorCaretTests(
"""
@cpython_only
# @requires_debug_ranges() # XXX: RUSTPYTHON patch
@requires_debug_ranges()
@force_not_colorized_test_class
class CPythonTracebackLegacyErrorCaretTests(
CAPIExceptionFormattingLegacyMixin,
@@ -1953,9 +2027,9 @@ class TracebackFormatMixin:
banner = tb_lines[0]
self.assertEqual(len(tb_lines), 5)
location, source_line = tb_lines[-2], tb_lines[-1]
self.assertTrue(banner.startswith('Traceback'))
self.assertTrue(location.startswith(' File'))
self.assertTrue(source_line.startswith(' raise'))
self.assertStartsWith(banner, 'Traceback')
self.assertStartsWith(location, ' File')
self.assertStartsWith(source_line, ' raise')
def test_traceback_format(self):
self.check_traceback_format()
@@ -2188,7 +2262,7 @@ class TracebackFormatMixin:
actual = stderr_g.getvalue().splitlines()
self.assertEqual(actual, expected)
# @requires_debug_ranges() # XXX: RUSTPYTHON patch
@requires_debug_ranges()
def test_recursive_traceback(self):
if self.DEBUG_RANGES:
self._check_recursive_traceback_display(traceback.print_exc)
@@ -2244,6 +2318,7 @@ class TracebackFormatMixin:
return e
@cpython_only
@support.skip_emscripten_stack_overflow()
def test_exception_group_deep_recursion_capi(self):
from _testcapi import exception_print
LIMIT = 75
@@ -2255,6 +2330,7 @@ class TracebackFormatMixin:
self.assertIn('ExceptionGroup', output)
self.assertLessEqual(output.count('ExceptionGroup'), LIMIT)
@support.skip_emscripten_stack_overflow()
def test_exception_group_deep_recursion_traceback(self):
LIMIT = 75
eg = self.deep_eg()
@@ -2332,12 +2408,12 @@ class BaseExceptionReportingTests:
def check_zero_div(self, msg):
lines = msg.splitlines()
if has_no_debug_ranges():
self.assertTrue(lines[-3].startswith(' File'))
self.assertStartsWith(lines[-3], ' File')
self.assertIn('1/0 # In zero_div', lines[-2])
else:
self.assertTrue(lines[-4].startswith(' File'))
self.assertStartsWith(lines[-4], ' File')
self.assertIn('1/0 # In zero_div', lines[-3])
self.assertTrue(lines[-1].startswith('ZeroDivisionError'), lines[-1])
self.assertStartsWith(lines[-1], 'ZeroDivisionError')
def test_simple(self):
try:
@@ -2347,12 +2423,12 @@ class BaseExceptionReportingTests:
lines = self.get_report(e).splitlines()
if has_no_debug_ranges():
self.assertEqual(len(lines), 4)
self.assertTrue(lines[3].startswith('ZeroDivisionError'))
self.assertStartsWith(lines[3], 'ZeroDivisionError')
else:
self.assertEqual(len(lines), 5)
self.assertTrue(lines[4].startswith('ZeroDivisionError'))
self.assertTrue(lines[0].startswith('Traceback'))
self.assertTrue(lines[1].startswith(' File'))
self.assertStartsWith(lines[4], 'ZeroDivisionError')
self.assertStartsWith(lines[0], 'Traceback')
self.assertStartsWith(lines[1], ' File')
self.assertIn('1/0 # Marker', lines[2])
def test_cause(self):
@@ -2393,9 +2469,9 @@ class BaseExceptionReportingTests:
e = _
lines = self.get_report(e).splitlines()
self.assertEqual(len(lines), 4)
self.assertTrue(lines[3].startswith('ZeroDivisionError'))
self.assertTrue(lines[0].startswith('Traceback'))
self.assertTrue(lines[1].startswith(' File'))
self.assertStartsWith(lines[3], 'ZeroDivisionError')
self.assertStartsWith(lines[0], 'Traceback')
self.assertStartsWith(lines[1], ' File')
self.assertIn('ZeroDivisionError from None', lines[2])
def test_cause_and_context(self):
@@ -3061,8 +3137,6 @@ class BaseExceptionReportingTests:
report = self.get_report(exc)
self.assertEqual(report, expected)
# TODO: RUSTPYTHON
'''
def test_exception_group_wrapped_naked(self):
# See gh-128799
@@ -3114,7 +3188,7 @@ class BaseExceptionReportingTests:
# remove trailing writespace:
report = '\n'.join([l.rstrip() for l in report.split('\n')])
self.assertEqual(report, expected)
'''
@force_not_colorized_test_class
class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
@@ -3132,6 +3206,10 @@ class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
self.assertEqual(sio.getvalue(), s)
return s
@unittest.expectedFailure # TODO: RUSTPYTHON; Diff is 1103 characters long. Set self.maxDiff to None to see it.
def test_exception_group_wrapped_naked(self):
return super().test_exception_group_wrapped_naked()
@force_not_colorized_test_class
class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
@@ -3357,11 +3435,17 @@ class TestStack(unittest.TestCase):
def test_walk_stack(self):
def deeper():
return list(traceback.walk_stack(None))
s1 = list(traceback.walk_stack(None))
s2 = deeper()
s1, s2 = list(traceback.walk_stack(None)), deeper()
self.assertEqual(len(s2) - len(s1), 1)
self.assertEqual(s2[1:], s1)
def test_walk_innermost_frame(self):
def inner():
return list(traceback.walk_stack(None))
frames = inner()
innermost_frame, _ = frames[0]
self.assertEqual(innermost_frame.f_code.co_name, "inner")
def test_walk_tb(self):
try:
1/0
@@ -3729,6 +3813,7 @@ class TestTracebackException(unittest.TestCase):
self.assertIsNone(te.exc_type)
def test_no_refs_to_exception_and_traceback_objects(self):
exc_obj = None
try:
1/0
except Exception as e:
@@ -3870,8 +3955,8 @@ class TestTracebackException(unittest.TestCase):
exc = traceback.TracebackException(Exception, Exception("haven"), None)
self.assertEqual(list(exc.format()), ["Exception: haven\n"])
# @requires_debug_ranges() # XXX: RUSTPYTHON patch
@unittest.expectedFailure # TODO: RUSTPYTHON; ? ^ +
@requires_debug_ranges()
def test_print(self):
def f():
x = 12
@@ -4788,9 +4873,8 @@ class MiscTest(unittest.TestCase):
def test_all(self):
expected = set()
denylist = {'print_list'}
for name in dir(traceback):
if name.startswith('_') or name in denylist:
if name.startswith('_'):
continue
module_object = getattr(traceback, name)
if getattr(module_object, '__module__', None) == 'traceback':
@@ -4883,6 +4967,8 @@ class MiscTest(unittest.TestCase):
class TestColorizedTraceback(unittest.TestCase):
maxDiff = None
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "y = \x1b[31mx['a']['b']\x1b[0m\x1b[1;31m['c']\x1b[0m" not found in 'Traceback (most recent call last):\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4764\x1b[0m, in \x1b[35mtest_colorized_traceback\x1b[0m\n \x1b[31mbar\x1b[0m\x1b[1;31m()\x1b[0m\n \x1b[31m~~~\x1b[0m\x1b[1;31m^^\x1b[0m\n bar = <function TestColorizedTraceback.test_colorized_traceback.<locals>.bar at 0xb57b09180>\n baz1 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz1 at 0xb57b09e00>\n baz2 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz2 at 0xb57b09cc0>\n e = TypeError("\'NoneType\' object is not subscriptable")\n foo = <function TestColorizedTraceback.test_colorized_traceback.<locals>.foo at 0xb57b08140>\n self = <test.test_traceback.TestColorizedTraceback testMethod=test_colorized_traceback>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4760\x1b[0m, in \x1b[35mbar\x1b[0m\n return baz1(1,\n 2,3\n ,4)\n baz1 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz1 at 0xb57b09e00>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4757\x1b[0m, in \x1b[35mbaz1\x1b[0m\n return baz2(1,2,3,4)\n args = (1, 2, 3, 4)\n baz2 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz2 at 0xb57b09cc0>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4754\x1b[0m, in \x1b[35mbaz2\x1b[0m\n return \x1b[31m(lambda *args: foo(*args))\x1b[0m\x1b[1;31m(1,2,3,4)\x1b[0m\n \x1b[31m~~~~~~~~~~~~~~~~~~~~~~~~~~\x1b[0m\x1b[1;31m^^^^^^^^^\x1b[0m\n args = (1, 2, 3, 4)\n foo = <function TestColorizedTraceback.test_colorized_traceback.<locals>.foo at 0xb57b08140>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4754\x1b[0m, in \x1b[35m<lambda>\x1b[0m\n return (lambda *args: \x1b[31mfoo\x1b[0m\x1b[1;31m(*args)\x1b[0m)(1,2,3,4)\n \x1b[31m~~~\x1b[0m\x1b[1;31m^^^^^^^\x1b[0m\n args = (1, 2, 3, 4)\n foo = <function TestColorizedTraceback.test_colorized_traceback.<locals>.foo at 0xb57b08140>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4751\x1b[0m, in \x1b[35mfoo\x1b[0m\n y = x[\'a\'][\'b\'][\x1b[1;31m\'c\'\x1b[0m]\n \x1b[1;31m^^^\x1b[0m\n args = (1, 2, 3, 4)\n x = {\'a\': {\'b\': None}}\n\x1b[1;35mTypeError\x1b[0m: \x1b[35m\'NoneType\' object is not subscriptable\x1b[0m\n'
def test_colorized_traceback(self):
def foo(*args):
@@ -4906,9 +4992,9 @@ class TestColorizedTraceback(unittest.TestCase):
e, capture_locals=True
)
lines = "".join(exc.format(colorize=True))
red = _colorize.ANSIColors.RED
boldr = _colorize.ANSIColors.BOLD_RED
reset = _colorize.ANSIColors.RESET
red = colors["e"]
boldr = colors["E"]
reset = colors["z"]
self.assertIn("y = " + red + "x['a']['b']" + reset + boldr + "['c']" + reset, lines)
self.assertIn("return " + red + "(lambda *args: foo(*args))" + reset + boldr + "(1,2,3,4)" + reset, lines)
self.assertIn("return (lambda *args: " + red + "foo" + reset + boldr + "(*args)" + reset + ")(1,2,3,4)", lines)
@@ -4925,18 +5011,16 @@ class TestColorizedTraceback(unittest.TestCase):
e, capture_locals=True
)
actual = "".join(exc.format(colorize=True))
red = _colorize.ANSIColors.RED
magenta = _colorize.ANSIColors.MAGENTA
boldm = _colorize.ANSIColors.BOLD_MAGENTA
boldr = _colorize.ANSIColors.BOLD_RED
reset = _colorize.ANSIColors.RESET
expected = "".join([
f' File {magenta}"<string>"{reset}, line {magenta}1{reset}\n',
f' a {boldr}${reset} b\n',
f' {boldr}^{reset}\n',
f'{boldm}SyntaxError{reset}: {magenta}invalid syntax{reset}\n']
)
self.assertIn(expected, actual)
def expected(t, m, fn, l, f, E, e, z):
return "".join(
[
f' File {fn}"<string>"{z}, line {l}1{z}\n',
f' a {E}${z} b\n',
f' {E}^{z}\n',
f'{t}SyntaxError{z}: {m}invalid syntax{z}\n'
]
)
self.assertIn(expected(**colors), actual)
@unittest.expectedFailure # TODO: RUSTPYTHON; ModuleNotFoundError: No module named '_testcapi'
def test_colorized_traceback_is_the_default(self):
@@ -4953,23 +5037,21 @@ class TestColorizedTraceback(unittest.TestCase):
exception_print(e)
actual = tbstderr.getvalue().splitlines()
red = _colorize.ANSIColors.RED
boldr = _colorize.ANSIColors.BOLD_RED
magenta = _colorize.ANSIColors.MAGENTA
boldm = _colorize.ANSIColors.BOLD_MAGENTA
reset = _colorize.ANSIColors.RESET
lno_foo = foo.__code__.co_firstlineno
expected = ['Traceback (most recent call last):',
f' File {magenta}"{__file__}"{reset}, '
f'line {magenta}{lno_foo+5}{reset}, in {magenta}test_colorized_traceback_is_the_default{reset}',
f' {red}foo{reset+boldr}(){reset}',
f' {red}~~~{reset+boldr}^^{reset}',
f' File {magenta}"{__file__}"{reset}, '
f'line {magenta}{lno_foo+1}{reset}, in {magenta}foo{reset}',
f' {red}1{reset+boldr}/{reset+red}0{reset}',
f' {red}~{reset+boldr}^{reset+red}~{reset}',
f'{boldm}ZeroDivisionError{reset}: {magenta}division by zero{reset}']
self.assertEqual(actual, expected)
def expected(t, m, fn, l, f, E, e, z):
return [
'Traceback (most recent call last):',
f' File {fn}"{__file__}"{z}, '
f'line {l}{lno_foo+5}{z}, in {f}test_colorized_traceback_is_the_default{z}',
f' {e}foo{z}{E}(){z}',
f' {e}~~~{z}{E}^^{z}',
f' File {fn}"{__file__}"{z}, '
f'line {l}{lno_foo+1}{z}, in {f}foo{z}',
f' {e}1{z}{E}/{z}{e}0{z}',
f' {e}~{z}{E}^{z}{e}~{z}',
f'{t}ZeroDivisionError{z}: {m}division by zero{z}',
]
self.assertEqual(actual, expected(**colors))
@unittest.expectedFailure # TODO: RUSTPYTHON; Diff is 1795 characters long. Set self.maxDiff to None to see it.
def test_colorized_traceback_from_exception_group(self):
@@ -4988,33 +5070,31 @@ class TestColorizedTraceback(unittest.TestCase):
e, capture_locals=True
)
red = _colorize.ANSIColors.RED
boldr = _colorize.ANSIColors.BOLD_RED
magenta = _colorize.ANSIColors.MAGENTA
boldm = _colorize.ANSIColors.BOLD_MAGENTA
reset = _colorize.ANSIColors.RESET
lno_foo = foo.__code__.co_firstlineno
actual = "".join(exc.format(colorize=True)).splitlines()
expected = [f" + Exception Group Traceback (most recent call last):",
f' | File {magenta}"{__file__}"{reset}, line {magenta}{lno_foo+9}{reset}, in {magenta}test_colorized_traceback_from_exception_group{reset}',
f' | {red}foo{reset}{boldr}(){reset}',
f' | {red}~~~{reset}{boldr}^^{reset}',
f" | e = ExceptionGroup('test', [ZeroDivisionError('division by zero')])",
f" | foo = {foo}",
f' | self = <{__name__}.TestColorizedTraceback testMethod=test_colorized_traceback_from_exception_group>',
f' | File {magenta}"{__file__}"{reset}, line {magenta}{lno_foo+6}{reset}, in {magenta}foo{reset}',
f' | raise ExceptionGroup("test", exceptions)',
f" | exceptions = [ZeroDivisionError('division by zero')]",
f' | {boldm}ExceptionGroup{reset}: {magenta}test (1 sub-exception){reset}',
f' +-+---------------- 1 ----------------',
f' | Traceback (most recent call last):',
f' | File {magenta}"{__file__}"{reset}, line {magenta}{lno_foo+3}{reset}, in {magenta}foo{reset}',
f' | {red}1 {reset}{boldr}/{reset}{red} 0{reset}',
f' | {red}~~{reset}{boldr}^{reset}{red}~~{reset}',
f" | exceptions = [ZeroDivisionError('division by zero')]",
f' | {boldm}ZeroDivisionError{reset}: {magenta}division by zero{reset}',
f' +------------------------------------']
self.assertEqual(actual, expected)
def expected(t, m, fn, l, f, E, e, z):
return [
f" + Exception Group Traceback (most recent call last):",
f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+9}{z}, in {f}test_colorized_traceback_from_exception_group{z}',
f' | {e}foo{z}{E}(){z}',
f' | {e}~~~{z}{E}^^{z}',
f" | e = ExceptionGroup('test', [ZeroDivisionError('division by zero')])",
f" | foo = {foo}",
f' | self = <{__name__}.TestColorizedTraceback testMethod=test_colorized_traceback_from_exception_group>',
f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+6}{z}, in {f}foo{z}',
f' | raise ExceptionGroup("test", exceptions)',
f" | exceptions = [ZeroDivisionError('division by zero')]",
f' | {t}ExceptionGroup{z}: {m}test (1 sub-exception){z}',
f' +-+---------------- 1 ----------------',
f' | Traceback (most recent call last):',
f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+3}{z}, in {f}foo{z}',
f' | {e}1 {z}{E}/{z}{e} 0{z}',
f' | {e}~~{z}{E}^{z}{e}~~{z}',
f" | exceptions = [ZeroDivisionError('division by zero')]",
f' | {t}ZeroDivisionError{z}: {m}division by zero{z}',
f' +------------------------------------',
]
self.assertEqual(actual, expected(**colors))
if __name__ == "__main__":
unittest.main()
+164 -59
View File
@@ -6,16 +6,20 @@ import linecache
import sys
import textwrap
import warnings
from contextlib import suppress
import codeop
import keyword
import tokenize
import io
import _colorize
from _colorize import ANSIColors
from contextlib import suppress
__all__ = ['extract_stack', 'extract_tb', 'format_exception',
'format_exception_only', 'format_list', 'format_stack',
'format_tb', 'print_exc', 'format_exc', 'print_exception',
'print_last', 'print_stack', 'print_tb', 'clear_frames',
'FrameSummary', 'StackSummary', 'TracebackException',
'walk_stack', 'walk_tb']
'walk_stack', 'walk_tb', 'print_list']
#
# Formatting and printing lists of traceback lines.
@@ -183,15 +187,13 @@ def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=
valuestr = _safe_string(value, 'exception')
end_char = "\n" if insert_final_newline else ""
if colorize:
if value is None or not valuestr:
line = f"{ANSIColors.BOLD_MAGENTA}{etype}{ANSIColors.RESET}{end_char}"
else:
line = f"{ANSIColors.BOLD_MAGENTA}{etype}{ANSIColors.RESET}: {ANSIColors.MAGENTA}{valuestr}{ANSIColors.RESET}{end_char}"
theme = _colorize.get_theme(force_color=True).traceback
else:
if value is None or not valuestr:
line = f"{etype}{end_char}"
else:
line = f"{etype}: {valuestr}{end_char}"
theme = _colorize.get_theme(force_no_color=True).traceback
if value is None or not valuestr:
line = f"{theme.type}{etype}{theme.reset}{end_char}"
else:
line = f"{theme.type}{etype}{theme.reset}: {theme.message}{valuestr}{theme.reset}{end_char}"
return line
@@ -384,10 +386,14 @@ 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_back.f_back
while f is not None:
yield f, f.f_lineno
f = f.f_back
f = sys._getframe().f_back
def walk_stack_generator(frame):
while frame is not None:
yield frame, frame.f_lineno
frame = frame.f_back
return walk_stack_generator(f)
def walk_tb(tb):
@@ -531,21 +537,22 @@ class StackSummary(list):
if frame_summary.filename.startswith("<stdin-") and frame_summary.filename.endswith('>'):
filename = "<stdin>"
if colorize:
row.append(' File {}"{}"{}, line {}{}{}, in {}{}{}\n'.format(
ANSIColors.MAGENTA,
filename,
ANSIColors.RESET,
ANSIColors.MAGENTA,
frame_summary.lineno,
ANSIColors.RESET,
ANSIColors.MAGENTA,
frame_summary.name,
ANSIColors.RESET,
)
)
theme = _colorize.get_theme(force_color=True).traceback
else:
row.append(' File "{}", line {}, in {}\n'.format(
filename, frame_summary.lineno, frame_summary.name))
theme = _colorize.get_theme(force_no_color=True).traceback
row.append(
' File {}"{}"{}, line {}{}{}, in {}{}{}\n'.format(
theme.filename,
filename,
theme.reset,
theme.line_no,
frame_summary.lineno,
theme.reset,
theme.frame,
frame_summary.name,
theme.reset,
)
)
if frame_summary._dedented_lines and frame_summary._dedented_lines.strip():
if (
frame_summary.colno is None or
@@ -664,11 +671,11 @@ class StackSummary(list):
for color, group in itertools.groupby(itertools.zip_longest(line, carets, fillvalue=""), key=lambda x: x[1]):
caret_group = list(group)
if color == "^":
colorized_line_parts.append(ANSIColors.BOLD_RED + "".join(char for char, _ in caret_group) + ANSIColors.RESET)
colorized_carets_parts.append(ANSIColors.BOLD_RED + "".join(caret for _, caret in caret_group) + ANSIColors.RESET)
colorized_line_parts.append(theme.error_highlight + "".join(char for char, _ in caret_group) + theme.reset)
colorized_carets_parts.append(theme.error_highlight + "".join(caret for _, caret in caret_group) + theme.reset)
elif color == "~":
colorized_line_parts.append(ANSIColors.RED + "".join(char for char, _ in caret_group) + ANSIColors.RESET)
colorized_carets_parts.append(ANSIColors.RED + "".join(caret for _, caret in caret_group) + ANSIColors.RESET)
colorized_line_parts.append(theme.error_range + "".join(char for char, _ in caret_group) + theme.reset)
colorized_carets_parts.append(theme.error_range + "".join(caret for _, caret in caret_group) + theme.reset)
else:
colorized_line_parts.append("".join(char for char, _ in caret_group))
colorized_carets_parts.append("".join(caret for _, caret in caret_group))
@@ -1086,6 +1093,7 @@ class TracebackException:
self.end_offset = exc_value.end_offset
self.msg = exc_value.msg
self._is_syntax_error = True
self._exc_metadata = getattr(exc_value, "_metadata", None)
elif exc_type and issubclass(exc_type, ImportError) and \
getattr(exc_value, "name_from", None) is not None:
wrong_name = getattr(exc_value, "name_from", None)
@@ -1269,24 +1277,120 @@ class TracebackException:
for ex in self.exceptions:
yield from ex.format_exception_only(show_group=show_group, _depth=_depth+1, colorize=colorize)
def _find_keyword_typos(self):
assert self._is_syntax_error
try:
import _suggestions
except ImportError:
_suggestions = None
# Only try to find keyword typos if there is no custom message
if self.msg != "invalid syntax" and "Perhaps you forgot a comma" not in self.msg:
return
if not self._exc_metadata:
return
line, offset, source = self._exc_metadata
end_line = int(self.lineno) if self.lineno is not None else 0
lines = None
from_filename = False
if source is None:
if self.filename:
try:
with open(self.filename) as f:
lines = f.read().splitlines()
except Exception:
line, end_line, offset = 0,1,0
else:
from_filename = True
lines = lines if lines is not None else self.text.splitlines()
else:
lines = source.splitlines()
error_code = lines[line -1 if line > 0 else 0:end_line]
error_code = textwrap.dedent('\n'.join(error_code))
# Do not continue if the source is too large
if len(error_code) > 1024:
return
error_lines = error_code.splitlines()
tokens = tokenize.generate_tokens(io.StringIO(error_code).readline)
tokens_left_to_process = 10
import difflib
for token in tokens:
start, end = token.start, token.end
if token.type != tokenize.NAME:
continue
# Only consider NAME tokens on the same line as the error
the_end = end_line if line == 0 else end_line + 1
if from_filename and token.start[0]+line != the_end:
continue
wrong_name = token.string
if wrong_name in keyword.kwlist:
continue
# Limit the number of valid tokens to consider to not spend
# to much time in this function
tokens_left_to_process -= 1
if tokens_left_to_process < 0:
break
# Limit the number of possible matches to try
max_matches = 3
matches = []
if _suggestions is not None:
suggestion = _suggestions._generate_suggestions(keyword.kwlist, wrong_name)
if suggestion:
matches.append(suggestion)
matches.extend(difflib.get_close_matches(wrong_name, keyword.kwlist, n=max_matches, cutoff=0.5))
matches = matches[:max_matches]
for suggestion in matches:
if not suggestion or suggestion == wrong_name:
continue
# Try to replace the token with the keyword
the_lines = error_lines.copy()
the_line = the_lines[start[0] - 1][:]
chars = list(the_line)
chars[token.start[1]:token.end[1]] = suggestion
the_lines[start[0] - 1] = ''.join(chars)
code = '\n'.join(the_lines)
# Check if it works
try:
codeop.compile_command(code, symbol="exec", flags=codeop.PyCF_ONLY_AST)
except SyntaxError:
continue
# Keep token.line but handle offsets correctly
self.text = token.line
self.offset = token.start[1] + 1
self.end_offset = token.end[1] + 1
self.lineno = start[0]
self.end_lineno = end[0]
self.msg = f"invalid syntax. Did you mean '{suggestion}'?"
return
def _format_syntax_error(self, stype, **kwargs):
"""Format SyntaxError exceptions (internal helper)."""
# Show exactly where the problem was found.
colorize = kwargs.get("colorize", False)
if colorize:
theme = _colorize.get_theme(force_color=True).traceback
else:
theme = _colorize.get_theme(force_no_color=True).traceback
filename_suffix = ''
if self.lineno is not None:
if colorize:
yield ' File {}"{}"{}, line {}{}{}\n'.format(
ANSIColors.MAGENTA,
self.filename or "<string>",
ANSIColors.RESET,
ANSIColors.MAGENTA,
self.lineno,
ANSIColors.RESET,
)
else:
yield ' File "{}", line {}\n'.format(
self.filename or "<string>", self.lineno)
yield ' File {}"{}"{}, line {}{}{}\n'.format(
theme.filename,
self.filename or "<string>",
theme.reset,
theme.line_no,
self.lineno,
theme.reset,
)
elif self.filename is not None:
filename_suffix = ' ({})'.format(self.filename)
@@ -1295,6 +1399,9 @@ class TracebackException:
# text = " foo\n"
# rtext = " foo"
# ltext = "foo"
with suppress(Exception):
self._find_keyword_typos()
text = self.text
rtext = text.rstrip('\n')
ltext = rtext.lstrip(' \n\f')
spaces = len(rtext) - len(ltext)
@@ -1333,11 +1440,11 @@ class TracebackException:
# colorize from colno to end_colno
ltext = (
ltext[:colno] +
ANSIColors.BOLD_RED + ltext[colno:end_colno] + ANSIColors.RESET +
theme.error_highlight + ltext[colno:end_colno] + theme.reset +
ltext[end_colno:]
)
start_color = ANSIColors.BOLD_RED
end_color = ANSIColors.RESET
start_color = theme.error_highlight
end_color = theme.reset
yield ' {}\n'.format(ltext)
yield ' {}{}{}{}\n'.format(
"".join(caretspace),
@@ -1348,17 +1455,15 @@ class TracebackException:
else:
yield ' {}\n'.format(ltext)
msg = self.msg or "<no detail available>"
if colorize:
yield "{}{}{}: {}{}{}{}\n".format(
ANSIColors.BOLD_MAGENTA,
stype,
ANSIColors.RESET,
ANSIColors.MAGENTA,
msg,
ANSIColors.RESET,
filename_suffix)
else:
yield "{}: {}{}\n".format(stype, msg, filename_suffix)
yield "{}{}{}: {}{}{}{}\n".format(
theme.type,
stype,
theme.reset,
theme.message,
msg,
theme.reset,
filename_suffix,
)
def format(self, *, chain=True, _ctx=None, **kwargs):
"""Format the exception.