Skip to content

Commit 4647e00

Browse files
committed
Resolve comments
1 parent f66910e commit 4647e00

File tree

2 files changed

+98
-31
lines changed

2 files changed

+98
-31
lines changed

Lib/_pyrepl/windows_console.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,26 @@ def input_hook(self):
247247
if nt is not None and nt._is_inputhook_installed():
248248
return nt._inputhook
249249

250+
def _has_wrapped_to_next_row(self, y: int) -> bool | None:
251+
"""
252+
Return whether the real console cursor wrapped to the next row.
253+
254+
Returns:
255+
True - cursor wrapped to the next visible row
256+
False - cursor did not wrap
257+
None - cannot query the real cursor position (e.g. invalid handle)
258+
"""
259+
info = CONSOLE_SCREEN_BUFFER_INFO()
260+
if not GetConsoleScreenBufferInfo(OutHandle, info):
261+
err = get_last_error()
262+
if err == 6: # ERROR_INVALID_HANDLE
263+
return None
264+
raise WinError(err)
265+
266+
win_y = int(info.dwCursorPosition.Y - info.srWindow.Top)
267+
expected = y - self.__offset
268+
return win_y == expected + 1
269+
250270
def __write_changed_line(
251271
self, y: int, oldline: str, newline: str, px_coord: int
252272
) -> None:
@@ -272,23 +292,13 @@ def __write_changed_line(
272292

273293
self.__write(newline[x_pos:])
274294
if wlen(newline) == self.width:
275-
info = CONSOLE_SCREEN_BUFFER_INFO()
276-
if not GetConsoleScreenBufferInfo(OutHandle, info):
277-
err = get_last_error()
278-
if err == 6: # ERROR_INVALID_HANDLE
279-
# Best-effort fallback: cursor stays at end-of-line.
280-
self.posxy = self.width, y
281-
else:
282-
raise WinError(err)
295+
did_wrap = self._has_wrapped_to_next_row(y)
296+
if did_wrap is True:
297+
# Terminal wrapped to next row.
298+
self.posxy = 0, y + 1
283299
else:
284-
win_y = int(info.dwCursorPosition.Y - info.srWindow.Top)
285-
expected = y - self.__offset
286-
if win_y == expected + 1:
287-
# Terminal wrapped to next row.
288-
self.posxy = 0, y + 1
289-
else:
290-
# Terminal did not wrap; cursor stays at end-of-line.
291-
self.posxy = self.width, y
300+
# Terminal did not wrap; cursor stays at end-of-line.
301+
self.posxy = self.width, y
292302
else:
293303
self.posxy = wlen(newline), y
294304

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2112,7 +2112,8 @@ class TestWindowsConsoleEolWrap(TestCase):
21122112
"""
21132113
When a line exactly fills the terminal width, Windows consoles differ on
21142114
whether the cursor immediately wraps to the next row (depends on host/mode).
2115-
We must synchronize our logical cursor position with the real console cursor.
2115+
We must synchronize our logical cursor position with the real console cursor
2116+
when possible, and degrade gracefully when it cannot be queried.
21162117
"""
21172118

21182119
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):
21362137

21372138
return con, wc
21382139

2139-
def _run_exact_width_case(self, *, vt: bool, did_wrap: bool):
2140+
def _run_exact_width_case(self, *, vt: bool, did_wrap: bool | None):
21402141
width = 10
21412142
y = 3
21422143
con, wc = self._make_console_like(width=width, offset=0, vt=vt)
21432144

2144-
def fake_gcsbi(_h, info):
2145-
info.srWindow.Top = 0
2146-
if did_wrap:
2147-
info.dwCursorPosition.X = 0
2148-
info.dwCursorPosition.Y = y + 1
2149-
else:
2150-
info.dwCursorPosition.X = width
2151-
info.dwCursorPosition.Y = y
2152-
return True
2153-
2154-
with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \
2155-
patch.object(wc, "OutHandle", 1):
2145+
with patch.object(con, "_has_wrapped_to_next_row", return_value=did_wrap):
21562146
old = ""
21572147
new = "a" * width
21582148
wc.WindowsConsole._WindowsConsole__write_changed_line(con, y, old, new, 0)
21592149

2160-
if did_wrap:
2150+
if did_wrap is True:
21612151
self.assertEqual(con.posxy, (0, y + 1))
21622152
self.assertNotIn((0, y + 1), [c.args for c in con._move_relative.mock_calls])
21632153
else:
@@ -2173,3 +2163,70 @@ def test_exact_width_line_did_not_wrap_vt_and_legacy(self):
21732163
for vt in (True, False):
21742164
with self.subTest(vt=vt):
21752165
self._run_exact_width_case(vt=vt, did_wrap=False)
2166+
2167+
def test_exact_width_line_unknown_wrap_vt_and_legacy(self):
2168+
# real cursor may be unavailable.
2169+
for vt in (True, False):
2170+
with self.subTest(vt=vt):
2171+
self._run_exact_width_case(vt=vt, did_wrap=None)
2172+
2173+
2174+
@skipUnless(sys.platform == "win32", "Windows console behavior only")
2175+
class TestHasWrappedToNextRow(TestCase):
2176+
def _make_console_like(self, *, offset: int):
2177+
from _pyrepl import windows_console as wc
2178+
2179+
con = object.__new__(wc.WindowsConsole)
2180+
setattr(con, "_WindowsConsole__offset", offset)
2181+
return con, wc
2182+
2183+
def test_returns_true_when_wrapped(self):
2184+
con, wc = self._make_console_like(offset=0)
2185+
y = 3
2186+
2187+
def fake_gcsbi(_h, info):
2188+
info.srWindow.Top = 0
2189+
info.dwCursorPosition.Y = y + 1
2190+
return True
2191+
2192+
with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \
2193+
patch.object(wc, "OutHandle", 1):
2194+
self.assertIs(con._has_wrapped_to_next_row(y), True)
2195+
2196+
def test_returns_false_when_not_wrapped(self):
2197+
con, wc = self._make_console_like(offset=0)
2198+
y = 3
2199+
2200+
def fake_gcsbi(_h, info):
2201+
info.srWindow.Top = 0
2202+
info.dwCursorPosition.Y = y
2203+
return True
2204+
2205+
with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \
2206+
patch.object(wc, "OutHandle", 1):
2207+
self.assertIs(con._has_wrapped_to_next_row(y), False)
2208+
2209+
def test_returns_none_on_invalid_handle(self):
2210+
con, wc = self._make_console_like(offset=0)
2211+
y = 3
2212+
2213+
def fake_gcsbi(_h, info):
2214+
return False
2215+
2216+
with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \
2217+
patch.object(wc, "OutHandle", 1), \
2218+
patch.object(wc, "get_last_error", return_value=6): # ERROR_INVALID_HANDLE
2219+
self.assertIs(con._has_wrapped_to_next_row(y), None)
2220+
2221+
def test_raises_on_unexpected_error(self):
2222+
con, wc = self._make_console_like(offset=0)
2223+
y = 3
2224+
2225+
def fake_gcsbi(_h, info):
2226+
return False
2227+
2228+
with patch.object(wc, "GetConsoleScreenBufferInfo", side_effect=fake_gcsbi), \
2229+
patch.object(wc, "OutHandle", 1), \
2230+
patch.object(wc, "get_last_error", return_value=5): # ERROR_ACCESS_DENIED
2231+
with self.assertRaises(OSError):
2232+
con._has_wrapped_to_next_row(y)

0 commit comments

Comments
 (0)