diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 96510eeec54640..03b63ab2b81894 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -5273,5 +5273,32 @@ def expected(t, m, fn, l, f, E, e, z): ] self.assertEqual(actual, expected(**colors)) + def test_colorized_traceback_unicode(self): + try: + 啊哈=1; 啊哈/0#### + except Exception as e: + exc = traceback.TracebackException.from_exception(e) + + actual = "".join(exc.format(colorize=True)).splitlines() + def expected(t, m, fn, l, f, E, e, z): + return [ + f" 啊哈=1; {e}啊哈{z}{E}/{z}{e}0{z}####", + f" {e}~~~~{z}{E}^{z}{e}~{z}", + ] + self.assertEqual(actual[2:4], expected(**colors)) + + try: + ééééé/0 + except Exception as e: + exc = traceback.TracebackException.from_exception(e) + + actual = "".join(exc.format(colorize=True)).splitlines() + def expected(t, m, fn, l, f, E, e, z): + return [ + f" {E}ééééé{z}/0", + f" {E}^^^^^{z}", + ] + self.assertEqual(actual[2:4], expected(**colors)) + if __name__ == "__main__": unittest.main() diff --git a/Lib/traceback.py b/Lib/traceback.py index f95d6bdbd016ac..b1fd024884c907 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -680,12 +680,12 @@ def output_line(lineno): colorized_line_parts = [] colorized_carets_parts = [] - for color, group in itertools.groupby(itertools.zip_longest(line, carets, fillvalue=""), key=lambda x: x[1]): + for color, group in itertools.groupby(_zip_display_width(line, carets), key=lambda x: x[1]): caret_group = list(group) - if color == "^": + if "^" in color: 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 == "~": + elif "~" in color: 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: @@ -967,7 +967,15 @@ def setup_positions(expr, force_valid=True): return None -_WIDE_CHAR_SPECIFIERS = "WF" + +def _zip_display_width(line, carets): + import unicodedata + carets = iter(carets) + for char in unicodedata.iter_graphemes(line): + char = str(char) + char_width = _display_width(char) + yield char, "".join(itertools.islice(carets, char_width)) + def _display_width(line, offset=None): """Calculate the extra amount of width space the given source @@ -981,13 +989,9 @@ def _display_width(line, offset=None): if line.isascii(): return offset - import unicodedata - - return sum( - 2 if unicodedata.east_asian_width(char) in _WIDE_CHAR_SPECIFIERS else 1 - for char in line[:offset] - ) + from _pyrepl.utils import wlen + return wlen(line[:offset]) class _ExceptionPrintContext: diff --git a/Misc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst b/Misc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst new file mode 100644 index 00000000000000..981c84a9372d04 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst @@ -0,0 +1 @@ +Fix traceback color output with unicode characters