Skip to content

Commit 72ffc53

Browse files
committed
Fix traceback color output with unicode characters
Closes #130273
1 parent 2db9573 commit 72ffc53

File tree

3 files changed

+51
-10
lines changed

3 files changed

+51
-10
lines changed

Lib/test/test_traceback.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5226,5 +5226,32 @@ def expected(t, m, fn, l, f, E, e, z):
52265226
]
52275227
self.assertEqual(actual, expected(**colors))
52285228

5229+
def test_colorized_traceback_unicode(self):
5230+
try:
5231+
啊哈=1; 啊哈/0####
5232+
except Exception as e:
5233+
exc = traceback.TracebackException.from_exception(e)
5234+
5235+
actual = "".join(exc.format(colorize=True)).splitlines()
5236+
def expected(t, m, fn, l, f, E, e, z):
5237+
return [
5238+
f" 啊哈=1; {e}啊哈{z}{E}/{z}{e}0{z}####",
5239+
f" {e}~~~~{z}{E}^{z}{e}~{z}",
5240+
]
5241+
self.assertEqual(actual[2:4], expected(**colors))
5242+
5243+
try:
5244+
ééééé/0
5245+
except Exception as e:
5246+
exc = traceback.TracebackException.from_exception(e)
5247+
5248+
actual = "".join(exc.format(colorize=True)).splitlines()
5249+
def expected(t, m, fn, l, f, E, e, z):
5250+
return [
5251+
f" {E}ééééé{z}/0",
5252+
f" {E}^^^^^{z}",
5253+
]
5254+
self.assertEqual(actual[2:4], expected(**colors))
5255+
52295256
if __name__ == "__main__":
52305257
unittest.main()

Lib/traceback.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -680,12 +680,12 @@ def output_line(lineno):
680680
colorized_line_parts = []
681681
colorized_carets_parts = []
682682

683-
for color, group in itertools.groupby(itertools.zip_longest(line, carets, fillvalue=""), key=lambda x: x[1]):
683+
for color, group in itertools.groupby(_zip_display_width(line, carets), key=lambda x: x[1]):
684684
caret_group = list(group)
685-
if color == "^":
685+
if "^" in color:
686686
colorized_line_parts.append(theme.error_highlight + "".join(char for char, _ in caret_group) + theme.reset)
687687
colorized_carets_parts.append(theme.error_highlight + "".join(caret for _, caret in caret_group) + theme.reset)
688-
elif color == "~":
688+
elif "~" in color:
689689
colorized_line_parts.append(theme.error_range + "".join(char for char, _ in caret_group) + theme.reset)
690690
colorized_carets_parts.append(theme.error_range + "".join(caret for _, caret in caret_group) + theme.reset)
691691
else:
@@ -967,7 +967,24 @@ def setup_positions(expr, force_valid=True):
967967

968968
return None
969969

970-
_WIDE_CHAR_SPECIFIERS = "WF"
970+
971+
def _lookahead(iterator, default):
972+
forked = itertools.tee(iterator, 1)[0]
973+
return next(forked, default)
974+
975+
976+
def _zip_display_width(line, carets):
977+
line = itertools.tee(line, 1)[0]
978+
carets = iter(carets)
979+
for char in line:
980+
char_width = _display_width(char)
981+
next_char = _lookahead(line, "")
982+
if next_char and char_width == _display_width(char + next_char):
983+
next(line)
984+
yield char + next_char, "".join(itertools.islice(carets, char_width))
985+
else:
986+
yield char, "".join(itertools.islice(carets, char_width))
987+
971988

972989
def _display_width(line, offset=None):
973990
"""Calculate the extra amount of width space the given source
@@ -981,13 +998,9 @@ def _display_width(line, offset=None):
981998
if line.isascii():
982999
return offset
9831000

984-
import unicodedata
985-
986-
return sum(
987-
2 if unicodedata.east_asian_width(char) in _WIDE_CHAR_SPECIFIERS else 1
988-
for char in line[:offset]
989-
)
1001+
from _pyrepl.utils import wlen
9901002

1003+
return wlen(line[:offset])
9911004

9921005

9931006
class _ExceptionPrintContext:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix traceback color output with unicode characters

0 commit comments

Comments
 (0)