From cfc3423081e3ff5d2cc624a6a8acfd216cfbdfc7 Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Wed, 28 Jan 2026 14:39:53 +0800 Subject: [PATCH 01/11] Fix Windows VT EOL wrap by syncing real console cursor Windows VT terminals do not consistently wrap the cursor when a line exactly fills the terminal width. Previously we assumed a wrap always happened, which could desynchronize the logical cursor from the real console cursor and break subsequent cursor movement. This change queries the real cursor position and updates posxy accordingly, and adds regression tests for both wrap and no-wrap cases. Signed-off-by: Yongtao Huang --- Lib/_pyrepl/windows_console.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 303af8a354ff00..bb1a7751153e1e 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -272,9 +272,22 @@ def __write_changed_line( self.__write(newline[x_pos:]) if wlen(newline) == self.width: - # If we wrapped we want to start at the next line - self._move_relative(0, y + 1) - self.posxy = 0, y + 1 + if self.__vt_support: + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise WinError(GetLastError()) + win_y = int(info.dwCursorPosition.Y - info.srWindow.Top) + expected = y - self.__offset + if win_y == expected + 1: + # Terminal wrapped to next row. + self.posxy = 0, y + 1 + else: + # Terminal did not wrap; cursor stays at end-of-line. + self.posxy = self.width, y + else: + # If we wrapped we want to start at the next line + self._move_relative(0, y + 1) + self.posxy = 0, y + 1 else: self.posxy = wlen(newline), y From 0acecdc64c1459dd076a6f3d39b9901fcaedc227 Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Wed, 28 Jan 2026 14:41:33 +0800 Subject: [PATCH 02/11] Add test case --- Lib/test/test_pyrepl/test_pyrepl.py | 74 ++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 35a1733787e7a2..0ea214b409ec96 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -12,7 +12,7 @@ import tempfile from pkgutil import ModuleInfo from unittest import TestCase, skipUnless, skipIf, SkipTest -from unittest.mock import patch +from unittest.mock import Mock, patch from test.support import force_not_colorized, make_clean_env, Py_DEBUG from test.support import has_subprocess_support, SHORT_TIMEOUT, STDLIB_DIR from test.support.import_helper import import_module @@ -2105,3 +2105,75 @@ def test_ctrl_d_single_line_end_no_newline(self): ) reader, _ = handle_all_events(events) self.assertEqual("hello", "".join(reader.buffer)) + + +@skipUnless(sys.platform == "win32", "Windows console VT behavior only") +class TestWindowsConsoleVtEolWrap(TestCase): + """ + When a line exactly fills the terminal width, VT terminals differ on whether + the cursor immediately wraps to the next row. In VT mode we must synchronize + our logical cursor position with the real console cursor. + """ + def _make_console_like(self, *, width: int, offset: int, vt: bool): + from _pyrepl import windows_console as wc + + con = object.__new__(wc.WindowsConsole) + + # Minimal state needed by __write_changed_line() + con.width = width + con.screen = [] + con.posxy = (0, 0) + setattr(con, "_WindowsConsole__offset", offset) + setattr(con, "_WindowsConsole__vt_support", vt) + + # Stub out side-effecting methods used by __write_changed_line() + con._hide_cursor = Mock() + con._erase_to_end = Mock() + con._move_relative = Mock() + con.move_cursor = Mock() + setattr(con, "_WindowsConsole__write", Mock()) + + return con, wc + + def test_vt_exact_width_line_did_wrap(self): + # Terminal wrapped to next row: posxy should become (0, y+1). + width = 10 + y = 3 + con, wc = self._make_console_like(width=width, offset=0, vt=True) + + def fake_gcsbi(_h, info): + info.dwCursorPosition.X = 0 + # Visible window top = 0, cursor now on next visible row + info.srWindow.Top = 0 + info.dwCursorPosition.Y = y + 1 + return True + + with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ + patch.object(wc, "OutHandle", 1): + old = "" + new = "a" * width + wc.WindowsConsole._WindowsConsole__write_changed_line(con, y, old, new, 0) + self.assertEqual(con.posxy, (0, y + 1)) + self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls]) + + def test_vt_exact_width_line_did_not_wrap(self): + # Terminal did NOT wrap yet: posxy should stay at (width, y). + width = 10 + y = 3 + con, wc = self._make_console_like(width=width, offset=0, vt=True) + + def fake_gcsbi(_h, info): + info.dwCursorPosition.X = width + info.srWindow.Top = 0 + # Cursor remains on the same visible row + info.dwCursorPosition.Y = y + return True + + with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ + patch.object(wc, "OutHandle", 1): + old = "" + new = "a" * width + wc.WindowsConsole._WindowsConsole__write_changed_line(con, y, old, new, 0) + + self.assertEqual(con.posxy, (width, y)) + self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls]) From a2d1786afbb90dfbb8d50f1b453bdaa9b0d09356 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 06:50:39 +0000 Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-01-28-06-50-37.gh-issue-144259.Xslknn.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-01-28-06-50-37.gh-issue-144259.Xslknn.rst diff --git a/Misc/NEWS.d/next/Library/2026-01-28-06-50-37.gh-issue-144259.Xslknn.rst b/Misc/NEWS.d/next/Library/2026-01-28-06-50-37.gh-issue-144259.Xslknn.rst new file mode 100644 index 00000000000000..e35780501a2aad --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-28-06-50-37.gh-issue-144259.Xslknn.rst @@ -0,0 +1 @@ +Fix Windows VT REPL cursor desynchronization when a line exactly fills the terminal width. From e085733efe3dc09b3b7073e4cc80dbdd86fdeec9 Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Wed, 28 Jan 2026 14:55:16 +0800 Subject: [PATCH 04/11] Post fix based on lint --- Lib/test/test_pyrepl/test_pyrepl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 0ea214b409ec96..667bb0d3a73a6d 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -2111,7 +2111,7 @@ def test_ctrl_d_single_line_end_no_newline(self): class TestWindowsConsoleVtEolWrap(TestCase): """ When a line exactly fills the terminal width, VT terminals differ on whether - the cursor immediately wraps to the next row. In VT mode we must synchronize + the cursor immediately wraps to the next row. In VT mode we must synchronize our logical cursor position with the real console cursor. """ def _make_console_like(self, *, width: int, offset: int, vt: bool): From 22a8e9e0d9622685ab8fbad2ac200bd4ca245ba6 Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Sun, 1 Feb 2026 20:13:13 +0800 Subject: [PATCH 05/11] Update Lib/_pyrepl/windows_console.py Co-authored-by: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> --- Lib/_pyrepl/windows_console.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index bb1a7751153e1e..67b672c37e075f 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -272,22 +272,17 @@ def __write_changed_line( self.__write(newline[x_pos:]) if wlen(newline) == self.width: - if self.__vt_support: - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - raise WinError(GetLastError()) - win_y = int(info.dwCursorPosition.Y - info.srWindow.Top) - expected = y - self.__offset - if win_y == expected + 1: - # Terminal wrapped to next row. - self.posxy = 0, y + 1 - else: - # Terminal did not wrap; cursor stays at end-of-line. - self.posxy = self.width, y - else: - # If we wrapped we want to start at the next line - self._move_relative(0, y + 1) + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise WinError(get_last_error()) + win_y = int(info.dwCursorPosition.Y - info.srWindow.Top) + expected = y - self.__offset + if win_y == expected + 1: + # Terminal wrapped to next row. self.posxy = 0, y + 1 + else: + # Terminal did not wrap; cursor stays at end-of-line. + self.posxy = self.width, y else: self.posxy = wlen(newline), y From e13d1ddf1b75d14e12246cea0e9f4ba844be25f3 Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Sun, 1 Feb 2026 20:39:19 +0800 Subject: [PATCH 06/11] Add test case --- Lib/test/test_pyrepl/test_pyrepl.py | 66 ++++++++++++++--------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 667bb0d3a73a6d..2cc205999c9703 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -2107,13 +2107,14 @@ def test_ctrl_d_single_line_end_no_newline(self): self.assertEqual("hello", "".join(reader.buffer)) -@skipUnless(sys.platform == "win32", "Windows console VT behavior only") -class TestWindowsConsoleVtEolWrap(TestCase): +@skipUnless(sys.platform == "win32", "Windows console behavior only") +class TestWindowsConsoleEolWrap(TestCase): """ - When a line exactly fills the terminal width, VT terminals differ on whether - the cursor immediately wraps to the next row. In VT mode we must synchronize - our logical cursor position with the real console cursor. + When a line exactly fills the terminal width, Windows consoles differ on + whether the cursor immediately wraps to the next row (depends on host/mode). + We must synchronize our logical cursor position with the real console cursor. """ + def _make_console_like(self, *, width: int, offset: int, vt: bool): from _pyrepl import windows_console as wc @@ -2135,45 +2136,40 @@ def _make_console_like(self, *, width: int, offset: int, vt: bool): return con, wc - def test_vt_exact_width_line_did_wrap(self): - # Terminal wrapped to next row: posxy should become (0, y+1). + def _run_exact_width_case(self, *, vt: bool, did_wrap: bool): width = 10 y = 3 - con, wc = self._make_console_like(width=width, offset=0, vt=True) + con, wc = self._make_console_like(width=width, offset=0, vt=vt) def fake_gcsbi(_h, info): - info.dwCursorPosition.X = 0 - # Visible window top = 0, cursor now on next visible row info.srWindow.Top = 0 - info.dwCursorPosition.Y = y + 1 - return True - - with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ - patch.object(wc, "OutHandle", 1): - old = "" - new = "a" * width - wc.WindowsConsole._WindowsConsole__write_changed_line(con, y, old, new, 0) - self.assertEqual(con.posxy, (0, y + 1)) - self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls]) - - def test_vt_exact_width_line_did_not_wrap(self): - # Terminal did NOT wrap yet: posxy should stay at (width, y). - width = 10 - y = 3 - con, wc = self._make_console_like(width=width, offset=0, vt=True) - - def fake_gcsbi(_h, info): - info.dwCursorPosition.X = width - info.srWindow.Top = 0 - # Cursor remains on the same visible row - info.dwCursorPosition.Y = y + if did_wrap: + info.dwCursorPosition.X = 0 + info.dwCursorPosition.Y = y + 1 + else: + info.dwCursorPosition.X = width + info.dwCursorPosition.Y = y return True with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ - patch.object(wc, "OutHandle", 1): + patch.object(wc, "OutHandle", 1): old = "" new = "a" * width wc.WindowsConsole._WindowsConsole__write_changed_line(con, y, old, new, 0) - self.assertEqual(con.posxy, (width, y)) - self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls]) + if did_wrap: + self.assertEqual(con.posxy, (0, y + 1)) + self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls]) + else: + self.assertEqual(con.posxy, (width, y)) + self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls]) + + def test_exact_width_line_did_wrap_vt_and_legacy(self): + for vt in (True, False): + with self.subTest(vt=vt): + self._run_exact_width_case(vt=vt, did_wrap=True) + + def test_exact_width_line_did_not_wrap_vt_and_legacy(self): + for vt in (True, False): + with self.subTest(vt=vt): + self._run_exact_width_case(vt=vt, did_wrap=False) From 460dfb346d88af2a56afae55124ef9bf7913dceb Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Sun, 1 Feb 2026 20:40:20 +0800 Subject: [PATCH 07/11] Update NEWs --- .../next/Library/2026-01-28-06-50-37.gh-issue-144259.Xslknn.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-01-28-06-50-37.gh-issue-144259.Xslknn.rst b/Misc/NEWS.d/next/Library/2026-01-28-06-50-37.gh-issue-144259.Xslknn.rst index e35780501a2aad..f50ecc24c17c6d 100644 --- a/Misc/NEWS.d/next/Library/2026-01-28-06-50-37.gh-issue-144259.Xslknn.rst +++ b/Misc/NEWS.d/next/Library/2026-01-28-06-50-37.gh-issue-144259.Xslknn.rst @@ -1 +1 @@ -Fix Windows VT REPL cursor desynchronization when a line exactly fills the terminal width. +Fix Windows REPL cursor desynchronization when a line exactly fills the terminal width. From f66910e02b83742bd46be2656740233c6483e072 Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Sun, 1 Feb 2026 21:40:21 +0800 Subject: [PATCH 08/11] Fix CI failed Fall back gracefully when querying the console cursor fails with an invalid handle, instead of raising an exception. Failed link: https://github.com/python/cpython/actions/runs/21563046527/job/62130029686?pr=144297 --- Lib/_pyrepl/windows_console.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 67b672c37e075f..bc33e3fbc9a762 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -274,15 +274,21 @@ def __write_changed_line( if wlen(newline) == self.width: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): - raise WinError(get_last_error()) - win_y = int(info.dwCursorPosition.Y - info.srWindow.Top) - expected = y - self.__offset - if win_y == expected + 1: - # Terminal wrapped to next row. - self.posxy = 0, y + 1 + err = get_last_error() + if err == 6: # ERROR_INVALID_HANDLE + # Best-effort fallback: cursor stays at end-of-line. + self.posxy = self.width, y + else: + raise WinError(err) else: - # Terminal did not wrap; cursor stays at end-of-line. - self.posxy = self.width, y + win_y = int(info.dwCursorPosition.Y - info.srWindow.Top) + expected = y - self.__offset + if win_y == expected + 1: + # Terminal wrapped to next row. + self.posxy = 0, y + 1 + else: + # Terminal did not wrap; cursor stays at end-of-line. + self.posxy = self.width, y else: self.posxy = wlen(newline), y From 4647e00dd2007edbf296058a1fb900423c8943aa Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Sun, 1 Feb 2026 22:24:56 +0800 Subject: [PATCH 09/11] Resolve comments --- Lib/_pyrepl/windows_console.py | 42 ++++++++------ Lib/test/test_pyrepl/test_pyrepl.py | 87 ++++++++++++++++++++++++----- 2 files changed, 98 insertions(+), 31 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index bc33e3fbc9a762..7c9d9b4ecc6fdf 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -247,6 +247,26 @@ def input_hook(self): if nt is not None and nt._is_inputhook_installed(): return nt._inputhook + def _has_wrapped_to_next_row(self, y: int) -> bool | None: + """ + Return whether the real console cursor wrapped to the next row. + + Returns: + True - cursor wrapped to the next visible row + False - cursor did not wrap + None - cannot query the real cursor position (e.g. invalid handle) + """ + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + err = get_last_error() + if err == 6: # ERROR_INVALID_HANDLE + return None + raise WinError(err) + + win_y = int(info.dwCursorPosition.Y - info.srWindow.Top) + expected = y - self.__offset + return win_y == expected + 1 + def __write_changed_line( self, y: int, oldline: str, newline: str, px_coord: int ) -> None: @@ -272,23 +292,13 @@ def __write_changed_line( self.__write(newline[x_pos:]) if wlen(newline) == self.width: - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - err = get_last_error() - if err == 6: # ERROR_INVALID_HANDLE - # Best-effort fallback: cursor stays at end-of-line. - self.posxy = self.width, y - else: - raise WinError(err) + did_wrap = self._has_wrapped_to_next_row(y) + if did_wrap is True: + # Terminal wrapped to next row. + self.posxy = 0, y + 1 else: - win_y = int(info.dwCursorPosition.Y - info.srWindow.Top) - expected = y - self.__offset - if win_y == expected + 1: - # Terminal wrapped to next row. - self.posxy = 0, y + 1 - else: - # Terminal did not wrap; cursor stays at end-of-line. - self.posxy = self.width, y + # Terminal did not wrap; cursor stays at end-of-line. + self.posxy = self.width, y else: self.posxy = wlen(newline), y diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 2cc205999c9703..f72a773a1c2f59 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -2112,7 +2112,8 @@ class TestWindowsConsoleEolWrap(TestCase): """ When a line exactly fills the terminal width, Windows consoles differ on whether the cursor immediately wraps to the next row (depends on host/mode). - We must synchronize our logical cursor position with the real console cursor. + We must synchronize our logical cursor position with the real console cursor + when possible, and degrade gracefully when it cannot be queried. """ def _make_console_like(self, *, width: int, offset: int, vt: bool): @@ -2136,28 +2137,17 @@ def _make_console_like(self, *, width: int, offset: int, vt: bool): return con, wc - def _run_exact_width_case(self, *, vt: bool, did_wrap: bool): + def _run_exact_width_case(self, *, vt: bool, did_wrap: bool | None): width = 10 y = 3 con, wc = self._make_console_like(width=width, offset=0, vt=vt) - def fake_gcsbi(_h, info): - info.srWindow.Top = 0 - if did_wrap: - info.dwCursorPosition.X = 0 - info.dwCursorPosition.Y = y + 1 - else: - info.dwCursorPosition.X = width - info.dwCursorPosition.Y = y - return True - - with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ - patch.object(wc, "OutHandle", 1): + with patch.object(con, "_has_wrapped_to_next_row", return_value=did_wrap): old = "" new = "a" * width wc.WindowsConsole._WindowsConsole__write_changed_line(con, y, old, new, 0) - if did_wrap: + if did_wrap is True: self.assertEqual(con.posxy, (0, y + 1)) self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls]) else: @@ -2173,3 +2163,70 @@ def test_exact_width_line_did_not_wrap_vt_and_legacy(self): for vt in (True, False): with self.subTest(vt=vt): self._run_exact_width_case(vt=vt, did_wrap=False) + + def test_exact_width_line_unknown_wrap_vt_and_legacy(self): + # real cursor may be unavailable. + for vt in (True, False): + with self.subTest(vt=vt): + self._run_exact_width_case(vt=vt, did_wrap=None) + + +@skipUnless(sys.platform == "win32", "Windows console behavior only") +class TestHasWrappedToNextRow(TestCase): + def _make_console_like(self, *, offset: int): + from _pyrepl import windows_console as wc + + con = object.__new__(wc.WindowsConsole) + setattr(con, "_WindowsConsole__offset", offset) + return con, wc + + def test_returns_true_when_wrapped(self): + con, wc = self._make_console_like(offset=0) + y = 3 + + def fake_gcsbi(_h, info): + info.srWindow.Top = 0 + info.dwCursorPosition.Y = y + 1 + return True + + with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ + patch.object(wc, "OutHandle", 1): + self.assertIs(con._has_wrapped_to_next_row(y), True) + + def test_returns_false_when_not_wrapped(self): + con, wc = self._make_console_like(offset=0) + y = 3 + + def fake_gcsbi(_h, info): + info.srWindow.Top = 0 + info.dwCursorPosition.Y = y + return True + + with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ + patch.object(wc, "OutHandle", 1): + self.assertIs(con._has_wrapped_to_next_row(y), False) + + def test_returns_none_on_invalid_handle(self): + con, wc = self._make_console_like(offset=0) + y = 3 + + def fake_gcsbi(_h, info): + return False + + with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ + patch.object(wc, "OutHandle", 1), \ + patch.object(wc, "get_last_error", return_value=6): # ERROR_INVALID_HANDLE + self.assertIs(con._has_wrapped_to_next_row(y), None) + + def test_raises_on_unexpected_error(self): + con, wc = self._make_console_like(offset=0) + y = 3 + + def fake_gcsbi(_h, info): + return False + + with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \ + patch.object(wc, "OutHandle", 1), \ + patch.object(wc, "get_last_error", return_value=5): # ERROR_ACCESS_DENIED + with self.assertRaises(OSError): + con._has_wrapped_to_next_row(y) From fb6394000a11fb917f9bca09868078fe7a2f2980 Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Mon, 2 Feb 2026 00:40:25 +0800 Subject: [PATCH 10/11] Update Lib/_pyrepl/windows_console.py Co-authored-by: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> --- Lib/_pyrepl/windows_console.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 7c9d9b4ecc6fdf..108d4bf5248441 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -258,10 +258,7 @@ def _has_wrapped_to_next_row(self, y: int) -> bool | None: """ info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): - err = get_last_error() - if err == 6: # ERROR_INVALID_HANDLE - return None - raise WinError(err) + raise WinError(get_last_error()) win_y = int(info.dwCursorPosition.Y - info.srWindow.Top) expected = y - self.__offset From 4a84421b487ececfac36ee365022dfd6b97860c0 Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Mon, 2 Feb 2026 00:40:43 +0800 Subject: [PATCH 11/11] Update Lib/_pyrepl/windows_console.py Co-authored-by: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> --- Lib/_pyrepl/windows_console.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 108d4bf5248441..1ccdb098704e24 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -289,9 +289,7 @@ def __write_changed_line( self.__write(newline[x_pos:]) if wlen(newline) == self.width: - did_wrap = self._has_wrapped_to_next_row(y) - if did_wrap is True: - # Terminal wrapped to next row. + if self._has_wrapped_to_next_row(y): self.posxy = 0, y + 1 else: # Terminal did not wrap; cursor stays at end-of-line.