Skip to content

gh-130273: Fix traceback color output with unicode characters#142529

Open
grayjk wants to merge 5 commits intopython:mainfrom
grayjk:issue-130273
Open

gh-130273: Fix traceback color output with unicode characters#142529
grayjk wants to merge 5 commits intopython:mainfrom
grayjk:issue-130273

Conversation

@grayjk
Copy link
Copy Markdown
Contributor

@grayjk grayjk commented Dec 10, 2025

Account for the display width of unicode characters so that colors and underlining in traceback output is correct

Closes #130273

@python-cla-bot
Copy link
Copy Markdown

python-cla-bot bot commented Dec 10, 2025

All commit authors signed the Contributor License Agreement.

CLA signed

@vstinner
Copy link
Copy Markdown
Member

@serhiy-storchaka: Here is a PR about text width and Unicode characters :-)

@grayjk
Copy link
Copy Markdown
Contributor Author

grayjk commented Jan 28, 2026

updated to use @serhiy-storchaka's recently added unicodedata.iter_graphemes

@grayjk
Copy link
Copy Markdown
Contributor Author

grayjk commented Feb 18, 2026

@pablogsal @hauntsaninja as recent reviewers of traceback.py, would you mind taking look

@StanFromIreland
Copy link
Copy Markdown
Member

There are conflicts, please fix them.

@grayjk
Copy link
Copy Markdown
Contributor Author

grayjk commented Feb 18, 2026

@StanFromIreland conflicts resolved

Lib/traceback.py Outdated
2 if unicodedata.east_asian_width(char) in _WIDE_CHAR_SPECIFIERS else 1
for char in line[:offset]
)
from _pyrepl.utils import wlen
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to not depend on _pyrepl in the traceback module. I would prefer to move wlen() here, and modify _pyrepl.utils to get it from traceback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved wlen/str_width in commit 467656e and made them private (prefixed with _) to avoid putting them in traceback.__all__ but mypy isn't happy about that. Should I make them public?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are # type: ignore comments in this case okay?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or alternatively I could move wlen to a new support file with a name prefixed with _

@StanFromIreland
Copy link
Copy Markdown
Member

There are conflicts again I'm afraid, and mypy isn't happy either.

return 2


ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should also be private.

Comment on lines +985 to +987
import unicodedata
if ord(c) < 128:
return 1
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to import unicodedata for ASCII characters:

Suggested change
import unicodedata
if ord(c) < 128:
return 1
if ord(c) < 128:
return 1
import unicodedata

Comment on lines +974 to +980
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))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to avoid the heavy unicodedata import for ASCII line?

Suggested change
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 _zip_display_width(line, carets):
carets = iter(carets)
if line.isascii():
for char in line:
yield char, next(carets, "")
else:
import unicodedata
for char in unicodedata.iter_graphemes(line):
char = str(char)
char_width = _display_width(char)
yield char, "".join(itertools.islice(carets, char_width))

I'm not sure that my code is correct :-)

@@ -0,0 +1 @@
Fix traceback color output with unicode characters
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Fix traceback color output with unicode characters
Fix traceback color output with Unicode characters.

@pablogsal
Copy link
Copy Markdown
Member

Claude Fleet — Thorough Review

{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01ND5n5aLjRPCKzQVQMgfjCM","type":"tool_result","content":"diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py\nindex 25d7ac1bd0b..d79c451949f 100644\n--- a/Lib/_pyrepl/utils.py\n+++ b/Lib/_pyrepl/utils.py\n@@ -1,6 +1,5 @@\n from future import annotations\n import builtins\n-import functools\n import keyword\n import re\n import token as T\n@@ -11,12 +10,12 @@\n from collections import deque\n from io import StringIO\n from tokenize import TokenInfo as TI\n+from traceback import _str_width as str_width, _wlen as wlen\n from typing import Iterable, Iterator, Match, NamedTuple, Self\n \n from .types import CharBuffer, CharWidths\n from .trace import trace\n \n-ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@][A-~]")\n ZERO_WIDTH_BRACKET = re.compile(r"\x01.?\x02")\n ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})\n IDENTIFIERS_AFTER = {"def", "class"}\n@@ -59,32 +58,6 @@ class ColorSpan(NamedTuple):\n tag: str\n \n \n-@functools.cache\n-def str_width(c: str) -> int:\n- if ord(c) < 128:\n- return 1\n- # gh-139246 for zero-width joiner and combining characters\n- if unicodedata.combining(c):\n- return 0\n- category = unicodedata.category(c)\n- if category == "Cf" and c != "\u00ad":\n- return 0\n- w = unicodedata.east_asian_width(c)\n- if w in ("N", "Na", "H", "A"):\n- return 1\n- return 2\n-\n-\n-def wlen(s: str) -> int:\n- if len(s) == 1 and s != "\x1a":\n- return str_width(s)\n- length = sum(str_width(i) for i in s)\n- # remove lengths of any escape sequences\n- sequence = ANSI_ESCAPE_SEQUENCE.findall(s)\n- ctrl_z_cnt = s.count("\x1a")\n- return length - sum(len(i) for i in sequence) + ctrl_z_cnt\n-\n-\n def unbracket(s: str, including_content: bool = False) -> str:\n r"""Return s with \001 and \002 characters removed.\n \ndiff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py\nindex 4f7f9d77933..be9f207ef24 100644\n--- a/Lib/test/test_pyrepl/support.py\n+++ b/Lib/test/test_pyrepl/support.py\n@@ -1,12 +1,13 @@\n from code import InteractiveConsole\n from functools import partial\n+from traceback import ANSI_ESCAPE_SEQUENCE\n from typing import Iterable\n from unittest.mock import MagicMock\n \n from _pyrepl.console import Console, Event\n from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig\n from _pyrepl.simple_interact import _strip_final_indent\n-from _pyrepl.utils import unbracket, ANSI_ESCAPE_SEQUENCE\n+from _pyrepl.utils import unbracket\n \n \n class ScreenEqualMixin:\ndiff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py\nindex 3c55b6bdaee..eddc81489b4 100644\n--- a/Lib/test/test_pyrepl/test_utils.py\n+++ b/Lib/test/test_pyrepl/test_utils.py\n@@ -1,52 +1,9 @@\n from unittest import TestCase\n \n-from pyrepl.utils import str_width, wlen, prev_next_window, gen_colors\n+from pyrepl.utils import prev_next_window, gen_colors\n \n \n class TestUtils(TestCase):\n- def test_str_width(self):\n- characters = [\n- 'a',\n- '1',\n- '',\n- '!',\n- '\x1a',\n- '\u263A',\n- '\uffb9',\n- '\N{LATIN SMALL LETTER E WITH ACUTE}', # é\n- '\N{LATIN SMALL LETTER E WITH CEDILLA}', # ȩ\n- '\u00ad',\n- ]\n- for c in characters:\n- self.assertEqual(str_width(c), 1)\n-\n- zero_width_characters = [\n- '\N{COMBINING ACUTE ACCENT}',\n- '\N{ZERO WIDTH JOINER}',\n- ]\n- for c in zero_width_characters:\n- with self.subTest(character=c):\n- self.assertEqual(str_width(c), 0)\n-\n- characters = [chr(99989), chr(99999)]\n- for c in characters:\n- self.assertEqual(str_width(c), 2)\n-\n- def test_wlen(self):\n- for c in ['a', 'b', '1', '!', '']:\n- self.assertEqual(wlen(c), 1)\n- self.assertEqual(wlen('\x1a'), 2)\n-\n- char_east_asian_width_N = chr(3800)\n- self.assertEqual(wlen(char_east_asian_width_N), 1)\n- char_east_asian_width_W = chr(4352)\n- self.assertEqual(wlen(char_east_asian_width_W), 2)\n-\n- self.assertEqual(wlen('hello'), 5)\n- self.assertEqual(wlen('hello' + '\x1a'), 7)\n- self.assertEqual(wlen('e\N{COMBINING ACUTE ACCENT}'), 1)\n- self.assertEqual(wlen('a\N{ZERO WIDTH JOINER}b'), 2)\n-\n def test_prev_next_window(self):\n def gen_normal():\n yield 1\ndiff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py\nindex 99ac7fd83d9..0c55b200872 100644\n--- a/Lib/test/test_traceback.py\n+++ b/Lib/test/test_traceback.py\n@@ -28,6 +28,7 @@\n import traceback\n from functools import partial\n from pathlib import Path\n+from traceback import _str_width, _wlen\n import colorize\n \n MODULE_PREFIX = f'{name}.' if name == 'main' else ''\n@@ -1787,6 +1788,50 @@ def f():\n ]\n self.assertEqual(result_lines, expected)\n \n+ def test_str_width(self):\n+ characters = [\n+ 'a',\n+ '1',\n+ '',\n+ '!',\n+ '\x1a',\n+ '\u263A',\n+ '\uffb9',\n+ '\N{LATIN SMALL LETTER E WITH ACUTE}', # é\n+ '\N{LATIN SMALL LETTER E WITH CEDILLA}', # ȩ\n+ '\u00ad',\n+ ]\n+ for c in characters:\n+ self.assertEqual(_str_width(c), 1)\n+\n+ zero_width_characters = [\n+ '\N{COMBINING ACUTE ACCENT}',\n+ '\N{ZERO WIDTH JOINER}',\n+ ]\n+ for c in zero_width_characters:\n+ with self.subTest(character=c):\n+ self.assertEqual(_str_width(c), 0)\n+\n+ characters = [chr(99989), chr(99999)]\n+ for c in characters:\n+ self.assertEqual(str_width(c), 2)\n+\n+ def test_wlen(self):\n+ for c in ['a', 'b', '1', '!', '']:\n+ self.assertEqual(_wlen(c), 1)\n+ self.assertEqual(_wlen('\x1a'), 2)\n+\n+ char_east_asian_width_N = chr(3800)\n+ self.assertEqual(_wlen(char_east_asian_width_N), 1)\n+ char_east_asian_width_W = chr(4352)\n+ self.assertEqual(_wlen(char_east_asian_width_W), 2)\n+\n+ self.assertEqual(_wlen('hello'), 5)\n+ self.assertEqual(_wlen('hello' + '\x1a'), 7)\n+ self.assertEqual(_wlen('e\N{COMBINING ACUTE ACCENT}'), 1)\n+ self.assertEqual(_wlen('a\N{ZERO WIDTH JOINER}b'), 2)\n+\n+\n class TestKeywordTypoSuggestions(unittest.TestCase):\n TYPO_CASES = [\n ("with block ad something:\n pass", "and"),\n@@ -5321,6 +5366,32 @@ def expected(t, m, fn, l, f, E, e, z):\n ]\n self.assertEqual(actual, expected(colors))\n \n+ def test_colorized_traceback_unicode(self):\n+ try:\n+ 啊哈=1; 啊哈/0####\n+ except Exception as e:\n+ exc = traceback.TracebackException.from_exception(e)\n+\n+ actual = "".join(exc.format(colorize=True)).splitlines()\n+ def expected(t, m, fn, l, f, E, e, z):\n+ return [\n+ f" 啊哈=1; {e}啊哈{z}{E}/{z}{e}0{z}####",\n+ f" {e}~~~~{z}{E}^{z}{e}{z}",\n+ ]\n+ self.assertEqual(actual[2:4], expected(**colors))\n+\n+ try:\n+ ééééé/0\n+ except Exception as e:\n+ exc = traceback.TracebackException.from_exception(e)\n+\n+ actual = "".join(exc.format(colorize=True)).splitlines()\n+ def expected(t, m, fn, l, f, E, e, z):\n+ return [\n+ f" {E}ééééé{z}/0",\n+ f" {E}^^^^^{z}",\n+ ]\n+ self.assertEqual(actual[2:4], expected(**colors))\n \n class TestLazyImportSuggestions(unittest.TestCase):\n """Test that lazy imports are not reified when computing AttributeError suggestions."""\ndiff --git a/Lib/traceback.py b/Lib/traceback.py\nindex 42453b4867c..05cb77b6cac 100644\n--- a/Lib/traceback.py\n+++ b/Lib/traceback.py\n@@ -1,8 +1,10 @@\n """Extract, format and print information about Python stack traces."""\n \n import collections.abc\n+import functools\n import itertools\n import linecache\n+import re\n import sys\n import textwrap\n import types\n@@ -681,12 +683,12 @@ def output_line(lineno):\n colorized_line_parts = []\n colorized_carets_parts = []\n \n- for color, group in itertools.groupby(itertools.zip_longest(line, carets, fillvalue=""), key=lambda x: x[1]):\n+ for color, group in itertools.groupby(_zip_display_width(line, carets), key=lambda x: x[1]):\n caret_group = list(group)\n- if color == "^":\n+ if "^" in color:\n colorized_line_parts.append(theme.error_highlight + "".join(char for char, _ in caret_group) + theme.reset)\n colorized_carets_parts.append(theme.error_highlight + "".join(caret for _, caret in caret_group) + theme.reset)\n- elif color == "":\n+ elif "" in color:\n colorized_line_parts.append(theme.error_range + "".join(char for char, _ in caret_group) + theme.reset)\n colorized_carets_parts.append(theme.error_range + "".join(caret for _, caret in caret_group) + theme.reset)\n else:\n@@ -968,7 +970,46 @@ def setup_positions(expr, force_valid=True):\n \n return None\n \n-_WIDE_CHAR_SPECIFIERS = "WF"\n+\n+def _zip_display_width(line, carets):\n+ import unicodedata\n+ carets = iter(carets)\n+ for char in unicodedata.iter_graphemes(line):\n+ char = str(char)\n+ char_width = _display_width(char)\n+ yield char, "".join(itertools.islice(carets, char_width))\n+\n+\n+@functools.cache\n+def _str_width(c: str) -> int:\n+ import unicodedata\n+ if ord(c) < 128:\n+ return 1\n+ # gh-139246 for zero-width joiner and combining characters\n+ if unicodedata.combining(c):\n+ return 0\n+ category = unicodedata.category(c)\n+ if category == "Cf" and c != "\u00ad":\n+ return 0\n+ w = unicodedata.east_asian_width(c)\n+ if w in ("N", "Na", "H", "A"):\n+ return 1\n+ return 2\n+\n+\n+ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-]")\n+\n+\n+def _wlen(s: str) -> int:\n+ if len(s) == 1 and s != "\x1a":\n+ return _str_width(s)\n+ length = sum(_str_width(i) for i in s)\n+ # remove lengths of any escape sequences\n+ sequence = ANSI_ESCAPE_SEQUENCE.findall(s)\n+ ctrl_z_cnt = s.count("\x1a")\n+ return length - sum(len(i) for i in sequence) + ctrl_z_cnt\n+\n+\n \n def _display_width(line, offset=None):\n """Calculate the extra amount of width space the given source\n@@ -982,13 +1023,7 @@ def _display_width(line, offset=None):\n if line.isascii():\n return offset\n \n- import unicodedata\n-\n- return sum(\n- 2 if unicodedata.east_asian_width(char) in _WIDE_CHAR_SPECIFIERS else 1\n- for char in line[:offset]\n- )\n-\n+ return _wlen(line[:offset])\n \n \n class _ExceptionPrintContext:\ndiff --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\nnew file mode 100644\nindex 00000000000..981c84a9372\n--- /dev/null\n+++ b/Misc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst\n@@ -0,0 +1 @@\n+Fix traceback color output with unicode characters","is_error":false}]},"parent_tool_use_id":null,"session_id":"3908ee0f-12b4-4e41-8375-a87d274a0a75","uuid":"4d3d1d02-0640-489f-8a51-78746049e89d","timestamp":"2026-04-04T22:06:21.456Z","tool_use_result":{"stdout":"diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py\nindex 25d7ac1bd0b..d79c451949f 100644\n--- a/Lib/_pyrepl/utils.py\n+++ b/Lib/_pyrepl/utils.py\n@@ -1,6 +1,5 @@\n from future import annotations\n import builtins\n-import functools\n import keyword\n import re\n import token as T\n@@ -11,12 +10,12 @@\n from collections import deque\n from io import StringIO\n from tokenize import TokenInfo as TI\n+from traceback import _str_width as str_width, _wlen as wlen\n from typing import Iterable, Iterator, Match, NamedTuple, Self\n \n from .types import CharBuffer, CharWidths\n from .trace import trace\n \n-ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@][A-~]")\n ZERO_WIDTH_BRACKET = re.compile(r"\x01.?\x02")\n ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})\n IDENTIFIERS_AFTER = {"def", "class"}\n@@ -59,32 +58,6 @@ class ColorSpan(NamedTuple):\n tag: str\n \n \n-@functools.cache\n-def str_width(c: str) -> int:\n- if ord(c) < 128:\n- return 1\n- # gh-139246 for zero-width joiner and combining characters\n- if unicodedata.combining(c):\n- return 0\n- category = unicodedata.category(c)\n- if category == "Cf" and c != "\u00ad":\n- return 0\n- w = unicodedata.east_asian_width(c)\n- if w in ("N", "Na", "H", "A"):\n- return 1\n- return 2\n-\n-\n-def wlen(s: str) -> int:\n- if len(s) == 1 and s != "\x1a":\n- return str_width(s)\n- length = sum(str_width(i) for i in s)\n- # remove lengths of any escape sequences\n- sequence = ANSI_ESCAPE_SEQUENCE.findall(s)\n- ctrl_z_cnt = s.count("\x1a")\n- return length - sum(len(i) for i in sequence) + ctrl_z_cnt\n-\n-\n def unbracket(s: str, including_content: bool = False) -> str:\n r"""Return s with \001 and \002 characters removed.\n \ndiff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py\nindex 4f7f9d77933..be9f207ef24 100644\n--- a/Lib/test/test_pyrepl/support.py\n+++ b/Lib/test/test_pyrepl/support.py\n@@ -1,12 +1,13 @@\n from code import InteractiveConsole\n from functools import partial\n+from traceback import ANSI_ESCAPE_SEQUENCE\n from typing import Iterable\n from unittest.mock import MagicMock\n \n from _pyrepl.console import Console, Event\n from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig\n from _pyrepl.simple_interact import _strip_final_indent\n-from _pyrepl.utils import unbracket, ANSI_ESCAPE_SEQUENCE\n+from _pyrepl.utils import unbracket\n \n \n class ScreenEqualMixin:\ndiff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py\nindex 3c55b6bdaee..eddc81489b4 100644\n--- a/Lib/test/test_pyrepl/test_utils.py\n+++ b/Lib/test/test_pyrepl/test_utils.py\n@@ -1,52 +1,9 @@\n from unittest import TestCase\n \n-from pyrepl.utils import str_width, wlen, prev_next_window, gen_colors\n+from pyrepl.utils import prev_next_window, gen_colors\n \n \n class TestUtils(TestCase):\n- def test_str_width(self):\n- characters = [\n- 'a',\n- '1',\n- '',\n- '!',\n- '\x1a',\n- '\u263A',\n- '\uffb9',\n- '\N{LATIN SMALL LETTER E WITH ACUTE}', # é\n- '\N{LATIN SMALL LETTER E WITH CEDILLA}', # ȩ\n- '\u00ad',\n- ]\n- for c in characters:\n- self.assertEqual(str_width(c), 1)\n-\n- zero_width_characters = [\n- '\N{COMBINING ACUTE ACCENT}',\n- '\N{ZERO WIDTH JOINER}',\n- ]\n- for c in zero_width_characters:\n- with self.subTest(character=c):\n- self.assertEqual(str_width(c), 0)\n-\n- characters = [chr(99989), chr(99999)]\n- for c in characters:\n- self.assertEqual(str_width(c), 2)\n-\n- def test_wlen(self):\n- for c in ['a', 'b', '1', '!', '']:\n- self.assertEqual(wlen(c), 1)\n- self.assertEqual(wlen('\x1a'), 2)\n-\n- char_east_asian_width_N = chr(3800)\n- self.assertEqual(wlen(char_east_asian_width_N), 1)\n- char_east_asian_width_W = chr(4352)\n- self.assertEqual(wlen(char_east_asian_width_W), 2)\n-\n- self.assertEqual(wlen('hello'), 5)\n- self.assertEqual(wlen('hello' + '\x1a'), 7)\n- self.assertEqual(wlen('e\N{COMBINING ACUTE ACCENT}'), 1)\n- self.assertEqual(wlen('a\N{ZERO WIDTH JOINER}b'), 2)\n-\n def test_prev_next_window(self):\n def gen_normal():\n yield 1\ndiff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py\nindex 99ac7fd83d9..0c55b200872 100644\n--- a/Lib/test/test_traceback.py\n+++ b/Lib/test/test_traceback.py\n@@ -28,6 +28,7 @@\n import traceback\n from functools import partial\n from pathlib import Path\n+from traceback import _str_width, _wlen\n import colorize\n \n MODULE_PREFIX = f'{name}.' if name == 'main' else ''\n@@ -1787,6 +1788,50 @@ def f():\n ]\n self.assertEqual(result_lines, expected)\n \n+ def test_str_width(self):\n+ characters = [\n+ 'a',\n+ '1',\n+ '',\n+ '!',\n+ '\x1a',\n+ '\u263A',\n+ '\uffb9',\n+ '\N{LATIN SMALL LETTER E WITH ACUTE}', # é\n+ '\N{LATIN SMALL LETTER E WITH CEDILLA}', # ȩ\n+ '\u00ad',\n+ ]\n+ for c in characters:\n+ self.assertEqual(_str_width(c), 1)\n+\n+ zero_width_characters = [\n+ '\N{COMBINING ACUTE ACCENT}',\n+ '\N{ZERO WIDTH JOINER}',\n+ ]\n+ for c in zero_width_characters:\n+ with self.subTest(character=c):\n+ self.assertEqual(_str_width(c), 0)\n+\n+ characters = [chr(99989), chr(99999)]\n+ for c in characters:\n+ self.assertEqual(str_width(c), 2)\n+\n+ def test_wlen(self):\n+ for c in ['a', 'b', '1', '!', '']:\n+ self.assertEqual(_wlen(c), 1)\n+ self.assertEqual(_wlen('\x1a'), 2)\n+\n+ char_east_asian_width_N = chr(3800)\n+ self.assertEqual(_wlen(char_east_asian_width_N), 1)\n+ char_east_asian_width_W = chr(4352)\n+ self.assertEqual(_wlen(char_east_asian_width_W), 2)\n+\n+ self.assertEqual(_wlen('hello'), 5)\n+ self.assertEqual(_wlen('hello' + '\x1a'), 7)\n+ self.assertEqual(_wlen('e\N{COMBINING ACUTE ACCENT}'), 1)\n+ self.assertEqual(_wlen('a\N{ZERO WIDTH JOINER}b'), 2)\n+\n+\n class TestKeywordTypoSuggestions(unittest.TestCase):\n TYPO_CASES = [\n ("with block ad something:\n pass", "and"),\n@@ -5321,6 +5366,32 @@ def expected(t, m, fn, l, f, E, e, z):\n ]\n self.assertEqual(actual, expected(colors))\n \n+ def test_colorized_traceback_unicode(self):\n+ try:\n+ 啊哈=1; 啊哈/0####\n+ except Exception as e:\n+ exc = traceback.TracebackException.from_exception(e)\n+\n+ actual = "".join(exc.format(colorize=True)).splitlines()\n+ def expected(t, m, fn, l, f, E, e, z):\n+ return [\n+ f" 啊哈=1; {e}啊哈{z}{E}/{z}{e}0{z}####",\n+ f" {e}~~~~{z}{E}^{z}{e}{z}",\n+ ]\n+ self.assertEqual(actual[2:4], expected(**colors))\n+\n+ try:\n+ ééééé/0\n+ except Exception as e:\n+ exc = traceback.TracebackException.from_exception(e)\n+\n+ actual = "".join(exc.format(colorize=True)).splitlines()\n+ def expected(t, m, fn, l, f, E, e, z):\n+ return [\n+ f" {E}ééééé{z}/0",\n+ f" {E}^^^^^{z}",\n+ ]\n+ self.assertEqual(actual[2:4], expected(**colors))\n \n class TestLazyImportSuggestions(unittest.TestCase):\n """Test that lazy imports are not reified when computing AttributeError suggestions."""\ndiff --git a/Lib/traceback.py b/Lib/traceback.py\nindex 42453b4867c..05cb77b6cac 100644\n--- a/Lib/traceback.py\n+++ b/Lib/traceback.py\n@@ -1,8 +1,10 @@\n """Extract, format and print information about Python stack traces."""\n \n import collections.abc\n+import functools\n import itertools\n import linecache\n+import re\n import sys\n import textwrap\n import types\n@@ -681,12 +683,12 @@ def output_line(lineno):\n colorized_line_parts = []\n colorized_carets_parts = []\n \n- for color, group in itertools.groupby(itertools.zip_longest(line, carets, fillvalue=""), key=lambda x: x[1]):\n+ for color, group in itertools.groupby(_zip_display_width(line, carets), key=lambda x: x[1]):\n caret_group = list(group)\n- if color == "^":\n+ if "^" in color:\n colorized_line_parts.append(theme.error_highlight + "".join(char for char, _ in caret_group) + theme.reset)\n colorized_carets_parts.append(theme.error_highlight + "".join(caret for _, caret in caret_group) + theme.reset)\n- elif color == "":\n+ elif "" in color:\n colorized_line_parts.append(theme.error_range + "".join(char for char, _ in caret_group) + theme.reset)\n colorized_carets_parts.append(theme.error_range + "".join(caret for _, caret in caret_group) + theme.reset)\n else:\n@@ -968,7 +970,46 @@ def setup_positions(expr, force_valid=True):\n \n return None\n \n-_WIDE_CHAR_SPECIFIERS = "WF"\n+\n+def _zip_display_width(line, carets):\n+ import unicodedata\n+ carets = iter(carets)\n+ for char in unicodedata.iter_graphemes(line):\n+ char = str(char)\n+ char_width = _display_width(char)\n+ yield char, "".join(itertools.islice(carets, char_width))\n+\n+\n+@functools.cache\n+def _str_width(c: str) -> int:\n+ import unicodedata\n+ if ord(c) < 128:\n+ return 1\n+ # gh-139246 for zero-width joiner and combining characters\n+ if unicodedata.combining(c):\n+ return 0\n+ category = unicodedata.category(c)\n+ if category == "Cf" and c != "\u00ad":\n+ return 0\n+ w = unicodedata.east_asian_width(c)\n+ if w in ("N", "Na", "H", "A"):\n+ return 1\n+ return 2\n+\n+\n+ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-]")\n+\n+\n+def _wlen(s: str) -> int:\n+ if len(s) == 1 and s != "\x1a":\n+ return _str_width(s)\n+ length = sum(_str_width(i) for i in s)\n+ # remove lengths of any escape sequences\n+ sequence = ANSI_ESCAPE_SEQUENCE.findall(s)\n+ ctrl_z_cnt = s.count("\x1a")\n+ return length - sum(len(i) for i in sequence) + ctrl_z_cnt\n+\n+\n \n def _display_width(line, offset=None):\n """Calculate the extra amount of width space the given source\n@@ -982,13 +1023,7 @@ def _display_width(line, offset=None):\n if line.isascii():\n return offset\n \n- import unicodedata\n-\n- return sum(\n- 2 if unicodedata.east_asian_width(char) in _WIDE_CHAR_SPECIFIERS else 1\n- for char in line[:offset]\n- )\n-\n+ return _wlen(line[:offset])\n \n \n class _ExceptionPrintContext:\ndiff --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\nnew file mode 100644\nindex 00000000000..981c84a9372\n--- /dev/null\n+++ b/Misc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst\n@@ -0,0 +1 @@\n+Fix traceback color output with unicode characters","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01EPJS4FbBC5YGKPupwZYhMH","type":"tool_result","content":" Lib/_pyrepl/utils.py | 29 +--------\n Lib/test/test_pyrepl/support.py | 3 +-\n Lib/test/test_pyrepl/test_utils.py | 45 +-------------\n Lib/test/test_traceback.py | 71 ++++++++++++++++++++++\n Lib/traceback.py | 57 +++++++++++++----\n .../2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst | 1 +\n 6 files changed, 122 insertions(+), 84 deletions(-)","is_error":false}]},"parent_tool_use_id":null,"session_id":"3908ee0f-12b4-4e41-8375-a87d274a0a75","uuid":"6d98ea89-2e18-4038-91ed-a179b5c87d3b","timestamp":"2026-04-04T22:06:22.046Z","tool_use_result":{"stdout":" Lib/_pyrepl/utils.py | 29 +--------\n Lib/test/test_pyrepl/support.py | 3 +-\n Lib/test/test_pyrepl/test_utils.py | 45 +-------------\n Lib/test/test_traceback.py | 71 ++++++++++++++++++++++\n Lib/traceback.py | 57 +++++++++++++----\n .../2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst | 1 +\n 6 files changed, 122 insertions(+), 84 deletions(-)","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01EpBjbPHnZeus5qwzpPjmzD","type":"tool_result","content":"M\tLib/_pyrepl/utils.py\nM\tLib/test/test_pyrepl/support.py\nM\tLib/test/test_pyrepl/test_utils.py\nM\tLib/test/test_traceback.py\nM\tLib/traceback.py\nA\tMisc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst","is_error":false}]},"parent_tool_use_id":null,"session_id":"3908ee0f-12b4-4e41-8375-a87d274a0a75","uuid":"9d6bffd3-fdec-49a0-a8a0-7cf9017535f8","timestamp":"2026-04-04T22:06:22.863Z","tool_use_result":{"stdout":"M\tLib/_pyrepl/utils.py\nM\tLib/test/test_pyrepl/support.py\nM\tLib/test/test_pyrepl/test_utils.py\nM\tLib/test/test_traceback.py\nM\tLib/traceback.py\nA\tMisc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01TRfTrGi3yvo73mr6YxiHEd","type":"tool_result","content":"\nOutput too large (48.2KB). Full output saved to: /home/pablogsal/.claude/projects/-tmp-claude-fleet-b4704ad8/3908ee0f-12b4-4e41-8375-a87d274a0a75/tool-results/b43d0q3of.txt\n\nPreview (first 2KB):\n4ff8b07a3d9 gh-142927: Show self time in flamegraph tooltip (#147706)\nc43b490ca3f gh-145000: Find correct merge base in reusable-check-html-ids.yml workflow (#147975)\n8bf8bf92921 gh-73613: Support Base32 and Base64 without padding (GH-147974)\n75be902a13c Docs: Standardize documentation authors (#148102)\n21fb9dc71d2 gh-146527: Heap-allocate gc_stats to avoid bloating PyInterpreterState (#148057)\nb1d2d9829cf Docs: Fix a typo in the 'Non-ASCII characters in names' section (#148043)\nfbdbea949f0 Regex HOWTO: invalid string literals result in SyntaxWarning (#148092)\n853dafe23a1 gh-148083: Prevent constant folding when lhs is container types (gh-148090)\nc398490fbf1 gh-148074: Fix typeobject.c missing error return (#148075)\n289f19adb0a gh-148083: Constant-fold _CONTAINS_OP_SET for frozenset (gh-148084)\nfe9befc1ca7 gh-145883: Fix two heap-buffer-overflows in _zoneinfo (#145885)\n328da67b2c7 gh-146073: Revert "gh-146073: Add fitness/exit quality mechanism for JIT trace frontend (GH-147966)" (#148082)\n611d606a189 gh-148067: Fix typo in asyncio event loop docs: 'signals' -> 'signal' (#148073)\nc50d6cd0121 gh-148078: Fix uses of sym_is_not_null in JIT optimizer (GH-148079)\ne7bf8eac0fe gh-131798: split recursion check to _CHECK_RECURSION_LIMIT and combine checks (GH-148070)\n113038f9407 gh-148016: Improve "Leading padding not allowed" error in Base32 and Base64 decoders (GH-148017)\n985216ced30 gh-148029: Fix error message for invalid number of Base32 characters (GH-148030)\n7bcc1c49205 gh-131798: relax GUARD_CALLABLE checks for self type checks (#148069)\n7e275d49658 gh-131798: JIT inline function addresses of builtin methods (#146906)\nb8470deb5d5 gh-146450: Normalise feature set of Android build script with other platform build scripts (#146451)\n848bbe9ff21 gh-146541: Allow building the Android testbed for 32-bit targets (#146542)\ndea4083aa95 GH-146210: Fix building the jit stencils on Windows when the interpreter is built with a different clang version (#146338)\n...\n","is_error":false}]},"parent_tool_use_id":null,"session_id":"3908ee0f-12b4-4e41-8375-a87d274a0a75","uuid":"3865a36e-f415-4f97-a426-0b700e8d93d3","timestamp":"2026-04-04T22:06:23.415Z","tool_use_result":{"stdout":"4ff8b07a3d9 gh-142927: Show self time in flamegraph tooltip (#147706)\nc43b490ca3f gh-145000: Find correct merge base in reusable-check-html-ids.yml workflow (#147975)\n8bf8bf92921 gh-73613: Support Base32 and Base64 without padding (GH-147974)\n75be902a13c Docs: Standardize documentation authors (#148102)\n21fb9dc71d2 gh-146527: Heap-allocate gc_stats to avoid bloating PyInterpreterState (#148057)\nb1d2d9829cf Docs: Fix a typo in the 'Non-ASCII characters in names' section (#148043)\nfbdbea949f0 Regex HOWTO: invalid string literals result in SyntaxWarning (#148092)\n853dafe23a1 gh-148083: Prevent constant folding when lhs is container types (gh-148090)\nc398490fbf1 gh-148074: Fix typeobject.c missing error return (#148075)\n289f19adb0a gh-148083: Constant-fold _CONTAINS_OP_SET for frozenset (gh-148084)\nfe9befc1ca7 gh-145883: Fix two heap-buffer-overflows in _zoneinfo (#145885)\n328da67b2c7 gh-146073: Revert "gh-146073: Add fitness/exit quality mechanism for JIT trace frontend (GH-147966)" (#148082)\n611d606a189 gh-148067: Fix typo in asyncio event loop docs: 'signals' -> 'signal' (#148073)\nc50d6cd0121 gh-148078: Fix uses of sym_is_not_null in JIT optimizer (GH-148079)\ne7bf8eac0fe gh-131798: split recursion check to _CHECK_RECURSION_LIMIT and combine checks (GH-148070)\n113038f9407 gh-148016: Improve "Leading padding not allowed" error in Base32 and Base64 decoders (GH-148017)\n985216ced30 gh-148029: Fix error message for invalid number of Base32 characters (GH-148030)\n7bcc1c49205 gh-131798: relax GUARD_CALLABLE checks for self type checks (#148069)\n7e275d49658 gh-131798: JIT inline function addresses of builtin methods (#146906)\nb8470deb5d5 gh-146450: Normalise feature set of Android build script with other platform build scripts (#146451)\n848bbe9ff21 gh-146541: Allow building the Android testbed for 32-bit targets (#146542)\ndea4083aa95 GH-146210: Fix building the jit stencils on Windows when the interpreter is built with a different clang version (#146338)\n80d0a85d969 gh-126676: Expand argparse docs for type=bool with warning and alternatives (#146435)\ncb76ab3819f gh-145098: Use macos-15-intel instead of unstable macos-26-intel (#148038)\n198b04b75f7 gh-146073: Add fitness/exit quality mechanism for JIT trace frontend (GH-147966)\n3681d47a440 gh-143930: Tweak the exception message and increase test coverage (GH-146476)\n48317feec81 gh-146640: Optimize int operations by mutating uniquely-referenced operands in place (JIT only) (GH-146641)\n80ab6d958a0 gh-125895: Fix static asset location for sphinx-notfound-page (#147984)\nedab6860a7d gh-148022: Add threat model to remote debugging docs (#148024)\n3908593039b test_copy_dir_no_read_permission requires non root user (GH-148028)\nf3b74d62698 gh-80642: timeit - make target time of autorange configurable (#140283)\n9d087d6d41e GH-147985: Use lock-free lookup in PySet_Contains (#147986)\n617f4cc1c26 gh-142533: Document CRLF injection vulnerabilities in http.server doc (#143395)\nc1b20a6d96e GH-145247: Use _PyTuple_FromPair in Modules (part 2) (#148003)\n0c100664bc4 GH-145247: Use _PyTuple_FromPair in Modules (part 1) (#148000)\n74a82a29ba0 gh-139038: Update JIT results in preparation for 3.15a8 (GH-148018)\n86b86177476 Move root user checks to test.support (#146195)\n9b08f8c56f8 GH-126910: Revert "Make _Py_get_machine_stack_pointer return the stack pointer (#147945)" (GH-147994)\nfc7a188fe70 gh-146613: Fix re-entrant use-after-free in itertools._grouper (#147962)\nc1a4112c225 gh-147988: Initialize digits in long_alloc() in debug mode (#147989)\na86963b3e2a gh-146636: Py_mod_abi mandatory for modules created from slots array (GH-146855)\n97babb8ef70 gh-143394: Skip pyrepl test_no_newline() basic REPL if readline is missing (#147973)\n7817651b21f gh-146444: Remove legacy iOS folder (#147993)\nc32e264227b gh-146656: properly raise NotImplementedError instead of NotImplemented in test_discover (#146661)\nb456cb25a98 gh-143050: Add helper _PyLong_InitTag() (#147956)\n4810beddbfc gh-147960: Fix memory leak in crossinterp _tuple_shared() on alloc failure (#147961)\n021ee7fd6a0 Docs: Use !r in repr example (gh-146273)\n255026d9eea GH-126910: Make _Py_get_machine_stack_pointer return the stack pointer (#147945)\n9e5b8383724 gh-146488: hash-pin check-html-ids action references (#147968)\n03f3b9ade97 gh-146907: Clarify ABI compatibility between debug and release builds (GH-146925)\n24523240013 gh-146636: PEP 803: add Py_TARGET_ABI3T and .abi3t.so extension (GH-146637)\n08c5d3d4234 gh-145000: Run check-html-ids.py in CI (#145632)\n07d5d724849 Increase dependabot interval to quarterly (#147959)\na504c0a590b gh-146488: hash-pin all action references\nb38cfb7bcaa gh-146479: Skip test_frame_pointer_unwind for gcc -O3 --enable-shared (#147947)\n473d2a35ced gh-147944: Increase range of bytes_per_sep (GH-147946)\n42825e63870 gh-146431: Add the wrapcol and ignorechars parameters for more Base XX codec (GH-146434)\n4497cf3aed5 build(deps): bump qs from 6.14.0 to 6.15.0 in /Platforms/emscripten/browser_test (#146048)\n9e1f1644cd7 Docs: fill in descriptor C API docs (GH-146644)\n62a6e898e01 gh-147856: Allow the 'count' argument of bytes.replace() to be a keyword (#147943)\n362145c20eb gh-139109: Document the trace recording interpreter in internaldocs (GH-146110)\ndb5936c5b89 gh-143050: Correct PyLong_FromString() to use _PyLong_Negate() (#145901)\n829e4d0b14e gh-141510: Support frozendict in plistlib (#145590)\n67354b2925e gh-145563: Add thread-safety annotation for PyCapsule C-API (#146612)\nb4fac15613a gh-145458: use self.skip_idle consistently in the tachyon profiler (#145459)\nbbf7fb2c15a gh-146615: Fix format specifiers in Objects/ directory (GH-146620)\ndcb260eff2d gh-146615: Fix format specifiers in Python/ directory (GH-146619)\nb7055533abc gh-146615: Fix format specifiers in test cextensions (GH-146618)\n1c396e18218 gh-146615: Fix format specifiers in extension modules (GH-146617)\n72d29ea363f gh-146615: Fix crash in get() for METH_METHOD descriptors with invalid type argument (GH-146634)\n70d1b08a4bb gh-146376: Reduce timeout in Emscripten GHA workflow (#146378)\nca95e979d6c gh-146442: Fix various bugs in compiler pipeline (#146443)\ne79fd603392 gh-146238: Support half-floats in the array module (#146242)\nec2bc12e549 gh-145056: Add support for merging collections.UserDict and frozendict (GH-146465)\n727890410b7 gh-145056: Fix merging of collections.OrderedDict and frozendict (GH-146466)\na25e09721af gh-139633: Run netrc file permission check only once per parse (GH-139634)\nafd8113e9d7 gh-144270: Make SubElement parent and tag positional-only (GH-144845)\n8e9d21c64b6 gh-146558: JIT optimize dict access for objects with known hash (#146559)\n76c554bcdf5 gh-139922: always run MSVC 64-bit tail-calling CI (GH-146570)\nadf2c47911b gh-126835: Fix _PY_IS_SMALL_INT() macro (#146631)\n8e10bd77ea0 gh-146587: fix type slot assignment incase of multiple slots for same name (#146593)\nf4d3c61f367 gh-138122: Add differential flame graph (#145785)\n6857de625f1 gh-146416: Emscripten: Improve standard stream handling in node_entry.mjs (#146417)\n6420847bdaa gh-146444: Don't package as part of iOS 'build hosts' target (#146628)\n7b2495e8111 gh-138850: Add --disable-epoll to configure (GH-145768)\n382c04308d7 gh-146444: Make Platforms/Apple/ compatible with Python 3.9 (#146624)\n7f0c4f6a0b1 gh-138577: Fix keyboard shortcuts in getpass with echo_char (#141597)\n2cf6a68f028 gh-146556: Fix infinite loop in annotationlib.get_annotations() on circular wrapped (#146557)\n4d0e8ee649c gh-146579: _zstd: Fix decompression options dict error message (#146577)\ne39d84a37df gh-146083: Upgrade bundled Expat to 2.7.5 (#146085)\n24db78c5329 gh-146080: fix a crash in SNI callbacks when the SSL object is gone (#146573)\naa6680775d6 gh-146090: fix memory management of internal sqlite3 callback contexts (#146569)\n6932c3ee6ad gh-145876: Do not mask KeyErrors raised during dictionary unpacking in call (GH-146472)\n1af025dd220 gh-146004: fix test_args_from_interpreter_flags on windows (#146580)\n1fd66eadd25 gh-139003: Use frozenset for module level attributes in _pyrepl.utils (#139004)\n5bf3a31bc23 GH-146527: Add more data to GC statistics and add it to PyDebugOffsets (#146532)\n087a5f6444f _pyrepl/pager.py: call less with --clear-screen (#146382)\n578d726d467 gh-146544: Fix asyncio.Queue docstring ambiguity (#146545)\n3ff582238fd Docs: don't rely on implicit 'above' directions in socket docs (#146426)\n1efe441de7c gh-146004: propagate all -X options to multiprocessing child processes (GH-146005)\n5992238986d gh-146381: Constant-fold frozendict subscript lookups via REPLACE_OPCODE_IF_EVALUATES_PURE (gh-146490)\na933e9ccee6 GH-145247: Use _PyTuple_FromPair[Steal] in Objects (#145884)\na5b9d60a69d gh-145059: Record lazy modules without submodules in sys.lazy_modules (#146081)\n69b08c397b9 gh-145057: Fix test names and comments to reflect sys.lazy_modules is a dict, not a set (#146084)\n73cc1fd4f45 gh-146310: Fix ensurepip to treat empty WHEEL_PKG_DIR as unset (#146357)\n1384f025f5e gh-126910: Verify that JIT stencils preserve frame pointer (GH-146524)\nb60b9261b5d GH-126910: avoid reading the FP for getting the SP (GH-146521)\n6763d26b2ba GH-126910: reserve FP on AArch64 when generating JIT stencils (GH-146520)\nf89fafed25e Mention _Float16 (type from Annex H of the C23) in the struct docs (#146243)\na492d9ff746 gh-145866: Convert LIST_EXTEND to leave its inputs on the stack to be cleaned up by _POP_TOP be cleaned up by _POP_TOP (GH-146383)\n1417737810e gh-146507: cache the buffer size for SelectorTransport.get_write_buffer_size() (#146518)\nc8ee196030b gh-146388: Add null check for sym_new(ctx) in make_bottom (GH-146389)\na209dd2bdb5 gh-85332: Support cross-compiling for GNU/Hurd (#146094)\n9214e3f33ee gh-123471: Make itertools.zip_longest safe in the FT build (#146033)\n0de4e08a599 gh-146250: Fix memory leak in re-initialization of SyntaxError (#146251)\n5466f57eadd gh-142518: add thread safety annotations for bytearray C-API (#146514)\n6a94980301b gh-142518: add thread safety docs on bytes C-API (#146415)\n2e64e36a2b1 GH-139922: update 3.15 whatsnew: Windows 64-bit binaries now use the tail-calling interpreter (#146391)\nd4153a9f767 gh-146480: Override the exception in _PyErr_SetKeyError() (#146486)\nd0061d50b0f gh-146440: Add array_hook parameter to JSON decoders (#146441)\n256907d3aac gh-138580: Revert sys.float_info.iec_60559 boolean flag (#146501)\n0c7a75aeef4 gh-146059: Cleanup pickle fast_save_enter() test (#146481)\n36e4ffc1731 gh-146444: Move the Apple folder to the Platforms directory (#146497)\n5684b3a04c6 gh-146498: Ensure binary content is correctly processed in multi-arch iOS XCframeworks (#146499)\nc75d220e257 gh-146410: Add --pythoninfo to make testios (#146494)\nca6dfa0f311 gh-146446: Miscellaneous improvements to iOS XCframework build script (#146447)\n17070f41d4c gh-144837: Improve documentation for more collection methods (GH-144841)\n5c0dcb3e0d8 gh-146059: Call fast_save_leave() in pickle save_frozenset() (#146173)\ne44993a6654 gh-146318: Document that signal.SIGSTOP is Unix-only (#146319)\n0e3b3b895c8 gh-146151: Add support for complex arrays in the array module (#146237)\nc68a19448a9 gh-146151: memoryview supports 'F' and 'D' format types (complex) (#146241)\nbfdaa3c5657 gh-131798: JIT: split call method and call builtin opcodes into smaller uops (#146463)\n0e543055b01 gh-145876: Do not mask AttributeErrors raised during dictionary unpacking (GH-145906)\ne1d48237973 gh-145650: Add logging.{Formatter,Filter}.__repr__ (GH-145652)\n3a2b81e9191 gh-145616: Detect Android sysconfig ABI correctly on 32-bit ARM Android on 64-bit ARM kernel (GH-145617)\n1516c263999 gh-145866: Convert DICT_UPDATE to leave its inputs on the stack to be cleaned up by _POP_TOP (GH-146190)\n8de70b31c59 gh-145633: Fix struct.pack('f') on s390x (#146422)\n1038a3ab2e0 gh-146410: Run tests with with --pythoninfo on iOS (#146457)\nfc1f6b176e2 gh-GH-131798: optimize jit attribute loads on immutable types (#146449)\nb5a27bdcf03 gh-146410: Add --pythoninfo option to regrtest (#146413)\nf5364ae7504 gh-145866: Convert _CALL_INTRINSIC_2 to leave its inputs on the stack to be cleaned up by _POP_TOP (GH-146262)\n495178ab058 gh-146453: fix _PyType_LookupByVersion for types with fixed pre-defined version tags (#146454)\n8e1469c952f gh-138573: Filter out failing math tests on Solaris (#146402)\n9343518c6f4 gh-146244: Fix initconfig.c SET_ITEM macro leaks dict on expression failure (GH-146246)\n4447f23f408 Fix typo in 3.14 What's New tail call interpreter docs (GH-146425)\n0055140b2cf gh-146358: Fix warnings.catch_warnings on Free Threading (#146374)\n04bd54305a5 gh-146386: fix missing va_end in _abiinfo_raise in modsupport.c (#146387)\n4240c7dd749 gh-145098: Run Apple Silicon macOS CI on macos-26 (Tahoe) (#145099)\nd0e66ef1c09 gh-146369: Ensure PYTHON_LAZY_IMPORTS=none overrides __lazy_modules__ (#146371)\n2be147e1e75 gh-146352: In Emscripten pyrepl test, pick port dynamically (#146375)\n3364e7e62fa gh-146207: Add support for OpenSSL 4.0.0 alpha1 (#146217)\n08f6b33bf32 gh-146355: Fix main_module ref leak in _PyRun_SimpleStringFlagsWithName (#146356)\nf2fcc494677 gh-145633: Remove remnants of the "unknown_format" (floats) in the struct module (#146189)\nf4d1be46b47 gh-145633: Drop runtime checks for floatting-point formats in the array module (#146162)\n60093096ba6 gh-146041: Avoid lock in sys.intern() for already interned strings (gh-146072)\n535b09c19d2 GH-126910: Allow most native profilers and debuggers to unwind through JIT frames (GH-143548)\n1887a95f515 gh-128341: Use _Py_ABI_SLOT in stdlib modules (#145770)\n119fce7b886 gh-144984: Skip test under tracerefs (GH-146218)\n951675c18a1 gh-146306: JIT: Optimize float operations by mutating uniquely-referenced operands in place (GH-146307)\n68c7fad757f gh-138580: Add sys.float_info.iec_60559 boolean flag (#138811)\n6d73bc22672 gh-145866: Convert DICT_MERGE to leave its inputs on the stack to be cleaned up by _POP_TOP (GH-146329)\n07a555d4fce gh-133879: Copyedit "What's new in Python 3.15" (#146345)\n95340ef63af Remove inactive CODEOWNERS (#145930)\nfae5761a762 gh-140196: Added constructor behavior changes in ast.rst for python 3.13 (GH-140243)\nbcff99cb3f3 gh-146202: Create tmp_dir in regrtest worker (#146347)\n306c556fdbe gh-144319: obtain SeLockMemoryPrivilege on Windows (#144928)\ne017971eb9f gh-146199: Fix error handling in code_richcompare when PyObject_RichCompareBool fails (#146200)\n7dc2f52a6f5 gh-146197: Include a bit more information in sys._emscripten_info.runtime (#146346)\ndaa159f98b3 gh-135871: Reload lock internal state while spinning in PyMutex_LockTimed (gh-146064)\n821581adaeb gh-145719: Add .efi file detection in mimetypes (#145720)\na57209eb989 gh-146197: Run -m test.pythoninfo on the Emscripten CI (#146332)\n0c83e8bed8b gh-146325: Use test.support.requires_fork in test_fastpath_cache_cleared_in_forked_child (#146330)\nc94048be025 gh-146197: Add Emscripten to CI (#146198)\nf5d47fceb0d gh-143387: Raise an exception instead of returning None when metadata file is missing. (#146234)\n1114d7f7f87 gh-108907: ctypes: Document type codes (GH-145837)\n91cd2e58068 gh-146175: Soft-deprecate outdated macros; convert internal usage (GH-146178)\n90f9991abb2 gh-146056: Rework ref counting in treebuilder_handle_end() (#146167)\nfb8d8d9c9f9 Add a warning about untrusted input to configparser docs (#146276)\n4561f6418a6 gh-145264: Do not ignore excess Base64 data after the first padded quad (GH-145267)\nae6adc90790 gh-146308: Fix error handling issues in _remote_debugging module (#146309)\nb4e5bc2164b gh-146192: Add base32 support to binascii (GH-146193)\na17301ab3d0 gh-135953: Properly obtain main thread identifier in Gecko Collector (#146045)\ne36f8db7e54 gh-143414: Implement unique reference tracking for JIT, optimize unpacking of such tuples (GH-144300)\n1ceb1fb284a gh-146261: Fix bug in _Py_uop_sym_set_func_version (GH-146291)\n161329cde2b gh-145144: Add more tests for UserList, UserDict, etc (GH-145145)\n97c725cd391 gh-143959: Fix test_datetime if _datetime is unavailable (GH-145248)\nc30fae4bea9 gh-146245: Fix reference and buffer leaks via audit hook in socket module (GH-146248)\nf0aeabc6f98 gh-140049: Colorize exception notes in traceback.py (#140051)\n3776aba2048 Update docs for gh-146056 (GH-146213)\n83360b5869a Docs: replace all datetime imports with import datetime as dt (#145640)\nf6b5eed47de gh-146153: Use frozendict in pure python fallback for curses.has_key (#146154)\n8a531f89df8 gh-138234: clarify returncode behavior for subprocesses created with shell=True (#138536)\n60fbc20ef91 gh-140947: fix contextvars handling for server tasks in asyncio (#141158)\n897fa231a7b GH-100108: Add async generators best practices section (#141885)\n879c85f6e4c GH-145667: Merge GET_ITER and GET_YIELD_FROM_ITER (GH-146120)\nb5e4c46be24 gh-146228: Better fork support in cached FastPath (#146231)\n1eff27f2c04 gh-146227: Fix wrong type in _Py_atomic_load_uint16 in pyatomic_std.h (gh-146229)\nb36b87bcbbb gh-145980: Fix copy/paste mistake in binascii.c (#146230)\n6450b1d142b gh-146092: Raise MemoryError on allocation failure in _zoneinfo (#146165)\n5ad738f8fb2 gh-91279: Note SOURCE_DATE_EPOCH support in ZipFile.writestr() doc (#139396)\ncd10a2e65c2 gh-146196: Fix Undefined Behavior in _PyUnicodeWriter_WriteASCIIString() (#146201)\n82a24a44423 gh-143930: Reject leading dashes in webbrowser URLs\nb38127f3d60 gh-145916: Soft-deprecate ctypes.util.find_library (GH-145919)\n9f9faa2d1a3 gh-146205: Check the errno with != 0 in close impls in select module (#146206)\n2f4eb34bd23 gh-146171: Fix nested AttributeError suggestions (#146188)\nacfb4528de0 gh-146099: Optimize _GUARD_CODE_VERSION+IP via function version symbols (GH-146101)\n4507d496b4a gh-145980: Add support for alternative alphabets in the binascii module (GH-145981)\nd357a7dbf38 gh-145754: Update signature retrieval in unittest.mock to use forwardref annotation format (#145756)\n6b5511d66ba gh-145177: Add emscripten run --test, uses test args from config.toml (#146160)\n98977ca4334 gh-131798: JIT: optimize LOAD_COMMON_CONSTANT (GH-146104)\ndb11623694d gh-145177: Add make-dependencies command to emscripten build script (#146158)\nab47892c32e Improve tests for the PyUnicodeWriter C API (GH-146157)\nd5b26816d02 gh-145866 : Update JIT contributor list (GH-146170)\n52c01864c47 gh-145633: Refine notes on non-IEEE platforms (#145845)\nced64605be2 GH-146096: Fix segfault in BaseExceptionGroup repr (#146141)\n004ce8d97b9 gh-137696: Rename 'fort' parameter to 'order' in PyBuffer* functions (#137866)\n0d37e423d55 gh-145866: Convert SET_UPDATE to leave its inputs on the stack to be cleaned up by _POP_TOP (GH-145979)\n982459132be gh-145394: Remove duplicated isearch_add_character in _pyrepl/historical_reader.py (#145396)\n580043dfae9 gh-145254: Fix formatting of thread safety annotations (#146111)\n89a154a0c0f gh-146091: Fix NULL check in termios.tcsetwinsize (#146147)\n91e1312b950 gh-145177: Put node version into emscripten/config.toml. (#146156)\nbecd7a967f3 gh-146143: Fix the PyUnicodeWriter_WriteUCS4() signature (GH-146144)\n0f2246b1553 gh-146056: Fix list.repr() for lists containing NULLs (GH-146129)\nabd52463056 gh-145805: Add python Platforms/emscripten run subcommand (#146051)\nb9d43188e93 gh-145410: Add _sysconfig.get_platform() function (#146145)\n656abe3c9a2 gh-141510: Document isinstance change for frozendict in What's New (#146107)\nbcdf231946b gh-146121: pkgutil.get_data() reject invalid resource arguments (#146122)\ne1e4852133e gh-146092: Handle _PyFrame_GetFrameObject() failures properly (#146124)\n724c7c8146f gh-146093: Fix csv set_str(): check if PyUnicode_DecodeASCII() failed (#146113)\n6fe91a9e803 gh-144888: JIT executor bloom filter wide-type optimization and function Inlining (GH-146114)\n2c6afb935ad docs: fix f-string in ExceptionGroup example (#146108)\n5b25eaec373 gh-142518: Annotate PyList* C APIs for thread safety (#146109)\n1e4ed932109 gh-146092: Fix error handling in _BINARY_OP_ADD_UNICODE opcode (#146117)\n70c7e040d4f gh-66419: Make optional arguments with nargs=REMAINDER consume all arguments (GH-124509)\n847f83ef1c1 gh-142518: Add thread safety notes for the buffer protocol (#145911)\n3b06d68d8a3 gh-146076: Fix crash when a ZoneInfo subclass is missing a _weak_cache (#146082)\nd42a04c0450 GH-60729: Add IEEE format wave audio support (GH-145931)\n64862112b7b gh-146075: Prevent crash in functools.partial() from malformed str subclass (GH-146078)\ndfde59f0e18 Fix entry in fuzz_pycompile.dict (GH-146069)\ne0f7c1097e1 gh-145779: Improve classmethod/staticmethod scaling in free-threaded build (#145826)\n966fc815314 gh-146058: Fix GUARD_CODE_VERSION* (GH-146060)\nff287a72de4 gh-144975: Fix wave.Wave_write.setframerate() validation order (GH-144976)\ndc24b8a6d42 gh-145966: Fix _csv DIALECT_GETATTR macro silently masking non-AttributeError exceptions (GH-145974)\n1efd9939c87 gh-131798: Optimize _ITER_CHECK_RANGE and _ITER_CHECK_LIST in the JIT (GH-144583)\n9d7621b75bc gh-146054: Limit the growth of encodings.search_function cache (GH-146055)\n99e2c5eccd2 gh-144545: Improve handling of default values in Argument Clinic (GH-146016)\n4f5e79805eb Docs: a brief note in the sets tutorial about order (#145984)\n1b118353bb0 gh-145176 Move Emscripten files into Platforms/emscripten (#145806)\n104cae039ac gh-142927: Move hotspots up flamegraph sidebar (#145428)\n4e96282ee42 Docs: remove unmatched parenthesis for asyncio.TaskGroup note (#146035)\n2a0fa500f82 gh-145870: Fix Format.SOURCE reference in get_annotations docstring (#145889)\nae1b3074330 build(deps): bump hypothesis from 6.135.26 to 6.151.9 in /Tools (#145948)\n3d0824aef26 gh-127958: Trace from RESUME in the JIT (GH-145905)\n81ef1b73175 gh-144888: Replace bloom filter linked lists with continuous arrays to optimize executor invalidating performance (GH-145873)\ne18abc6a1f1 gh-135329: Remove flaky test_repl_eio test (gh-145932)\n182aea2f570 gh-146018: Disable over-aggressive optimization for _GUARD_CODE_VERSION (GH-145923)\nfd50b41aa90 GH-145247: Use _PyTuple_FromPair in exceptions.c (GH-145910)\n57e88c1cf95 gh-145599, CVE 2026-3644: Reject control characters in http.cookies.Morsel.update() (#145600)\n77632f085d0 gh-141510: Avoid critical section on frozendict copy (#145920)\ne6b9a140698 gh-144984: Fix crash in Expat's ExternalEntityParserCreate error paths (#144992)\n37121ef77ee gh-145866: Convert CALL_INTRINSIC_1 to leave its inputs on the stack to be cleaned up by _POP_TOP. (GH-145964)\n36b5284f04b gh-145649: Fix man page text wrapping for -X option (#145656)\ne2e62033aa7 gh-139038: Link to Savannah's webpage for JIT results (#146013)\n3a248564470 gh-123471: make concurrent iteration over itertools.accumulate thread-safe (#144486)\n6f8867a6765 gh-129849: Add tests for Py_tp_bases (#143208)\n70397fd1030 Docs: fix a form error and a grammatical error in float.rst (#140989)\nee5318025b0 gh-140814: Fix freeze_support() setting start method as side effect (GH-144608)\n40095d526bd Expand fuzz_pycompile.dict for new syntax\n83edae33a55 gh-145990: sort --help-env sections by environment variable name (GH-145997)\nb062f391cfb gh-145990: Sort python --help-xoptions by option name (GH-145991)\nf7cb789dc5a gh-145998: Remove duplicated "What's New in 3.15" entry (#145994)\neb0e8be3a7e gh-145986: Avoid unbound C recursion in conv_content_model in pyexpat.c (CVE 2026-4224) (#145987)\n33044b015ba gh-145976: Remove Misc/indent.pro (#145992)\n4a71946b8fb gh-122575: gh-142349: fix sys.flags tuple size (it unintentionally increased) (GH-145988)\nec5e3a5a073 gh-145968: Fix base64.b64decode altchars translation in specific cases (GH-145969)\n757e2ff9d99 gh-145976: Remove Misc/{Porting,vgrindefs} (#145973)\n2f4e4ec2e72 gh-142518: Document thread-safety guarantees of bytearray objects (#145226)\ne167e06f8c6 Bump mypy to 1.19.1 (#145956)\n1dfe99ae3be gh-141004: Document PyDTrace* (GH-141856)\n788c3291172 gh-123720: When closing an asyncio server, stop the handlers (#124689)\n31c41a61f1a Fix fuzz_builtin_int fuzzer reproducibility (#145890)\n0575ce936d3 build(deps): bump the actions group with 4 updates (#145952)\n9a83c02a0f5 gh-137650: Group dependabot actions updates (#145947)\n798070d8ca4 gh-134584: Eliminate redundant refcounting in JIT for MATCH_CLASS (GH-144821)\n97968564b61 gh-143636: fix a crash when calling __replace__ on invalid SimpleNamespace instances (#143655)\n51e8acf8de1 Docs: fix missing period in Doc/library/stdtypes.rst (#145935)\n77c06f3da62 gh-145703: Fix asyncio.BaseEventLoop low clock resolution (#145706)\n3c38feb2a21 gh-129813: Document that PyBytesWriter_GetData() cannot fail (#145900)\n747ef70faa8 GH-60729: Revert "Add IEEE format wave audio support (GH-145384)" (GH-145928)\n00a25859a94 gh-145376: Fix GC tracking in structseq.__replace__ (#145820)\n79b91e7c50d gh-142518: Document thread-safety guarantees of set objects (#145225)\n61f2a1a5993 GH-60729: Add IEEE format wave audio support (GH-145384)\nf884dc6f70f gh-140715: Add %t and %n format codes support to strptime() (GH-144896)\n91622385111 gh-99631: Add shelve custom serialization to What's New in 3.15 (#145253)\n59d97683c19 gh-145792: Fix incorrect alloca allocation size in traceback.c (#145814)\n0b6a2346e5b gh-145887: Use write() instead of stream.write() in PrettyPrinter._pprint_frozendict (#145894)\n962fb872ebd gh-145850: Change some implementation details in struct.Struct (GH-145851)\ne1c224624ae gh-145783: Propagate errors raised in NEW_TYPE_COMMENT (#145784)\n6d1e9ceed3e Docs: except with multiple exceptions parentheses not required (#145848)\n0adc7289c3a Revert "gh-143050: Remove redundant decref in _PyLong_Negate (gh-143051)" (#145891)\n08a018ebe0d gh-145801: Use gcc -fprofile-update=atomic for PGO builds (#145802)\nf1052655388 GH-132042: Fix calculation of slotdef index in update_one_slot() (#145880)\ncd521728311 gh-145685: Improve scaling of type attribute lookups (gh-145774)\n7a65900764e gh-145717: Add a few Microsoft-specific MIME types, and synchronize between mimetypes module and tests (#145718)\ne13f6dccd7a gh-140131: Fix REPL cursor position on Windows when module completion suggestion line hits console width (GH-140333)\nd4cc553294a gh-141510: Update PyDict C API doc for frozendict (#145533)\n17eb0354ff3 gh-145446: Add critical section in functools module for PyDict_Next (GH-145487)\n7836ecc5daf gh-145681: do not deallocate list buffer in _PyList_AsTupleAndClear (GH-145680)\n453562a467a GH-145692: Convert DEOPT_IFs to EXIT_IFs (GH-145751)\n86a0756234d gh-140594: Fix an out of bounds read when feeding NUL byte to PyOS_StdioReadline() (#140910)\n72456309e96 gh-143715: Deprecate incomplete initialization of struct.Struct() (GH-145580)\n0dce4c6eab9 gh-145254: Add thread safety annotation in docs (#145255)\nd19de375a20 GH-145247: Use _PyTuple_FromPair in Parser and Python (#145842)\nf062014d387 gh-144540: Add _MAKE_HEAP_SAFE uop to eliminate unnecessary refcount operations in RETURN_VALUE and YIELD_VALUE (GH-144414)\n77d6d5d8fcc gh-145736: Fix Tkinter tests for Tk 8.7, 9.0 and 9.1 (GH-145738)\n713be70175a gh-141510: Raise TypeError in PyDict_SetItem() on frozendict (#145564)\n706fd4ec08a gh-142183: Cache one datachunk per tstate to prevent alloc/dealloc thrashing (#145789)\n4722202a1a8 gh-139933: correctly suggest attributes for classes with a custom __dir__ (GH-139950)\n7a1da4575b1 gh-142518: Improve mimalloc allocator docs (#145224)\n805ca4f292e Add GitHub team for maintaining fuzzers\n42d754e34c0 gh-141707: Skip TarInfo DIRTYPE normalization during GNU long name handling\nce1abaf9b83 gh-99875: Document rounding mode for old-style formatting (#126382)\n4f87c99a5ae gh-145376: Fix refleak & pointer type bug in uncommon code paths in Parser/ (GH-145684)\naa4240ebea1 gh-145492: fix regression test for defaultdict factory repr (GH-145788)\neb9ae65e5b1 Warn that overriding __builtins__ for eval is not a security mechanism (GH-145773)\nece712197d2 gh-145546: unittest.util: fix sorted_list_difference tail deduplication (GH-145547)\n5d6e8dd683b gh-145587: fix busy loop in multiprocessing.connection.wait on Windows (GH-145597)\ndae85c4d939 gh-145633: Remove support for ancient ARM platforms with mixed-endian doubles (#145634)\nbdf6de8c3f0 gh-145685: Avoid contention on TYPE_LOCK in super() lookups (gh-145775)\ncf7c67b7c6b gh-101100: Fix sphinx reference warnings around I/O (#139592)\nc3955e049fd gh-145713: make bytearray.resize thread-safe on free-threading (#145714)\nebb150e76ab gh-145219: Cache Emscripten build dependencies, add install-emscripten (#145664)\n5197ecb5e4d gh-143050: Remove redundant decref in _PyLong_Negate (gh-143051)\n665c1db94f4 gh-142763: Fix race in ZoneInfo cache eviction (gh-144978)\n4d0dce0c8dd Fix integer overflow for formats "s" and "p" in the struct module (GH-145750)\n9c1c71066e3 gh-145010: Fix Python.h compilation with -masm=intel (#145011)\nf26eca7732c gh-145685: Update find_name_in_mro() to return a _PyStackRef (gh-145693)\nc4333a12708 gh-144173: fix flaky test_complex.test_truediv() (#144355)\n50f82d062b3 gh-145044: avoid calling Py_DECREF in unsafe_object_compare on immortal objects (#145045)\n9e0802330ca gh-145036: Fix data race for list capacity in free-threading (#145365)\n3f33bf83e84 gh-145743: Fix inconsistency after calling Struct.init() with invalid format (GH-145744)\nbf4017b1614 gh-125053: Document that ob_mutex must only be used via critical section API (#144599)\n2756d56eefe gh-85277: Fix building without stropts.h or empty stropts.h (#143521)\n2114da976c3 gh-145591: Move slicing note to getitem (GH-145606)\n467507a651e gh-145697: Add .sql and .sqlite3 recognition in mimetypes (GH-145698)\n7990313afa3 Docs: Improve the C API documentation involving threads (GH-145520)\n7fbdc8fb545 Merge branch 'main' of https://github.com/python/cpython\n1900aa9d36f Post 3.15.0a7\n9585f509d79 gh-106318: Add examples for str.isspace() docs (#145399)\nae0d2875bcf gh-145035: Allows removing the _pyrepl module to completely disable the modern REPL (GH-145159)\n2d","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false,"persistedOutputPath":"/home/pablogsal/.claude/projects/-tmp-claude-fleet-b4704ad8/3908ee0f-12b4-4e41-8375-a87d274a0a75/tool-results/b43d0q3of.txt","persistedOutputSize":49310}}
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012icR1gWtYG4tA9zEwit9UN","type":"tool_result","content":"No files found"}]},"parent_tool_use_id":null,"session_id":"3908ee0f-12b4-4e41-8375-a87d274a0a75","uuid":"936a4e16-d00d-42ad-bbd4-fed693fb1bcd","timestamp":"2026-04-04T22:06:26.461Z","tool_use_result":{"filenames":[],"durationMs":8,"numFiles":0,"truncated":false}}
Now launching 2 review agents in parallel.
{"type":"system","subtype":"task_started","task_id":"a193a24bab342602c","tool_use_id":"toolu_0114y28m8YK7WSnSV1i9XJe2","description":"Code Analyst review agent","task_type":"local_agent","prompt":"You are reviewing a code change on the CPython repository. Branch: issue-130273 vs main. No CLAUDE.md files exist.\n\nYour mandate covers three areas:\n1. Bugs: Logic errors, off-by-one errors, null/None dereferences, race conditions, incorrect assumptions, missing error handling for likely failure modes. Focus on the changes themselves.\n2. Code comment compliance: Read code comments in modified files. Check TODOs, docstrings, invariant comments, and warning comments that the change may violate.\n3. General correctness: Look for issues that could cause problems in practice.\n\nMax 10 tool uses
— be efficient, don't explore unnecessarily.\n\nReturn a list of issues, each with:\n- Location: file:line\n- Severity: CRITICAL, MAJOR, MINOR, or NITPICK\n- Description: What the issue is and why it matters\n- Suggested fix: How to fix it\n\nIf you find NO issues, return "No issues found."\n\nAvoid flagging: pre-existing issues, linter catches, pedantic nitpicks, intentional changes, issues on lines not modified.\n\n## File manifest\nM\tLib/_pyrepl/utils.py\nM\tLib/test/test_pyrepl/support.py\nM\tLib/test/test_pyrepl/test_utils.py\nM\tLib/test/test_traceback.py\nM\tLib/traceback.py\nA\tMisc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst\n\n## Scope: Current branch vs main (6 files, 122 insertions, 84 deletions)\n\n## Summary\nThis change fixes traceback color output with unicode characters (gh-130273). It:\n1. Moves str_width, wlen, and ANSI_ESCAPE_SEQUENCE from Lib/_pyrepl/utils.py to Lib/traceback.py (with underscore-prefixed names _str_width, _wlen)\n2. Adds _zip_display_width() which uses unicodedata.iter_graphemes() for proper grapheme cluster handling\n3. Changes colorized traceback grouping to use \"^\" in color / \"~\" in color instead of == \"^\" / == \"~\" (to handle multi-caret strings from grapheme clusters)\n4. Replaces _display_width's inline calculation with _wlen\n5. Moves corresponding tests from test_pyrepl to test_traceback\n\n## Full diff\n\n```diff\ndiff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py\nindex 25d7ac1bd0b..d79c451949f 100644\n--- a/Lib/_pyrepl/utils.py\n+++ b/Lib/_pyrepl/utils.py\n@@ -1,6 +1,5 @@\n from future import annotations\n import builtins\n-import functools\n import keyword\n import re\n import token as T\n@@ -11,12 +10,12 @@\n from collections import deque\n from io import StringIO\n from tokenize import TokenInfo as TI\n+from traceback import _str_width as str_width, _wlen as wlen\n from typing import Iterable, Iterator, Match, NamedTuple, Self\n \n from .types import CharBuffer, CharWidths\n from .trace import trace\n \n-ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@][A-~]")\n ZERO_WIDTH_BRACKET = re.compile(r"\x01.?\x02")\n ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})\n IDENTIFIERS_AFTER = {"def", "class"}\n@@ -59,32 +58,6 @@ class ColorSpan(NamedTuple):\n tag: str\n \n \n-@functools.cache\n-def str_width(c: str) -> int:\n- if ord(c) < 128:\n- return 1\n- # gh-139246 for zero-width joiner and combining characters\n- if unicodedata.combining(c):\n- return 0\n- category = unicodedata.category(c)\n- if category == "Cf" and c != "\u00ad":\n- return 0\n- w = unicodedata.east_asian_width(c)\n- if w in ("N", "Na", "H", "A"):\n- return 1\n- return 2\n-\n-\n-def wlen(s: str) -> int:\n- if len(s) == 1 and s != "\x1a":\n- return str_width(s)\n- length = sum(str_width(i) for i in s)\n- # remove lengths of any escape sequences\n- sequence = ANSI_ESCAPE_SEQUENCE.findall(s)\n- ctrl_z_cnt = s.count("\x1a")\n- return length - sum(len(i) for i in sequence) + ctrl_z_cnt\n-\n-\n def unbracket(s: str, including_content: bool = False) -> str:\n r"""Return s with \001 and \002 characters removed.\n \ndiff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py\nindex 4f7f9d77933..be9f207ef24 100644\n--- a/Lib/test/test_pyrepl/support.py\n+++ b/Lib/test/test_pyrepl/support.py\n@@ -1,12 +1,13 @@\n from code import InteractiveConsole\n from functools import partial\n+from traceback import ANSI_ESCAPE_SEQUENCE\n from typing import Iterable\n from unittest.mock import MagicMock\n \n from _pyrepl.console import Console, Event\n from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig\n from _pyrepl.simple_interact import _strip_final_indent\n-from _pyrepl.utils import unbracket, ANSI_ESCAPE_SEQUENCE\n+from _pyrepl.utils import unbracket\n \n \n class ScreenEqualMixin:\ndiff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py\nindex 3c55b6bdaee..eddc81489b4 100644\n--- a/Lib/test/test_pyrepl/test_utils.py\n+++ b/Lib/test/test_pyrepl/test_utils.py\n@@ -1,52 +1,9 @@\n from unittest import TestCase\n \n-from pyrepl.utils import str_width, wlen, prev_next

(truncated)

@pablogsal
Copy link
Copy Markdown
Member

Claude Fleet — Thorough Review

{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BVnZXRJ7NosSSQTzvjJ2qX","type":"tool_result","content":"diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py\nindex 25d7ac1bd0b..d79c451949f 100644\n--- a/Lib/_pyrepl/utils.py\n+++ b/Lib/_pyrepl/utils.py\n@@ -1,6 +1,5 @@\n from future import annotations\n import builtins\n-import functools\n import keyword\n import re\n import token as T\n@@ -11,12 +10,12 @@\n from collections import deque\n from io import StringIO\n from tokenize import TokenInfo as TI\n+from traceback import _str_width as str_width, _wlen as wlen\n from typing import Iterable, Iterator, Match, NamedTuple, Self\n \n from .types import CharBuffer, CharWidths\n from .trace import trace\n \n-ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@][A-~]")\n ZERO_WIDTH_BRACKET = re.compile(r"\x01.?\x02")\n ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})\n IDENTIFIERS_AFTER = {"def", "class"}\n@@ -59,32 +58,6 @@ class ColorSpan(NamedTuple):\n tag: str\n \n \n-@functools.cache\n-def str_width(c: str) -> int:\n- if ord(c) < 128:\n- return 1\n- # gh-139246 for zero-width joiner and combining characters\n- if unicodedata.combining(c):\n- return 0\n- category = unicodedata.category(c)\n- if category == "Cf" and c != "\u00ad":\n- return 0\n- w = unicodedata.east_asian_width(c)\n- if w in ("N", "Na", "H", "A"):\n- return 1\n- return 2\n-\n-\n-def wlen(s: str) -> int:\n- if len(s) == 1 and s != "\x1a":\n- return str_width(s)\n- length = sum(str_width(i) for i in s)\n- # remove lengths of any escape sequences\n- sequence = ANSI_ESCAPE_SEQUENCE.findall(s)\n- ctrl_z_cnt = s.count("\x1a")\n- return length - sum(len(i) for i in sequence) + ctrl_z_cnt\n-\n-\n def unbracket(s: str, including_content: bool = False) -> str:\n r"""Return s with \001 and \002 characters removed.\n \ndiff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py\nindex 4f7f9d77933..be9f207ef24 100644\n--- a/Lib/test/test_pyrepl/support.py\n+++ b/Lib/test/test_pyrepl/support.py\n@@ -1,12 +1,13 @@\n from code import InteractiveConsole\n from functools import partial\n+from traceback import ANSI_ESCAPE_SEQUENCE\n from typing import Iterable\n from unittest.mock import MagicMock\n \n from _pyrepl.console import Console, Event\n from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig\n from _pyrepl.simple_interact import _strip_final_indent\n-from _pyrepl.utils import unbracket, ANSI_ESCAPE_SEQUENCE\n+from _pyrepl.utils import unbracket\n \n \n class ScreenEqualMixin:\ndiff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py\nindex 3c55b6bdaee..eddc81489b4 100644\n--- a/Lib/test/test_pyrepl/test_utils.py\n+++ b/Lib/test/test_pyrepl/test_utils.py\n@@ -1,52 +1,9 @@\n from unittest import TestCase\n \n-from pyrepl.utils import str_width, wlen, prev_next_window, gen_colors\n+from pyrepl.utils import prev_next_window, gen_colors\n \n \n class TestUtils(TestCase):\n- def test_str_width(self):\n- characters = [\n- 'a',\n- '1',\n- '',\n- '!',\n- '\x1a',\n- '\u263A',\n- '\uffb9',\n- '\N{LATIN SMALL LETTER E WITH ACUTE}', # é\n- '\N{LATIN SMALL LETTER E WITH CEDILLA}', # ȩ\n- '\u00ad',\n- ]\n- for c in characters:\n- self.assertEqual(str_width(c), 1)\n-\n- zero_width_characters = [\n- '\N{COMBINING ACUTE ACCENT}',\n- '\N{ZERO WIDTH JOINER}',\n- ]\n- for c in zero_width_characters:\n- with self.subTest(character=c):\n- self.assertEqual(str_width(c), 0)\n-\n- characters = [chr(99989), chr(99999)]\n- for c in characters:\n- self.assertEqual(str_width(c), 2)\n-\n- def test_wlen(self):\n- for c in ['a', 'b', '1', '!', '']:\n- self.assertEqual(wlen(c), 1)\n- self.assertEqual(wlen('\x1a'), 2)\n-\n- char_east_asian_width_N = chr(3800)\n- self.assertEqual(wlen(char_east_asian_width_N), 1)\n- char_east_asian_width_W = chr(4352)\n- self.assertEqual(wlen(char_east_asian_width_W), 2)\n-\n- self.assertEqual(wlen('hello'), 5)\n- self.assertEqual(wlen('hello' + '\x1a'), 7)\n- self.assertEqual(wlen('e\N{COMBINING ACUTE ACCENT}'), 1)\n- self.assertEqual(wlen('a\N{ZERO WIDTH JOINER}b'), 2)\n-\n def test_prev_next_window(self):\n def gen_normal():\n yield 1\ndiff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py\nindex 99ac7fd83d9..0c55b200872 100644\n--- a/Lib/test/test_traceback.py\n+++ b/Lib/test/test_traceback.py\n@@ -28,6 +28,7 @@\n import traceback\n from functools import partial\n from pathlib import Path\n+from traceback import _str_width, _wlen\n import colorize\n \n MODULE_PREFIX = f'{name}.' if name == 'main' else ''\n@@ -1787,6 +1788,50 @@ def f():\n ]\n self.assertEqual(result_lines, expected)\n \n+ def test_str_width(self):\n+ characters = [\n+ 'a',\n+ '1',\n+ '',\n+ '!',\n+ '\x1a',\n+ '\u263A',\n+ '\uffb9',\n+ '\N{LATIN SMALL LETTER E WITH ACUTE}', # é\n+ '\N{LATIN SMALL LETTER E WITH CEDILLA}', # ȩ\n+ '\u00ad',\n+ ]\n+ for c in characters:\n+ self.assertEqual(_str_width(c), 1)\n+\n+ zero_width_characters = [\n+ '\N{COMBINING ACUTE ACCENT}',\n+ '\N{ZERO WIDTH JOINER}',\n+ ]\n+ for c in zero_width_characters:\n+ with self.subTest(character=c):\n+ self.assertEqual(_str_width(c), 0)\n+\n+ characters = [chr(99989), chr(99999)]\n+ for c in characters:\n+ self.assertEqual(str_width(c), 2)\n+\n+ def test_wlen(self):\n+ for c in ['a', 'b', '1', '!', '']:\n+ self.assertEqual(_wlen(c), 1)\n+ self.assertEqual(_wlen('\x1a'), 2)\n+\n+ char_east_asian_width_N = chr(3800)\n+ self.assertEqual(_wlen(char_east_asian_width_N), 1)\n+ char_east_asian_width_W = chr(4352)\n+ self.assertEqual(_wlen(char_east_asian_width_W), 2)\n+\n+ self.assertEqual(_wlen('hello'), 5)\n+ self.assertEqual(_wlen('hello' + '\x1a'), 7)\n+ self.assertEqual(_wlen('e\N{COMBINING ACUTE ACCENT}'), 1)\n+ self.assertEqual(_wlen('a\N{ZERO WIDTH JOINER}b'), 2)\n+\n+\n class TestKeywordTypoSuggestions(unittest.TestCase):\n TYPO_CASES = [\n ("with block ad something:\n pass", "and"),\n@@ -5321,6 +5366,32 @@ def expected(t, m, fn, l, f, E, e, z):\n ]\n self.assertEqual(actual, expected(colors))\n \n+ def test_colorized_traceback_unicode(self):\n+ try:\n+ 啊哈=1; 啊哈/0####\n+ except Exception as e:\n+ exc = traceback.TracebackException.from_exception(e)\n+\n+ actual = "".join(exc.format(colorize=True)).splitlines()\n+ def expected(t, m, fn, l, f, E, e, z):\n+ return [\n+ f" 啊哈=1; {e}啊哈{z}{E}/{z}{e}0{z}####",\n+ f" {e}~~~~{z}{E}^{z}{e}{z}",\n+ ]\n+ self.assertEqual(actual[2:4], expected(**colors))\n+\n+ try:\n+ ééééé/0\n+ except Exception as e:\n+ exc = traceback.TracebackException.from_exception(e)\n+\n+ actual = "".join(exc.format(colorize=True)).splitlines()\n+ def expected(t, m, fn, l, f, E, e, z):\n+ return [\n+ f" {E}ééééé{z}/0",\n+ f" {E}^^^^^{z}",\n+ ]\n+ self.assertEqual(actual[2:4], expected(**colors))\n \n class TestLazyImportSuggestions(unittest.TestCase):\n """Test that lazy imports are not reified when computing AttributeError suggestions."""\ndiff --git a/Lib/traceback.py b/Lib/traceback.py\nindex 42453b4867c..05cb77b6cac 100644\n--- a/Lib/traceback.py\n+++ b/Lib/traceback.py\n@@ -1,8 +1,10 @@\n """Extract, format and print information about Python stack traces."""\n \n import collections.abc\n+import functools\n import itertools\n import linecache\n+import re\n import sys\n import textwrap\n import types\n@@ -681,12 +683,12 @@ def output_line(lineno):\n colorized_line_parts = []\n colorized_carets_parts = []\n \n- for color, group in itertools.groupby(itertools.zip_longest(line, carets, fillvalue=""), key=lambda x: x[1]):\n+ for color, group in itertools.groupby(_zip_display_width(line, carets), key=lambda x: x[1]):\n caret_group = list(group)\n- if color == "^":\n+ if "^" in color:\n colorized_line_parts.append(theme.error_highlight + "".join(char for char, _ in caret_group) + theme.reset)\n colorized_carets_parts.append(theme.error_highlight + "".join(caret for _, caret in caret_group) + theme.reset)\n- elif color == "":\n+ elif "" in color:\n colorized_line_parts.append(theme.error_range + "".join(char for char, _ in caret_group) + theme.reset)\n colorized_carets_parts.append(theme.error_range + "".join(caret for _, caret in caret_group) + theme.reset)\n else:\n@@ -968,7 +970,46 @@ def setup_positions(expr, force_valid=True):\n \n return None\n \n-_WIDE_CHAR_SPECIFIERS = "WF"\n+\n+def _zip_display_width(line, carets):\n+ import unicodedata\n+ carets = iter(carets)\n+ for char in unicodedata.iter_graphemes(line):\n+ char = str(char)\n+ char_width = _display_width(char)\n+ yield char, "".join(itertools.islice(carets, char_width))\n+\n+\n+@functools.cache\n+def _str_width(c: str) -> int:\n+ import unicodedata\n+ if ord(c) < 128:\n+ return 1\n+ # gh-139246 for zero-width joiner and combining characters\n+ if unicodedata.combining(c):\n+ return 0\n+ category = unicodedata.category(c)\n+ if category == "Cf" and c != "\u00ad":\n+ return 0\n+ w = unicodedata.east_asian_width(c)\n+ if w in ("N", "Na", "H", "A"):\n+ return 1\n+ return 2\n+\n+\n+ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-]")\n+\n+\n+def _wlen(s: str) -> int:\n+ if len(s) == 1 and s != "\x1a":\n+ return _str_width(s)\n+ length = sum(_str_width(i) for i in s)\n+ # remove lengths of any escape sequences\n+ sequence = ANSI_ESCAPE_SEQUENCE.findall(s)\n+ ctrl_z_cnt = s.count("\x1a")\n+ return length - sum(len(i) for i in sequence) + ctrl_z_cnt\n+\n+\n \n def _display_width(line, offset=None):\n """Calculate the extra amount of width space the given source\n@@ -982,13 +1023,7 @@ def _display_width(line, offset=None):\n if line.isascii():\n return offset\n \n- import unicodedata\n-\n- return sum(\n- 2 if unicodedata.east_asian_width(char) in _WIDE_CHAR_SPECIFIERS else 1\n- for char in line[:offset]\n- )\n-\n+ return _wlen(line[:offset])\n \n \n class _ExceptionPrintContext:\ndiff --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\nnew file mode 100644\nindex 00000000000..981c84a9372\n--- /dev/null\n+++ b/Misc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst\n@@ -0,0 +1 @@\n+Fix traceback color output with unicode characters","is_error":false}]},"parent_tool_use_id":null,"session_id":"6685e9c4-0f7d-433f-9378-e5dedce2d003","uuid":"f311b20c-da66-45ef-86a4-b710ef2ac0f5","timestamp":"2026-04-04T22:10:35.101Z","tool_use_result":{"stdout":"diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py\nindex 25d7ac1bd0b..d79c451949f 100644\n--- a/Lib/_pyrepl/utils.py\n+++ b/Lib/_pyrepl/utils.py\n@@ -1,6 +1,5 @@\n from future import annotations\n import builtins\n-import functools\n import keyword\n import re\n import token as T\n@@ -11,12 +10,12 @@\n from collections import deque\n from io import StringIO\n from tokenize import TokenInfo as TI\n+from traceback import _str_width as str_width, _wlen as wlen\n from typing import Iterable, Iterator, Match, NamedTuple, Self\n \n from .types import CharBuffer, CharWidths\n from .trace import trace\n \n-ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@][A-~]")\n ZERO_WIDTH_BRACKET = re.compile(r"\x01.?\x02")\n ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})\n IDENTIFIERS_AFTER = {"def", "class"}\n@@ -59,32 +58,6 @@ class ColorSpan(NamedTuple):\n tag: str\n \n \n-@functools.cache\n-def str_width(c: str) -> int:\n- if ord(c) < 128:\n- return 1\n- # gh-139246 for zero-width joiner and combining characters\n- if unicodedata.combining(c):\n- return 0\n- category = unicodedata.category(c)\n- if category == "Cf" and c != "\u00ad":\n- return 0\n- w = unicodedata.east_asian_width(c)\n- if w in ("N", "Na", "H", "A"):\n- return 1\n- return 2\n-\n-\n-def wlen(s: str) -> int:\n- if len(s) == 1 and s != "\x1a":\n- return str_width(s)\n- length = sum(str_width(i) for i in s)\n- # remove lengths of any escape sequences\n- sequence = ANSI_ESCAPE_SEQUENCE.findall(s)\n- ctrl_z_cnt = s.count("\x1a")\n- return length - sum(len(i) for i in sequence) + ctrl_z_cnt\n-\n-\n def unbracket(s: str, including_content: bool = False) -> str:\n r"""Return s with \001 and \002 characters removed.\n \ndiff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py\nindex 4f7f9d77933..be9f207ef24 100644\n--- a/Lib/test/test_pyrepl/support.py\n+++ b/Lib/test/test_pyrepl/support.py\n@@ -1,12 +1,13 @@\n from code import InteractiveConsole\n from functools import partial\n+from traceback import ANSI_ESCAPE_SEQUENCE\n from typing import Iterable\n from unittest.mock import MagicMock\n \n from _pyrepl.console import Console, Event\n from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig\n from _pyrepl.simple_interact import _strip_final_indent\n-from _pyrepl.utils import unbracket, ANSI_ESCAPE_SEQUENCE\n+from _pyrepl.utils import unbracket\n \n \n class ScreenEqualMixin:\ndiff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py\nindex 3c55b6bdaee..eddc81489b4 100644\n--- a/Lib/test/test_pyrepl/test_utils.py\n+++ b/Lib/test/test_pyrepl/test_utils.py\n@@ -1,52 +1,9 @@\n from unittest import TestCase\n \n-from pyrepl.utils import str_width, wlen, prev_next_window, gen_colors\n+from pyrepl.utils import prev_next_window, gen_colors\n \n \n class TestUtils(TestCase):\n- def test_str_width(self):\n- characters = [\n- 'a',\n- '1',\n- '',\n- '!',\n- '\x1a',\n- '\u263A',\n- '\uffb9',\n- '\N{LATIN SMALL LETTER E WITH ACUTE}', # é\n- '\N{LATIN SMALL LETTER E WITH CEDILLA}', # ȩ\n- '\u00ad',\n- ]\n- for c in characters:\n- self.assertEqual(str_width(c), 1)\n-\n- zero_width_characters = [\n- '\N{COMBINING ACUTE ACCENT}',\n- '\N{ZERO WIDTH JOINER}',\n- ]\n- for c in zero_width_characters:\n- with self.subTest(character=c):\n- self.assertEqual(str_width(c), 0)\n-\n- characters = [chr(99989), chr(99999)]\n- for c in characters:\n- self.assertEqual(str_width(c), 2)\n-\n- def test_wlen(self):\n- for c in ['a', 'b', '1', '!', '']:\n- self.assertEqual(wlen(c), 1)\n- self.assertEqual(wlen('\x1a'), 2)\n-\n- char_east_asian_width_N = chr(3800)\n- self.assertEqual(wlen(char_east_asian_width_N), 1)\n- char_east_asian_width_W = chr(4352)\n- self.assertEqual(wlen(char_east_asian_width_W), 2)\n-\n- self.assertEqual(wlen('hello'), 5)\n- self.assertEqual(wlen('hello' + '\x1a'), 7)\n- self.assertEqual(wlen('e\N{COMBINING ACUTE ACCENT}'), 1)\n- self.assertEqual(wlen('a\N{ZERO WIDTH JOINER}b'), 2)\n-\n def test_prev_next_window(self):\n def gen_normal():\n yield 1\ndiff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py\nindex 99ac7fd83d9..0c55b200872 100644\n--- a/Lib/test/test_traceback.py\n+++ b/Lib/test/test_traceback.py\n@@ -28,6 +28,7 @@\n import traceback\n from functools import partial\n from pathlib import Path\n+from traceback import _str_width, _wlen\n import colorize\n \n MODULE_PREFIX = f'{name}.' if name == 'main' else ''\n@@ -1787,6 +1788,50 @@ def f():\n ]\n self.assertEqual(result_lines, expected)\n \n+ def test_str_width(self):\n+ characters = [\n+ 'a',\n+ '1',\n+ '',\n+ '!',\n+ '\x1a',\n+ '\u263A',\n+ '\uffb9',\n+ '\N{LATIN SMALL LETTER E WITH ACUTE}', # é\n+ '\N{LATIN SMALL LETTER E WITH CEDILLA}', # ȩ\n+ '\u00ad',\n+ ]\n+ for c in characters:\n+ self.assertEqual(_str_width(c), 1)\n+\n+ zero_width_characters = [\n+ '\N{COMBINING ACUTE ACCENT}',\n+ '\N{ZERO WIDTH JOINER}',\n+ ]\n+ for c in zero_width_characters:\n+ with self.subTest(character=c):\n+ self.assertEqual(_str_width(c), 0)\n+\n+ characters = [chr(99989), chr(99999)]\n+ for c in characters:\n+ self.assertEqual(str_width(c), 2)\n+\n+ def test_wlen(self):\n+ for c in ['a', 'b', '1', '!', '']:\n+ self.assertEqual(_wlen(c), 1)\n+ self.assertEqual(_wlen('\x1a'), 2)\n+\n+ char_east_asian_width_N = chr(3800)\n+ self.assertEqual(_wlen(char_east_asian_width_N), 1)\n+ char_east_asian_width_W = chr(4352)\n+ self.assertEqual(_wlen(char_east_asian_width_W), 2)\n+\n+ self.assertEqual(_wlen('hello'), 5)\n+ self.assertEqual(_wlen('hello' + '\x1a'), 7)\n+ self.assertEqual(_wlen('e\N{COMBINING ACUTE ACCENT}'), 1)\n+ self.assertEqual(_wlen('a\N{ZERO WIDTH JOINER}b'), 2)\n+\n+\n class TestKeywordTypoSuggestions(unittest.TestCase):\n TYPO_CASES = [\n ("with block ad something:\n pass", "and"),\n@@ -5321,6 +5366,32 @@ def expected(t, m, fn, l, f, E, e, z):\n ]\n self.assertEqual(actual, expected(colors))\n \n+ def test_colorized_traceback_unicode(self):\n+ try:\n+ 啊哈=1; 啊哈/0####\n+ except Exception as e:\n+ exc = traceback.TracebackException.from_exception(e)\n+\n+ actual = "".join(exc.format(colorize=True)).splitlines()\n+ def expected(t, m, fn, l, f, E, e, z):\n+ return [\n+ f" 啊哈=1; {e}啊哈{z}{E}/{z}{e}0{z}####",\n+ f" {e}~~~~{z}{E}^{z}{e}{z}",\n+ ]\n+ self.assertEqual(actual[2:4], expected(**colors))\n+\n+ try:\n+ ééééé/0\n+ except Exception as e:\n+ exc = traceback.TracebackException.from_exception(e)\n+\n+ actual = "".join(exc.format(colorize=True)).splitlines()\n+ def expected(t, m, fn, l, f, E, e, z):\n+ return [\n+ f" {E}ééééé{z}/0",\n+ f" {E}^^^^^{z}",\n+ ]\n+ self.assertEqual(actual[2:4], expected(**colors))\n \n class TestLazyImportSuggestions(unittest.TestCase):\n """Test that lazy imports are not reified when computing AttributeError suggestions."""\ndiff --git a/Lib/traceback.py b/Lib/traceback.py\nindex 42453b4867c..05cb77b6cac 100644\n--- a/Lib/traceback.py\n+++ b/Lib/traceback.py\n@@ -1,8 +1,10 @@\n """Extract, format and print information about Python stack traces."""\n \n import collections.abc\n+import functools\n import itertools\n import linecache\n+import re\n import sys\n import textwrap\n import types\n@@ -681,12 +683,12 @@ def output_line(lineno):\n colorized_line_parts = []\n colorized_carets_parts = []\n \n- for color, group in itertools.groupby(itertools.zip_longest(line, carets, fillvalue=""), key=lambda x: x[1]):\n+ for color, group in itertools.groupby(_zip_display_width(line, carets), key=lambda x: x[1]):\n caret_group = list(group)\n- if color == "^":\n+ if "^" in color:\n colorized_line_parts.append(theme.error_highlight + "".join(char for char, _ in caret_group) + theme.reset)\n colorized_carets_parts.append(theme.error_highlight + "".join(caret for _, caret in caret_group) + theme.reset)\n- elif color == "":\n+ elif "" in color:\n colorized_line_parts.append(theme.error_range + "".join(char for char, _ in caret_group) + theme.reset)\n colorized_carets_parts.append(theme.error_range + "".join(caret for _, caret in caret_group) + theme.reset)\n else:\n@@ -968,7 +970,46 @@ def setup_positions(expr, force_valid=True):\n \n return None\n \n-_WIDE_CHAR_SPECIFIERS = "WF"\n+\n+def _zip_display_width(line, carets):\n+ import unicodedata\n+ carets = iter(carets)\n+ for char in unicodedata.iter_graphemes(line):\n+ char = str(char)\n+ char_width = _display_width(char)\n+ yield char, "".join(itertools.islice(carets, char_width))\n+\n+\n+@functools.cache\n+def _str_width(c: str) -> int:\n+ import unicodedata\n+ if ord(c) < 128:\n+ return 1\n+ # gh-139246 for zero-width joiner and combining characters\n+ if unicodedata.combining(c):\n+ return 0\n+ category = unicodedata.category(c)\n+ if category == "Cf" and c != "\u00ad":\n+ return 0\n+ w = unicodedata.east_asian_width(c)\n+ if w in ("N", "Na", "H", "A"):\n+ return 1\n+ return 2\n+\n+\n+ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-]")\n+\n+\n+def _wlen(s: str) -> int:\n+ if len(s) == 1 and s != "\x1a":\n+ return _str_width(s)\n+ length = sum(_str_width(i) for i in s)\n+ # remove lengths of any escape sequences\n+ sequence = ANSI_ESCAPE_SEQUENCE.findall(s)\n+ ctrl_z_cnt = s.count("\x1a")\n+ return length - sum(len(i) for i in sequence) + ctrl_z_cnt\n+\n+\n \n def _display_width(line, offset=None):\n """Calculate the extra amount of width space the given source\n@@ -982,13 +1023,7 @@ def _display_width(line, offset=None):\n if line.isascii():\n return offset\n \n- import unicodedata\n-\n- return sum(\n- 2 if unicodedata.east_asian_width(char) in _WIDE_CHAR_SPECIFIERS else 1\n- for char in line[:offset]\n- )\n-\n+ return _wlen(line[:offset])\n \n \n class _ExceptionPrintContext:\ndiff --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\nnew file mode 100644\nindex 00000000000..981c84a9372\n--- /dev/null\n+++ b/Misc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst\n@@ -0,0 +1 @@\n+Fix traceback color output with unicode characters","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01HXtQSKy9Ny98yiQxNjL938","type":"tool_result","content":" Lib/_pyrepl/utils.py | 29 +--------\n Lib/test/test_pyrepl/support.py | 3 +-\n Lib/test/test_pyrepl/test_utils.py | 45 +-------------\n Lib/test/test_traceback.py | 71 ++++++++++++++++++++++\n Lib/traceback.py | 57 +++++++++++++----\n .../2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst | 1 +\n 6 files changed, 122 insertions(+), 84 deletions(-)","is_error":false}]},"parent_tool_use_id":null,"session_id":"6685e9c4-0f7d-433f-9378-e5dedce2d003","uuid":"85b95a64-4ac5-4dc5-b0c9-d447e1e87833","timestamp":"2026-04-04T22:10:36.212Z","tool_use_result":{"stdout":" Lib/_pyrepl/utils.py | 29 +--------\n Lib/test/test_pyrepl/support.py | 3 +-\n Lib/test/test_pyrepl/test_utils.py | 45 +-------------\n Lib/test/test_traceback.py | 71 ++++++++++++++++++++++\n Lib/traceback.py | 57 +++++++++++++----\n .../2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst | 1 +\n 6 files changed, 122 insertions(+), 84 deletions(-)","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01VhyQ82b39YfCUpeyG4Ugem","type":"tool_result","content":"M\tLib/_pyrepl/utils.py\nM\tLib/test/test_pyrepl/support.py\nM\tLib/test/test_pyrepl/test_utils.py\nM\tLib/test/test_traceback.py\nM\tLib/traceback.py\nA\tMisc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst","is_error":false}]},"parent_tool_use_id":null,"session_id":"6685e9c4-0f7d-433f-9378-e5dedce2d003","uuid":"97c920ec-3db2-4f8f-8d24-7c1de9477e4e","timestamp":"2026-04-04T22:10:36.919Z","tool_use_result":{"stdout":"M\tLib/_pyrepl/utils.py\nM\tLib/test/test_pyrepl/support.py\nM\tLib/test/test_pyrepl/test_utils.py\nM\tLib/test/test_traceback.py\nM\tLib/traceback.py\nA\tMisc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false}}
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KqFrSuooaJx1ZijciP1ud8","type":"tool_result","content":"\nOutput too large (48.2KB). Full output saved to: /home/pablogsal/.claude/projects/-tmp-claude-fleet-work-66dbad37/6685e9c4-0f7d-433f-9378-e5dedce2d003/tool-results/bwoqsfo5w.txt\n\nPreview (first 2KB):\n4ff8b07a3d9 gh-142927: Show self time in flamegraph tooltip (#147706)\nc43b490ca3f gh-145000: Find correct merge base in reusable-check-html-ids.yml workflow (#147975)\n8bf8bf92921 gh-73613: Support Base32 and Base64 without padding (GH-147974)\n75be902a13c Docs: Standardize documentation authors (#148102)\n21fb9dc71d2 gh-146527: Heap-allocate gc_stats to avoid bloating PyInterpreterState (#148057)\nb1d2d9829cf Docs: Fix a typo in the 'Non-ASCII characters in names' section (#148043)\nfbdbea949f0 Regex HOWTO: invalid string literals result in SyntaxWarning (#148092)\n853dafe23a1 gh-148083: Prevent constant folding when lhs is container types (gh-148090)\nc398490fbf1 gh-148074: Fix typeobject.c missing error return (#148075)\n289f19adb0a gh-148083: Constant-fold _CONTAINS_OP_SET for frozenset (gh-148084)\nfe9befc1ca7 gh-145883: Fix two heap-buffer-overflows in _zoneinfo (#145885)\n328da67b2c7 gh-146073: Revert "gh-146073: Add fitness/exit quality mechanism for JIT trace frontend (GH-147966)" (#148082)\n611d606a189 gh-148067: Fix typo in asyncio event loop docs: 'signals' -> 'signal' (#148073)\nc50d6cd0121 gh-148078: Fix uses of sym_is_not_null in JIT optimizer (GH-148079)\ne7bf8eac0fe gh-131798: split recursion check to _CHECK_RECURSION_LIMIT and combine checks (GH-148070)\n113038f9407 gh-148016: Improve "Leading padding not allowed" error in Base32 and Base64 decoders (GH-148017)\n985216ced30 gh-148029: Fix error message for invalid number of Base32 characters (GH-148030)\n7bcc1c49205 gh-131798: relax GUARD_CALLABLE checks for self type checks (#148069)\n7e275d49658 gh-131798: JIT inline function addresses of builtin methods (#146906)\nb8470deb5d5 gh-146450: Normalise feature set of Android build script with other platform build scripts (#146451)\n848bbe9ff21 gh-146541: Allow building the Android testbed for 32-bit targets (#146542)\ndea4083aa95 GH-146210: Fix building the jit stencils on Windows when the interpreter is built with a different clang version (#146338)\n...\n","is_error":false}]},"parent_tool_use_id":null,"session_id":"6685e9c4-0f7d-433f-9378-e5dedce2d003","uuid":"6b1da132-66c9-437e-88ee-9366434a7fa7","timestamp":"2026-04-04T22:10:37.541Z","tool_use_result":{"stdout":"4ff8b07a3d9 gh-142927: Show self time in flamegraph tooltip (#147706)\nc43b490ca3f gh-145000: Find correct merge base in reusable-check-html-ids.yml workflow (#147975)\n8bf8bf92921 gh-73613: Support Base32 and Base64 without padding (GH-147974)\n75be902a13c Docs: Standardize documentation authors (#148102)\n21fb9dc71d2 gh-146527: Heap-allocate gc_stats to avoid bloating PyInterpreterState (#148057)\nb1d2d9829cf Docs: Fix a typo in the 'Non-ASCII characters in names' section (#148043)\nfbdbea949f0 Regex HOWTO: invalid string literals result in SyntaxWarning (#148092)\n853dafe23a1 gh-148083: Prevent constant folding when lhs is container types (gh-148090)\nc398490fbf1 gh-148074: Fix typeobject.c missing error return (#148075)\n289f19adb0a gh-148083: Constant-fold _CONTAINS_OP_SET for frozenset (gh-148084)\nfe9befc1ca7 gh-145883: Fix two heap-buffer-overflows in _zoneinfo (#145885)\n328da67b2c7 gh-146073: Revert "gh-146073: Add fitness/exit quality mechanism for JIT trace frontend (GH-147966)" (#148082)\n611d606a189 gh-148067: Fix typo in asyncio event loop docs: 'signals' -> 'signal' (#148073)\nc50d6cd0121 gh-148078: Fix uses of sym_is_not_null in JIT optimizer (GH-148079)\ne7bf8eac0fe gh-131798: split recursion check to _CHECK_RECURSION_LIMIT and combine checks (GH-148070)\n113038f9407 gh-148016: Improve "Leading padding not allowed" error in Base32 and Base64 decoders (GH-148017)\n985216ced30 gh-148029: Fix error message for invalid number of Base32 characters (GH-148030)\n7bcc1c49205 gh-131798: relax GUARD_CALLABLE checks for self type checks (#148069)\n7e275d49658 gh-131798: JIT inline function addresses of builtin methods (#146906)\nb8470deb5d5 gh-146450: Normalise feature set of Android build script with other platform build scripts (#146451)\n848bbe9ff21 gh-146541: Allow building the Android testbed for 32-bit targets (#146542)\ndea4083aa95 GH-146210: Fix building the jit stencils on Windows when the interpreter is built with a different clang version (#146338)\n80d0a85d969 gh-126676: Expand argparse docs for type=bool with warning and alternatives (#146435)\ncb76ab3819f gh-145098: Use macos-15-intel instead of unstable macos-26-intel (#148038)\n198b04b75f7 gh-146073: Add fitness/exit quality mechanism for JIT trace frontend (GH-147966)\n3681d47a440 gh-143930: Tweak the exception message and increase test coverage (GH-146476)\n48317feec81 gh-146640: Optimize int operations by mutating uniquely-referenced operands in place (JIT only) (GH-146641)\n80ab6d958a0 gh-125895: Fix static asset location for sphinx-notfound-page (#147984)\nedab6860a7d gh-148022: Add threat model to remote debugging docs (#148024)\n3908593039b test_copy_dir_no_read_permission requires non root user (GH-148028)\nf3b74d62698 gh-80642: timeit - make target time of autorange configurable (#140283)\n9d087d6d41e GH-147985: Use lock-free lookup in PySet_Contains (#147986)\n617f4cc1c26 gh-142533: Document CRLF injection vulnerabilities in http.server doc (#143395)\nc1b20a6d96e GH-145247: Use _PyTuple_FromPair in Modules (part 2) (#148003)\n0c100664bc4 GH-145247: Use _PyTuple_FromPair in Modules (part 1) (#148000)\n74a82a29ba0 gh-139038: Update JIT results in preparation for 3.15a8 (GH-148018)\n86b86177476 Move root user checks to test.support (#146195)\n9b08f8c56f8 GH-126910: Revert "Make _Py_get_machine_stack_pointer return the stack pointer (#147945)" (GH-147994)\nfc7a188fe70 gh-146613: Fix re-entrant use-after-free in itertools._grouper (#147962)\nc1a4112c225 gh-147988: Initialize digits in long_alloc() in debug mode (#147989)\na86963b3e2a gh-146636: Py_mod_abi mandatory for modules created from slots array (GH-146855)\n97babb8ef70 gh-143394: Skip pyrepl test_no_newline() basic REPL if readline is missing (#147973)\n7817651b21f gh-146444: Remove legacy iOS folder (#147993)\nc32e264227b gh-146656: properly raise NotImplementedError instead of NotImplemented in test_discover (#146661)\nb456cb25a98 gh-143050: Add helper _PyLong_InitTag() (#147956)\n4810beddbfc gh-147960: Fix memory leak in crossinterp _tuple_shared() on alloc failure (#147961)\n021ee7fd6a0 Docs: Use !r in repr example (gh-146273)\n255026d9eea GH-126910: Make _Py_get_machine_stack_pointer return the stack pointer (#147945)\n9e5b8383724 gh-146488: hash-pin check-html-ids action references (#147968)\n03f3b9ade97 gh-146907: Clarify ABI compatibility between debug and release builds (GH-146925)\n24523240013 gh-146636: PEP 803: add Py_TARGET_ABI3T and .abi3t.so extension (GH-146637)\n08c5d3d4234 gh-145000: Run check-html-ids.py in CI (#145632)\n07d5d724849 Increase dependabot interval to quarterly (#147959)\na504c0a590b gh-146488: hash-pin all action references\nb38cfb7bcaa gh-146479: Skip test_frame_pointer_unwind for gcc -O3 --enable-shared (#147947)\n473d2a35ced gh-147944: Increase range of bytes_per_sep (GH-147946)\n42825e63870 gh-146431: Add the wrapcol and ignorechars parameters for more Base XX codec (GH-146434)\n4497cf3aed5 build(deps): bump qs from 6.14.0 to 6.15.0 in /Platforms/emscripten/browser_test (#146048)\n9e1f1644cd7 Docs: fill in descriptor C API docs (GH-146644)\n62a6e898e01 gh-147856: Allow the 'count' argument of bytes.replace() to be a keyword (#147943)\n362145c20eb gh-139109: Document the trace recording interpreter in internaldocs (GH-146110)\ndb5936c5b89 gh-143050: Correct PyLong_FromString() to use _PyLong_Negate() (#145901)\n829e4d0b14e gh-141510: Support frozendict in plistlib (#145590)\n67354b2925e gh-145563: Add thread-safety annotation for PyCapsule C-API (#146612)\nb4fac15613a gh-145458: use self.skip_idle consistently in the tachyon profiler (#145459)\nbbf7fb2c15a gh-146615: Fix format specifiers in Objects/ directory (GH-146620)\ndcb260eff2d gh-146615: Fix format specifiers in Python/ directory (GH-146619)\nb7055533abc gh-146615: Fix format specifiers in test cextensions (GH-146618)\n1c396e18218 gh-146615: Fix format specifiers in extension modules (GH-146617)\n72d29ea363f gh-146615: Fix crash in get() for METH_METHOD descriptors with invalid type argument (GH-146634)\n70d1b08a4bb gh-146376: Reduce timeout in Emscripten GHA workflow (#146378)\nca95e979d6c gh-146442: Fix various bugs in compiler pipeline (#146443)\ne79fd603392 gh-146238: Support half-floats in the array module (#146242)\nec2bc12e549 gh-145056: Add support for merging collections.UserDict and frozendict (GH-146465)\n727890410b7 gh-145056: Fix merging of collections.OrderedDict and frozendict (GH-146466)\na25e09721af gh-139633: Run netrc file permission check only once per parse (GH-139634)\nafd8113e9d7 gh-144270: Make SubElement parent and tag positional-only (GH-144845)\n8e9d21c64b6 gh-146558: JIT optimize dict access for objects with known hash (#146559)\n76c554bcdf5 gh-139922: always run MSVC 64-bit tail-calling CI (GH-146570)\nadf2c47911b gh-126835: Fix _PY_IS_SMALL_INT() macro (#146631)\n8e10bd77ea0 gh-146587: fix type slot assignment incase of multiple slots for same name (#146593)\nf4d3c61f367 gh-138122: Add differential flame graph (#145785)\n6857de625f1 gh-146416: Emscripten: Improve standard stream handling in node_entry.mjs (#146417)\n6420847bdaa gh-146444: Don't package as part of iOS 'build hosts' target (#146628)\n7b2495e8111 gh-138850: Add --disable-epoll to configure (GH-145768)\n382c04308d7 gh-146444: Make Platforms/Apple/ compatible with Python 3.9 (#146624)\n7f0c4f6a0b1 gh-138577: Fix keyboard shortcuts in getpass with echo_char (#141597)\n2cf6a68f028 gh-146556: Fix infinite loop in annotationlib.get_annotations() on circular wrapped (#146557)\n4d0e8ee649c gh-146579: _zstd: Fix decompression options dict error message (#146577)\ne39d84a37df gh-146083: Upgrade bundled Expat to 2.7.5 (#146085)\n24db78c5329 gh-146080: fix a crash in SNI callbacks when the SSL object is gone (#146573)\naa6680775d6 gh-146090: fix memory management of internal sqlite3 callback contexts (#146569)\n6932c3ee6ad gh-145876: Do not mask KeyErrors raised during dictionary unpacking in call (GH-146472)\n1af025dd220 gh-146004: fix test_args_from_interpreter_flags on windows (#146580)\n1fd66eadd25 gh-139003: Use frozenset for module level attributes in _pyrepl.utils (#139004)\n5bf3a31bc23 GH-146527: Add more data to GC statistics and add it to PyDebugOffsets (#146532)\n087a5f6444f _pyrepl/pager.py: call less with --clear-screen (#146382)\n578d726d467 gh-146544: Fix asyncio.Queue docstring ambiguity (#146545)\n3ff582238fd Docs: don't rely on implicit 'above' directions in socket docs (#146426)\n1efe441de7c gh-146004: propagate all -X options to multiprocessing child processes (GH-146005)\n5992238986d gh-146381: Constant-fold frozendict subscript lookups via REPLACE_OPCODE_IF_EVALUATES_PURE (gh-146490)\na933e9ccee6 GH-145247: Use _PyTuple_FromPair[Steal] in Objects (#145884)\na5b9d60a69d gh-145059: Record lazy modules without submodules in sys.lazy_modules (#146081)\n69b08c397b9 gh-145057: Fix test names and comments to reflect sys.lazy_modules is a dict, not a set (#146084)\n73cc1fd4f45 gh-146310: Fix ensurepip to treat empty WHEEL_PKG_DIR as unset (#146357)\n1384f025f5e gh-126910: Verify that JIT stencils preserve frame pointer (GH-146524)\nb60b9261b5d GH-126910: avoid reading the FP for getting the SP (GH-146521)\n6763d26b2ba GH-126910: reserve FP on AArch64 when generating JIT stencils (GH-146520)\nf89fafed25e Mention _Float16 (type from Annex H of the C23) in the struct docs (#146243)\na492d9ff746 gh-145866: Convert LIST_EXTEND to leave its inputs on the stack to be cleaned up by _POP_TOP be cleaned up by _POP_TOP (GH-146383)\n1417737810e gh-146507: cache the buffer size for SelectorTransport.get_write_buffer_size() (#146518)\nc8ee196030b gh-146388: Add null check for sym_new(ctx) in make_bottom (GH-146389)\na209dd2bdb5 gh-85332: Support cross-compiling for GNU/Hurd (#146094)\n9214e3f33ee gh-123471: Make itertools.zip_longest safe in the FT build (#146033)\n0de4e08a599 gh-146250: Fix memory leak in re-initialization of SyntaxError (#146251)\n5466f57eadd gh-142518: add thread safety annotations for bytearray C-API (#146514)\n6a94980301b gh-142518: add thread safety docs on bytes C-API (#146415)\n2e64e36a2b1 GH-139922: update 3.15 whatsnew: Windows 64-bit binaries now use the tail-calling interpreter (#146391)\nd4153a9f767 gh-146480: Override the exception in _PyErr_SetKeyError() (#146486)\nd0061d50b0f gh-146440: Add array_hook parameter to JSON decoders (#146441)\n256907d3aac gh-138580: Revert sys.float_info.iec_60559 boolean flag (#146501)\n0c7a75aeef4 gh-146059: Cleanup pickle fast_save_enter() test (#146481)\n36e4ffc1731 gh-146444: Move the Apple folder to the Platforms directory (#146497)\n5684b3a04c6 gh-146498: Ensure binary content is correctly processed in multi-arch iOS XCframeworks (#146499)\nc75d220e257 gh-146410: Add --pythoninfo to make testios (#146494)\nca6dfa0f311 gh-146446: Miscellaneous improvements to iOS XCframework build script (#146447)\n17070f41d4c gh-144837: Improve documentation for more collection methods (GH-144841)\n5c0dcb3e0d8 gh-146059: Call fast_save_leave() in pickle save_frozenset() (#146173)\ne44993a6654 gh-146318: Document that signal.SIGSTOP is Unix-only (#146319)\n0e3b3b895c8 gh-146151: Add support for complex arrays in the array module (#146237)\nc68a19448a9 gh-146151: memoryview supports 'F' and 'D' format types (complex) (#146241)\nbfdaa3c5657 gh-131798: JIT: split call method and call builtin opcodes into smaller uops (#146463)\n0e543055b01 gh-145876: Do not mask AttributeErrors raised during dictionary unpacking (GH-145906)\ne1d48237973 gh-145650: Add logging.{Formatter,Filter}.__repr__ (GH-145652)\n3a2b81e9191 gh-145616: Detect Android sysconfig ABI correctly on 32-bit ARM Android on 64-bit ARM kernel (GH-145617)\n1516c263999 gh-145866: Convert DICT_UPDATE to leave its inputs on the stack to be cleaned up by _POP_TOP (GH-146190)\n8de70b31c59 gh-145633: Fix struct.pack('f') on s390x (#146422)\n1038a3ab2e0 gh-146410: Run tests with with --pythoninfo on iOS (#146457)\nfc1f6b176e2 gh-GH-131798: optimize jit attribute loads on immutable types (#146449)\nb5a27bdcf03 gh-146410: Add --pythoninfo option to regrtest (#146413)\nf5364ae7504 gh-145866: Convert _CALL_INTRINSIC_2 to leave its inputs on the stack to be cleaned up by _POP_TOP (GH-146262)\n495178ab058 gh-146453: fix _PyType_LookupByVersion for types with fixed pre-defined version tags (#146454)\n8e1469c952f gh-138573: Filter out failing math tests on Solaris (#146402)\n9343518c6f4 gh-146244: Fix initconfig.c SET_ITEM macro leaks dict on expression failure (GH-146246)\n4447f23f408 Fix typo in 3.14 What's New tail call interpreter docs (GH-146425)\n0055140b2cf gh-146358: Fix warnings.catch_warnings on Free Threading (#146374)\n04bd54305a5 gh-146386: fix missing va_end in _abiinfo_raise in modsupport.c (#146387)\n4240c7dd749 gh-145098: Run Apple Silicon macOS CI on macos-26 (Tahoe) (#145099)\nd0e66ef1c09 gh-146369: Ensure PYTHON_LAZY_IMPORTS=none overrides __lazy_modules__ (#146371)\n2be147e1e75 gh-146352: In Emscripten pyrepl test, pick port dynamically (#146375)\n3364e7e62fa gh-146207: Add support for OpenSSL 4.0.0 alpha1 (#146217)\n08f6b33bf32 gh-146355: Fix main_module ref leak in _PyRun_SimpleStringFlagsWithName (#146356)\nf2fcc494677 gh-145633: Remove remnants of the "unknown_format" (floats) in the struct module (#146189)\nf4d1be46b47 gh-145633: Drop runtime checks for floatting-point formats in the array module (#146162)\n60093096ba6 gh-146041: Avoid lock in sys.intern() for already interned strings (gh-146072)\n535b09c19d2 GH-126910: Allow most native profilers and debuggers to unwind through JIT frames (GH-143548)\n1887a95f515 gh-128341: Use _Py_ABI_SLOT in stdlib modules (#145770)\n119fce7b886 gh-144984: Skip test under tracerefs (GH-146218)\n951675c18a1 gh-146306: JIT: Optimize float operations by mutating uniquely-referenced operands in place (GH-146307)\n68c7fad757f gh-138580: Add sys.float_info.iec_60559 boolean flag (#138811)\n6d73bc22672 gh-145866: Convert DICT_MERGE to leave its inputs on the stack to be cleaned up by _POP_TOP (GH-146329)\n07a555d4fce gh-133879: Copyedit "What's new in Python 3.15" (#146345)\n95340ef63af Remove inactive CODEOWNERS (#145930)\nfae5761a762 gh-140196: Added constructor behavior changes in ast.rst for python 3.13 (GH-140243)\nbcff99cb3f3 gh-146202: Create tmp_dir in regrtest worker (#146347)\n306c556fdbe gh-144319: obtain SeLockMemoryPrivilege on Windows (#144928)\ne017971eb9f gh-146199: Fix error handling in code_richcompare when PyObject_RichCompareBool fails (#146200)\n7dc2f52a6f5 gh-146197: Include a bit more information in sys._emscripten_info.runtime (#146346)\ndaa159f98b3 gh-135871: Reload lock internal state while spinning in PyMutex_LockTimed (gh-146064)\n821581adaeb gh-145719: Add .efi file detection in mimetypes (#145720)\na57209eb989 gh-146197: Run -m test.pythoninfo on the Emscripten CI (#146332)\n0c83e8bed8b gh-146325: Use test.support.requires_fork in test_fastpath_cache_cleared_in_forked_child (#146330)\nc94048be025 gh-146197: Add Emscripten to CI (#146198)\nf5d47fceb0d gh-143387: Raise an exception instead of returning None when metadata file is missing. (#146234)\n1114d7f7f87 gh-108907: ctypes: Document type codes (GH-145837)\n91cd2e58068 gh-146175: Soft-deprecate outdated macros; convert internal usage (GH-146178)\n90f9991abb2 gh-146056: Rework ref counting in treebuilder_handle_end() (#146167)\nfb8d8d9c9f9 Add a warning about untrusted input to configparser docs (#146276)\n4561f6418a6 gh-145264: Do not ignore excess Base64 data after the first padded quad (GH-145267)\nae6adc90790 gh-146308: Fix error handling issues in _remote_debugging module (#146309)\nb4e5bc2164b gh-146192: Add base32 support to binascii (GH-146193)\na17301ab3d0 gh-135953: Properly obtain main thread identifier in Gecko Collector (#146045)\ne36f8db7e54 gh-143414: Implement unique reference tracking for JIT, optimize unpacking of such tuples (GH-144300)\n1ceb1fb284a gh-146261: Fix bug in _Py_uop_sym_set_func_version (GH-146291)\n161329cde2b gh-145144: Add more tests for UserList, UserDict, etc (GH-145145)\n97c725cd391 gh-143959: Fix test_datetime if _datetime is unavailable (GH-145248)\nc30fae4bea9 gh-146245: Fix reference and buffer leaks via audit hook in socket module (GH-146248)\nf0aeabc6f98 gh-140049: Colorize exception notes in traceback.py (#140051)\n3776aba2048 Update docs for gh-146056 (GH-146213)\n83360b5869a Docs: replace all datetime imports with import datetime as dt (#145640)\nf6b5eed47de gh-146153: Use frozendict in pure python fallback for curses.has_key (#146154)\n8a531f89df8 gh-138234: clarify returncode behavior for subprocesses created with shell=True (#138536)\n60fbc20ef91 gh-140947: fix contextvars handling for server tasks in asyncio (#141158)\n897fa231a7b GH-100108: Add async generators best practices section (#141885)\n879c85f6e4c GH-145667: Merge GET_ITER and GET_YIELD_FROM_ITER (GH-146120)\nb5e4c46be24 gh-146228: Better fork support in cached FastPath (#146231)\n1eff27f2c04 gh-146227: Fix wrong type in _Py_atomic_load_uint16 in pyatomic_std.h (gh-146229)\nb36b87bcbbb gh-145980: Fix copy/paste mistake in binascii.c (#146230)\n6450b1d142b gh-146092: Raise MemoryError on allocation failure in _zoneinfo (#146165)\n5ad738f8fb2 gh-91279: Note SOURCE_DATE_EPOCH support in ZipFile.writestr() doc (#139396)\ncd10a2e65c2 gh-146196: Fix Undefined Behavior in _PyUnicodeWriter_WriteASCIIString() (#146201)\n82a24a44423 gh-143930: Reject leading dashes in webbrowser URLs\nb38127f3d60 gh-145916: Soft-deprecate ctypes.util.find_library (GH-145919)\n9f9faa2d1a3 gh-146205: Check the errno with != 0 in close impls in select module (#146206)\n2f4eb34bd23 gh-146171: Fix nested AttributeError suggestions (#146188)\nacfb4528de0 gh-146099: Optimize _GUARD_CODE_VERSION+IP via function version symbols (GH-146101)\n4507d496b4a gh-145980: Add support for alternative alphabets in the binascii module (GH-145981)\nd357a7dbf38 gh-145754: Update signature retrieval in unittest.mock to use forwardref annotation format (#145756)\n6b5511d66ba gh-145177: Add emscripten run --test, uses test args from config.toml (#146160)\n98977ca4334 gh-131798: JIT: optimize LOAD_COMMON_CONSTANT (GH-146104)\ndb11623694d gh-145177: Add make-dependencies command to emscripten build script (#146158)\nab47892c32e Improve tests for the PyUnicodeWriter C API (GH-146157)\nd5b26816d02 gh-145866 : Update JIT contributor list (GH-146170)\n52c01864c47 gh-145633: Refine notes on non-IEEE platforms (#145845)\nced64605be2 GH-146096: Fix segfault in BaseExceptionGroup repr (#146141)\n004ce8d97b9 gh-137696: Rename 'fort' parameter to 'order' in PyBuffer* functions (#137866)\n0d37e423d55 gh-145866: Convert SET_UPDATE to leave its inputs on the stack to be cleaned up by _POP_TOP (GH-145979)\n982459132be gh-145394: Remove duplicated isearch_add_character in _pyrepl/historical_reader.py (#145396)\n580043dfae9 gh-145254: Fix formatting of thread safety annotations (#146111)\n89a154a0c0f gh-146091: Fix NULL check in termios.tcsetwinsize (#146147)\n91e1312b950 gh-145177: Put node version into emscripten/config.toml. (#146156)\nbecd7a967f3 gh-146143: Fix the PyUnicodeWriter_WriteUCS4() signature (GH-146144)\n0f2246b1553 gh-146056: Fix list.repr() for lists containing NULLs (GH-146129)\nabd52463056 gh-145805: Add python Platforms/emscripten run subcommand (#146051)\nb9d43188e93 gh-145410: Add _sysconfig.get_platform() function (#146145)\n656abe3c9a2 gh-141510: Document isinstance change for frozendict in What's New (#146107)\nbcdf231946b gh-146121: pkgutil.get_data() reject invalid resource arguments (#146122)\ne1e4852133e gh-146092: Handle _PyFrame_GetFrameObject() failures properly (#146124)\n724c7c8146f gh-146093: Fix csv set_str(): check if PyUnicode_DecodeASCII() failed (#146113)\n6fe91a9e803 gh-144888: JIT executor bloom filter wide-type optimization and function Inlining (GH-146114)\n2c6afb935ad docs: fix f-string in ExceptionGroup example (#146108)\n5b25eaec373 gh-142518: Annotate PyList* C APIs for thread safety (#146109)\n1e4ed932109 gh-146092: Fix error handling in _BINARY_OP_ADD_UNICODE opcode (#146117)\n70c7e040d4f gh-66419: Make optional arguments with nargs=REMAINDER consume all arguments (GH-124509)\n847f83ef1c1 gh-142518: Add thread safety notes for the buffer protocol (#145911)\n3b06d68d8a3 gh-146076: Fix crash when a ZoneInfo subclass is missing a _weak_cache (#146082)\nd42a04c0450 GH-60729: Add IEEE format wave audio support (GH-145931)\n64862112b7b gh-146075: Prevent crash in functools.partial() from malformed str subclass (GH-146078)\ndfde59f0e18 Fix entry in fuzz_pycompile.dict (GH-146069)\ne0f7c1097e1 gh-145779: Improve classmethod/staticmethod scaling in free-threaded build (#145826)\n966fc815314 gh-146058: Fix GUARD_CODE_VERSION* (GH-146060)\nff287a72de4 gh-144975: Fix wave.Wave_write.setframerate() validation order (GH-144976)\ndc24b8a6d42 gh-145966: Fix _csv DIALECT_GETATTR macro silently masking non-AttributeError exceptions (GH-145974)\n1efd9939c87 gh-131798: Optimize _ITER_CHECK_RANGE and _ITER_CHECK_LIST in the JIT (GH-144583)\n9d7621b75bc gh-146054: Limit the growth of encodings.search_function cache (GH-146055)\n99e2c5eccd2 gh-144545: Improve handling of default values in Argument Clinic (GH-146016)\n4f5e79805eb Docs: a brief note in the sets tutorial about order (#145984)\n1b118353bb0 gh-145176 Move Emscripten files into Platforms/emscripten (#145806)\n104cae039ac gh-142927: Move hotspots up flamegraph sidebar (#145428)\n4e96282ee42 Docs: remove unmatched parenthesis for asyncio.TaskGroup note (#146035)\n2a0fa500f82 gh-145870: Fix Format.SOURCE reference in get_annotations docstring (#145889)\nae1b3074330 build(deps): bump hypothesis from 6.135.26 to 6.151.9 in /Tools (#145948)\n3d0824aef26 gh-127958: Trace from RESUME in the JIT (GH-145905)\n81ef1b73175 gh-144888: Replace bloom filter linked lists with continuous arrays to optimize executor invalidating performance (GH-145873)\ne18abc6a1f1 gh-135329: Remove flaky test_repl_eio test (gh-145932)\n182aea2f570 gh-146018: Disable over-aggressive optimization for _GUARD_CODE_VERSION (GH-145923)\nfd50b41aa90 GH-145247: Use _PyTuple_FromPair in exceptions.c (GH-145910)\n57e88c1cf95 gh-145599, CVE 2026-3644: Reject control characters in http.cookies.Morsel.update() (#145600)\n77632f085d0 gh-141510: Avoid critical section on frozendict copy (#145920)\ne6b9a140698 gh-144984: Fix crash in Expat's ExternalEntityParserCreate error paths (#144992)\n37121ef77ee gh-145866: Convert CALL_INTRINSIC_1 to leave its inputs on the stack to be cleaned up by _POP_TOP. (GH-145964)\n36b5284f04b gh-145649: Fix man page text wrapping for -X option (#145656)\ne2e62033aa7 gh-139038: Link to Savannah's webpage for JIT results (#146013)\n3a248564470 gh-123471: make concurrent iteration over itertools.accumulate thread-safe (#144486)\n6f8867a6765 gh-129849: Add tests for Py_tp_bases (#143208)\n70397fd1030 Docs: fix a form error and a grammatical error in float.rst (#140989)\nee5318025b0 gh-140814: Fix freeze_support() setting start method as side effect (GH-144608)\n40095d526bd Expand fuzz_pycompile.dict for new syntax\n83edae33a55 gh-145990: sort --help-env sections by environment variable name (GH-145997)\nb062f391cfb gh-145990: Sort python --help-xoptions by option name (GH-145991)\nf7cb789dc5a gh-145998: Remove duplicated "What's New in 3.15" entry (#145994)\neb0e8be3a7e gh-145986: Avoid unbound C recursion in conv_content_model in pyexpat.c (CVE 2026-4224) (#145987)\n33044b015ba gh-145976: Remove Misc/indent.pro (#145992)\n4a71946b8fb gh-122575: gh-142349: fix sys.flags tuple size (it unintentionally increased) (GH-145988)\nec5e3a5a073 gh-145968: Fix base64.b64decode altchars translation in specific cases (GH-145969)\n757e2ff9d99 gh-145976: Remove Misc/{Porting,vgrindefs} (#145973)\n2f4e4ec2e72 gh-142518: Document thread-safety guarantees of bytearray objects (#145226)\ne167e06f8c6 Bump mypy to 1.19.1 (#145956)\n1dfe99ae3be gh-141004: Document PyDTrace* (GH-141856)\n788c3291172 gh-123720: When closing an asyncio server, stop the handlers (#124689)\n31c41a61f1a Fix fuzz_builtin_int fuzzer reproducibility (#145890)\n0575ce936d3 build(deps): bump the actions group with 4 updates (#145952)\n9a83c02a0f5 gh-137650: Group dependabot actions updates (#145947)\n798070d8ca4 gh-134584: Eliminate redundant refcounting in JIT for MATCH_CLASS (GH-144821)\n97968564b61 gh-143636: fix a crash when calling __replace__ on invalid SimpleNamespace instances (#143655)\n51e8acf8de1 Docs: fix missing period in Doc/library/stdtypes.rst (#145935)\n77c06f3da62 gh-145703: Fix asyncio.BaseEventLoop low clock resolution (#145706)\n3c38feb2a21 gh-129813: Document that PyBytesWriter_GetData() cannot fail (#145900)\n747ef70faa8 GH-60729: Revert "Add IEEE format wave audio support (GH-145384)" (GH-145928)\n00a25859a94 gh-145376: Fix GC tracking in structseq.__replace__ (#145820)\n79b91e7c50d gh-142518: Document thread-safety guarantees of set objects (#145225)\n61f2a1a5993 GH-60729: Add IEEE format wave audio support (GH-145384)\nf884dc6f70f gh-140715: Add %t and %n format codes support to strptime() (GH-144896)\n91622385111 gh-99631: Add shelve custom serialization to What's New in 3.15 (#145253)\n59d97683c19 gh-145792: Fix incorrect alloca allocation size in traceback.c (#145814)\n0b6a2346e5b gh-145887: Use write() instead of stream.write() in PrettyPrinter._pprint_frozendict (#145894)\n962fb872ebd gh-145850: Change some implementation details in struct.Struct (GH-145851)\ne1c224624ae gh-145783: Propagate errors raised in NEW_TYPE_COMMENT (#145784)\n6d1e9ceed3e Docs: except with multiple exceptions parentheses not required (#145848)\n0adc7289c3a Revert "gh-143050: Remove redundant decref in _PyLong_Negate (gh-143051)" (#145891)\n08a018ebe0d gh-145801: Use gcc -fprofile-update=atomic for PGO builds (#145802)\nf1052655388 GH-132042: Fix calculation of slotdef index in update_one_slot() (#145880)\ncd521728311 gh-145685: Improve scaling of type attribute lookups (gh-145774)\n7a65900764e gh-145717: Add a few Microsoft-specific MIME types, and synchronize between mimetypes module and tests (#145718)\ne13f6dccd7a gh-140131: Fix REPL cursor position on Windows when module completion suggestion line hits console width (GH-140333)\nd4cc553294a gh-141510: Update PyDict C API doc for frozendict (#145533)\n17eb0354ff3 gh-145446: Add critical section in functools module for PyDict_Next (GH-145487)\n7836ecc5daf gh-145681: do not deallocate list buffer in _PyList_AsTupleAndClear (GH-145680)\n453562a467a GH-145692: Convert DEOPT_IFs to EXIT_IFs (GH-145751)\n86a0756234d gh-140594: Fix an out of bounds read when feeding NUL byte to PyOS_StdioReadline() (#140910)\n72456309e96 gh-143715: Deprecate incomplete initialization of struct.Struct() (GH-145580)\n0dce4c6eab9 gh-145254: Add thread safety annotation in docs (#145255)\nd19de375a20 GH-145247: Use _PyTuple_FromPair in Parser and Python (#145842)\nf062014d387 gh-144540: Add _MAKE_HEAP_SAFE uop to eliminate unnecessary refcount operations in RETURN_VALUE and YIELD_VALUE (GH-144414)\n77d6d5d8fcc gh-145736: Fix Tkinter tests for Tk 8.7, 9.0 and 9.1 (GH-145738)\n713be70175a gh-141510: Raise TypeError in PyDict_SetItem() on frozendict (#145564)\n706fd4ec08a gh-142183: Cache one datachunk per tstate to prevent alloc/dealloc thrashing (#145789)\n4722202a1a8 gh-139933: correctly suggest attributes for classes with a custom __dir__ (GH-139950)\n7a1da4575b1 gh-142518: Improve mimalloc allocator docs (#145224)\n805ca4f292e Add GitHub team for maintaining fuzzers\n42d754e34c0 gh-141707: Skip TarInfo DIRTYPE normalization during GNU long name handling\nce1abaf9b83 gh-99875: Document rounding mode for old-style formatting (#126382)\n4f87c99a5ae gh-145376: Fix refleak & pointer type bug in uncommon code paths in Parser/ (GH-145684)\naa4240ebea1 gh-145492: fix regression test for defaultdict factory repr (GH-145788)\neb9ae65e5b1 Warn that overriding __builtins__ for eval is not a security mechanism (GH-145773)\nece712197d2 gh-145546: unittest.util: fix sorted_list_difference tail deduplication (GH-145547)\n5d6e8dd683b gh-145587: fix busy loop in multiprocessing.connection.wait on Windows (GH-145597)\ndae85c4d939 gh-145633: Remove support for ancient ARM platforms with mixed-endian doubles (#145634)\nbdf6de8c3f0 gh-145685: Avoid contention on TYPE_LOCK in super() lookups (gh-145775)\ncf7c67b7c6b gh-101100: Fix sphinx reference warnings around I/O (#139592)\nc3955e049fd gh-145713: make bytearray.resize thread-safe on free-threading (#145714)\nebb150e76ab gh-145219: Cache Emscripten build dependencies, add install-emscripten (#145664)\n5197ecb5e4d gh-143050: Remove redundant decref in _PyLong_Negate (gh-143051)\n665c1db94f4 gh-142763: Fix race in ZoneInfo cache eviction (gh-144978)\n4d0dce0c8dd Fix integer overflow for formats "s" and "p" in the struct module (GH-145750)\n9c1c71066e3 gh-145010: Fix Python.h compilation with -masm=intel (#145011)\nf26eca7732c gh-145685: Update find_name_in_mro() to return a _PyStackRef (gh-145693)\nc4333a12708 gh-144173: fix flaky test_complex.test_truediv() (#144355)\n50f82d062b3 gh-145044: avoid calling Py_DECREF in unsafe_object_compare on immortal objects (#145045)\n9e0802330ca gh-145036: Fix data race for list capacity in free-threading (#145365)\n3f33bf83e84 gh-145743: Fix inconsistency after calling Struct.init() with invalid format (GH-145744)\nbf4017b1614 gh-125053: Document that ob_mutex must only be used via critical section API (#144599)\n2756d56eefe gh-85277: Fix building without stropts.h or empty stropts.h (#143521)\n2114da976c3 gh-145591: Move slicing note to getitem (GH-145606)\n467507a651e gh-145697: Add .sql and .sqlite3 recognition in mimetypes (GH-145698)\n7990313afa3 Docs: Improve the C API documentation involving threads (GH-145520)\n7fbdc8fb545 Merge branch 'main' of https://github.com/python/cpython\n1900aa9d36f Post 3.15.0a7\n9585f509d79 gh-106318: Add examples for str.isspace() docs (#145399)\nae0d2875bcf gh-145035: Allows removing the _pyrepl module to completely disable the modern REPL (GH-145159)\n2d","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false,"persistedOutputPath":"/home/pablogsal/.claude/projects/-tmp-claude-fleet-work-66dbad37/6685e9c4-0f7d-433f-9378-e5dedce2d003/tool-results/bwoqsfo5w.txt","persistedOutputSize":49310}}
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_018T8AKLQE4vDrQZ9LBP7UXi","type":"tool_result","content":"No files found"}]},"parent_tool_use_id":null,"session_id":"6685e9c4-0f7d-433f-9378-e5dedce2d003","uuid":"7b546b45-27b7-45f6-bdf6-07a881cfd368","timestamp":"2026-04-04T22:10:41.294Z","tool_use_result":{"filenames":[],"durationMs":8,"numFiles":0,"truncated":false}}
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XE99uprYWnsji3EnTi7hfh","type":"tool_result","content":"No files found"}]},"parent_tool_use_id":null,"session_id":"6685e9c4-0f7d-433f-9378-e5dedce2d003","uuid":"d623d943-f325-47bb-825c-55f2b00eef03","timestamp":"2026-04-04T22:10:42.051Z","tool_use_result":{"filenames":[],"durationMs":6,"numFiles":0,"truncated":false}}
Now launching the two review agents in parallel.
{"type":"system","subtype":"task_started","task_id":"a157b065c7a82be5d","tool_use_id":"toolu_01X6XvQjU58c3MbHVCDzpK13","description":"Code Analyst review","task_type":"local_agent","prompt":"You are reviewing a code change for CPython (the branch issue-130273 vs main). Your mandate covers:\n\n1. Bugs: Logic errors, off-by-one errors, null/None dereferences, race conditions, incorrect assumptions, missing error handling. Focus on the changes themselves.\n2. Code comment compliance: Read code comments in modified files. Check TODOs, docstrings, invariant comments, and warning comments that the change may violate.\n\nMax 10 tool uses
— be efficient.\n\nThere are no CLAUDE.md files in this repo.\n\n## Scope\nCurrent branch vs main — 6 files changed, 122 insertions, 84 deletions.\n\n## File manifest\n\nM\tLib/_pyrepl/utils.py\nM\tLib/test/test_pyrepl/support.py\nM\tLib/test/test_pyrepl/test_utils.py\nM\tLib/test/test_traceback.py\nM\tLib/traceback.py\nA\tMisc/NEWS.d/next/Library/2025-12-10-15-15-09.gh-issue-130273.iCfiY5.rst\n\n\n## Full diff\n\n```diff\ndiff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py\nindex 25d7ac1bd0b..d79c451949f 100644\n--- a/Lib/_pyrepl/utils.py\n+++ b/Lib/_pyrepl/utils.py\n@@ -1,6 +1,5 @@\n from future import annotations\n import builtins\n-import functools\n import keyword\n import re\n import token as T\n@@ -11,12 +10,12 @@\n from collections import deque\n from io import StringIO\n from tokenize import TokenInfo as TI\n+from traceback import _str_width as str_width, _wlen as wlen\n from typing import Iterable, Iterator, Match, NamedTuple, Self\n \n from .types import CharBuffer, CharWidths\n from .trace import trace\n \n-ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@][A-~]")\n ZERO_WIDTH_BRACKET = re.compile(r"\x01.?\x02")\n ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})\n IDENTIFIERS_AFTER = {"def", "class"}\n@@ -59,32 +58,6 @@ class ColorSpan(NamedTuple):\n tag: str\n \n \n-@functools.cache\n-def str_width(c: str) -> int:\n- if ord(c) < 128:\n- return 1\n- # gh-139246 for zero-width joiner and combining characters\n- if unicodedata.combining(c):\n- return 0\n- category = unicodedata.category(c)\n- if category == "Cf" and c != "\u00ad":\n- return 0\n- w = unicodedata.east_asian_width(c)\n- if w in ("N", "Na", "H", "A"):\n- return 1\n- return 2\n-\n-\n-def wlen(s: str) -> int:\n- if len(s) == 1 and s != "\x1a":\n- return str_width(s)\n- length = sum(str_width(i) for i in s)\n- # remove lengths of any escape sequences\n- sequence = ANSI_ESCAPE_SEQUENCE.findall(s)\n- ctrl_z_cnt = s.count("\x1a")\n- return length - sum(len(i) for i in sequence) + ctrl_z_cnt\n-\n-\n def unbracket(s: str, including_content: bool = False) -> str:\n r"""Return s with \001 and \002 characters removed.\n \ndiff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py\nindex 4f7f9d77933..be9f207ef24 100644\n--- a/Lib/test/test_pyrepl/support.py\n+++ b/Lib/test/test_pyrepl/support.py\n@@ -1,12 +1,13 @@\n from code import InteractiveConsole\n from functools import partial\n+from traceback import ANSI_ESCAPE_SEQUENCE\n from typing import Iterable\n from unittest.mock import MagicMock\n \n from _pyrepl.console import Console, Event\n from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig\n from _pyrepl.simple_interact import _strip_final_indent\n-from _pyrepl.utils import unbracket, ANSI_ESCAPE_SEQUENCE\n+from _pyrepl.utils import unbracket\n \n \n class ScreenEqualMixin:\ndiff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py\nindex 3c55b6bdaee..eddc81489b4 100644\n--- a/Lib/test/test_pyrepl/test_utils.py\n+++ b/Lib/test/test_pyrepl/test_utils.py\n@@ -1,52 +1,9 @@\n from unittest import TestCase\n \n-from _pyrepl.utils import str_width, wlen, prev_next_window, gen_colors\n+from _pyrepl.utils import prev_next_window, gen_colors\n \n \n class TestUtils(TestCase):\n- def test_str_width(self):\n- ...removed tests...\n-\n- def test_wlen(self):\n- ...removed tests...\n-\n def test_prev_next_window(self):\n def gen_normal():\n yield 1\ndiff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py\nindex 99ac7fd83d9..0c55b200872 100644\n--- a/Lib/test/test_traceback.py\n+++ b/Lib/test/test_traceback.py\n@@ -28,6 +28,7 @@\n import traceback\n from functools import partial\n from pathlib import Path\n+from traceback import _str_width, _wlen\n import _colorize\n \n MODULE_PREFIX = f'{name}.' if name == 'main' else ''\n@@ -1787,6 +1788,50 @@ def f():\n ]\n

(truncated)

@Wulian233
Copy link
Copy Markdown
Contributor

@pablogsal You maybe make wrong something :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Traceback colors are shifted when the line contains wide unicode characters

5 participants