From 511455019daab8743d9561ab40e813ec7e3aef89 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Tue, 28 Oct 2025 17:15:51 +0800 Subject: [PATCH 1/4] fix: default REPL does not handle tab right Signed-off-by: yihong0618 --- Lib/_pyrepl/utils.py | 6 +++++- Lib/test/test_pyrepl/test_pyrepl.py | 18 ++++++++++++++++++ ...5-10-28-17-15-30.gh-issue-140502.ilUv1f.rst | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-28-17-15-30.gh-issue-140502.ilUv1f.rst diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index 64708e843b685b..a2b86195b88894 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -330,7 +330,11 @@ def disp_str( if colors and colors[0].span.start == i: # new color starts now pre_color = theme[colors[0].tag] - if c == "\x1a": # CTRL-Z on Windows + if c == "\t": # gh-140502: handle tabs + width = 8 - (sum(char_widths) % 8) + chars.append(" " * width) + char_widths.append(width) + elif c == "\x1a": # CTRL-Z on Windows chars.append(c) char_widths.append(2) elif ord(c) < 128: diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index e298b2add52c3e..6a1a0a7e94a929 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -37,8 +37,10 @@ ReadlineConfig, _ReadlineWrapper, ) +from _pyrepl.utils import disp_str from _pyrepl.readline import multiline_input as readline_multiline_input + try: import pty except ImportError: @@ -1911,3 +1913,19 @@ def test_ctrl_d_single_line_end_no_newline(self): ) reader, _ = handle_all_events(events) self.assertEqual("hello", "".join(reader.buffer)) + + +class TestDispStr(TestCase): + def test_disp_str_width_calculation(self): + test_cases = [ + ("\tX", [8, 1]), # column 0 -> 8 spaces + ("X\t", [1, 7]), # column 1 -> 7 spaces to column 8 + ("ABC\tX", [1, 1, 1, 5, 1]), # column 3 -> 5 spaces to column 8 + ("XXXXXXX\t", [1, 1, 1, 1, 1, 1, 1, 1]), # column 7 -> 1 space + ("XXXXXXXX\t", [1, 1, 1, 1, 1, 1, 1, 1, 8]), # column 8 -> 8 spaces + ("中\tX", [2, 6, 1]), # wide char + tab + ] + for buffer, expected_widths in test_cases: + with self.subTest(buffer=repr(buffer)): + _, widths = disp_str(buffer, 0) + self.assertEqual(widths, expected_widths) diff --git a/Misc/NEWS.d/next/Library/2025-10-28-17-15-30.gh-issue-140502.ilUv1f.rst b/Misc/NEWS.d/next/Library/2025-10-28-17-15-30.gh-issue-140502.ilUv1f.rst new file mode 100644 index 00000000000000..7f3186e450ec7f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-28-17-15-30.gh-issue-140502.ilUv1f.rst @@ -0,0 +1 @@ +Fix: default REPL ``disp_str`` does not handle tab right. From 39db06d07b89a59bf37d63eb306884b2c2e0f023 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Tue, 28 Oct 2025 17:19:19 +0800 Subject: [PATCH 2/4] fix: delete extra line in import Signed-off-by: yihong0618 --- Lib/test/test_pyrepl/test_pyrepl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 6a1a0a7e94a929..7e00cfcd26ee25 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -40,7 +40,6 @@ from _pyrepl.utils import disp_str from _pyrepl.readline import multiline_input as readline_multiline_input - try: import pty except ImportError: From feaa1dbabd27ec06312cdc7e44f8d6f7e5dea6e5 Mon Sep 17 00:00:00 2001 From: yihong Date: Wed, 29 Oct 2025 08:45:51 +0800 Subject: [PATCH 3/4] Update Lib/_pyrepl/utils.py Co-authored-by: Sergey Miryanov --- Lib/_pyrepl/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index a2b86195b88894..7d296d7eedb169 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -331,6 +331,7 @@ def disp_str( pre_color = theme[colors[0].tag] if c == "\t": # gh-140502: handle tabs + if c == "\t": # gh-140502: properly handle tabs when pasting multiline text width = 8 - (sum(char_widths) % 8) chars.append(" " * width) char_widths.append(width) From 357cb9b625907ab55f92263eee20d246ae21c01f Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Wed, 29 Oct 2025 08:52:42 +0800 Subject: [PATCH 4/4] fix: apply comments Signed-off-by: yihong0618 --- Lib/_pyrepl/utils.py | 5 ++--- Lib/test/test_pyrepl/test_pyrepl.py | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index 7d296d7eedb169..75d8cc05e4441d 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -329,9 +329,8 @@ def disp_str( for i, c in enumerate(buffer, start_index): if colors and colors[0].span.start == i: # new color starts now pre_color = theme[colors[0].tag] - - if c == "\t": # gh-140502: handle tabs - if c == "\t": # gh-140502: properly handle tabs when pasting multiline text + # gh-140502: properly handle tabs when pasting multiline text + if c == "\t": width = 8 - (sum(char_widths) % 8) chars.append(" " * width) char_widths.append(width) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 7e00cfcd26ee25..ddc039f5205982 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1917,12 +1917,12 @@ def test_ctrl_d_single_line_end_no_newline(self): class TestDispStr(TestCase): def test_disp_str_width_calculation(self): test_cases = [ - ("\tX", [8, 1]), # column 0 -> 8 spaces - ("X\t", [1, 7]), # column 1 -> 7 spaces to column 8 - ("ABC\tX", [1, 1, 1, 5, 1]), # column 3 -> 5 spaces to column 8 - ("XXXXXXX\t", [1, 1, 1, 1, 1, 1, 1, 1]), # column 7 -> 1 space + ("\tX", [8, 1]), # column 0 -> 8 spaces + ("X\t", [1, 7]), # column 1 -> 7 spaces + ("ABC\tX", [1, 1, 1, 5, 1]), # column 3 -> 5 spaces + ("XXXXXXX\t", [1, 1, 1, 1, 1, 1, 1, 1]), # column 7 -> 1 space ("XXXXXXXX\t", [1, 1, 1, 1, 1, 1, 1, 1, 8]), # column 8 -> 8 spaces - ("中\tX", [2, 6, 1]), # wide char + tab + ("中\tX", [2, 6, 1]), # wide char + tab ] for buffer, expected_widths in test_cases: with self.subTest(buffer=repr(buffer)):