From 0a38c2ed2952c09cd7fae26eb922fb07f01fefa9 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:02:56 +0200 Subject: [PATCH 01/10] fix WindowsConsole.wait() --- Lib/_pyrepl/windows_console.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 47fd3fd8f8909b..630e8579f3ed8b 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -22,8 +22,6 @@ import io import os import sys -import time -import msvcrt import ctypes from ctypes.wintypes import ( @@ -108,6 +106,9 @@ def __init__(self, err: int | None, descr: str | None = None) -> None: ALT_ACTIVE = 0x01 | 0x02 CTRL_ACTIVE = 0x04 | 0x08 +WAIT_TIMEOUT = 0x102 +WAIT_FAILED = 0xFFFFFFFF + class _error(Exception): pass @@ -410,10 +411,10 @@ def _getscrollbacksize(self) -> int: def _read_input(self, block: bool = True) -> INPUT_RECORD | None: if not block: - events = DWORD() - if not GetNumberOfConsoleInputEvents(InHandle, events): - raise WinError(GetLastError()) - if not events.value: + ret = WaitForSingleObject(InHandle, 0) + if ret == WAIT_FAILED: + raise WinError(ctypes.get_last_error()) + elif ret == WAIT_TIMEOUT: return None rec = INPUT_RECORD() @@ -522,14 +523,9 @@ def getpending(self) -> Event: def wait(self, timeout: float | None) -> bool: """Wait for an event.""" - # Poor man's Windows select loop - start_time = time.time() - while True: - if msvcrt.kbhit(): # type: ignore[attr-defined] - return True - if timeout and time.time() - start_time > timeout / 1000: - return False - time.sleep(0.01) + ret = WaitForSingleObject(InHandle, int(timeout)) + if ret == WAIT_FAILED: + raise WinError(ctypes.get_last_error()) def repaint(self) -> None: raise NotImplementedError("No repaint support") @@ -649,14 +645,15 @@ class INPUT_RECORD(Structure): ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] ReadConsoleInput.restype = BOOL - GetNumberOfConsoleInputEvents = _KERNEL32.GetNumberOfConsoleInputEvents - GetNumberOfConsoleInputEvents.argtypes = [HANDLE, POINTER(DWORD)] - GetNumberOfConsoleInputEvents.restype = BOOL FlushConsoleInputBuffer = _KERNEL32.FlushConsoleInputBuffer FlushConsoleInputBuffer.argtypes = [HANDLE] FlushConsoleInputBuffer.restype = BOOL + WaitForSingleObject = _KERNEL32.WaitForSingleObject + WaitForSingleObject.argtypes = [HANDLE, DWORD] + WaitForSingleObject.restype = DWORD + OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) InHandle = GetStdHandle(STD_INPUT_HANDLE) else: @@ -670,7 +667,7 @@ def _win_only(*args, **kwargs): GetConsoleMode = _win_only SetConsoleMode = _win_only ReadConsoleInput = _win_only - GetNumberOfConsoleInputEvents = _win_only FlushConsoleInputBuffer = _win_only + WaitForSingleObject = _win_only OutHandle = 0 InHandle = 0 From 05d26ff9c5159943897cf23beb134f8fd7555ee5 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:11:14 +0200 Subject: [PATCH 02/10] blurb it --- .../next/Library/2025-04-24-18-07-49.gh-issue-130328.z7CN8z.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-04-24-18-07-49.gh-issue-130328.z7CN8z.rst diff --git a/Misc/NEWS.d/next/Library/2025-04-24-18-07-49.gh-issue-130328.z7CN8z.rst b/Misc/NEWS.d/next/Library/2025-04-24-18-07-49.gh-issue-130328.z7CN8z.rst new file mode 100644 index 00000000000000..f53b2bd3512139 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-24-18-07-49.gh-issue-130328.z7CN8z.rst @@ -0,0 +1 @@ +Speedup pasting in ``PyREPL`` on Windows. Fix by Chris Eibl. From cad6bfb50a84af0b617d99f9ed86cca5d10ea8fc Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Thu, 24 Apr 2025 19:36:02 +0200 Subject: [PATCH 03/10] fix timeout=None --- Lib/_pyrepl/windows_console.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 630e8579f3ed8b..6bd7ae09d18c2e 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -109,6 +109,9 @@ def __init__(self, err: int | None, descr: str | None = None) -> None: WAIT_TIMEOUT = 0x102 WAIT_FAILED = 0xFFFFFFFF +# from winbase.h +INFINITE = 0xFFFFFFFF + class _error(Exception): pass @@ -523,7 +526,11 @@ def getpending(self) -> Event: def wait(self, timeout: float | None) -> bool: """Wait for an event.""" - ret = WaitForSingleObject(InHandle, int(timeout)) + if timeout is None: + timeout = INFINITE + else: + timeout = int(timeout) + ret = WaitForSingleObject(InHandle, timeout) if ret == WAIT_FAILED: raise WinError(ctypes.get_last_error()) From 3d1da666ba77d7de9761eb51fc79b0696ef98718 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Thu, 24 Apr 2025 19:53:48 +0200 Subject: [PATCH 04/10] fix mypy for ctypes.get_last_error --- Lib/_pyrepl/windows_console.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 6bd7ae09d18c2e..284d772ab504d1 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -42,7 +42,7 @@ from .windows_eventqueue import EventQueue try: - from ctypes import GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined] + from ctypes import get_last_error, GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined] except: # Keep MyPy happy off Windows from ctypes import CDLL as WinDLL, cdll as windll @@ -50,6 +50,9 @@ def GetLastError() -> int: return 42 + def get_last_error() -> int: + return 42 + class WinError(OSError): # type: ignore[no-redef] def __init__(self, err: int | None, descr: str | None = None) -> None: self.err = err @@ -416,7 +419,7 @@ def _read_input(self, block: bool = True) -> INPUT_RECORD | None: if not block: ret = WaitForSingleObject(InHandle, 0) if ret == WAIT_FAILED: - raise WinError(ctypes.get_last_error()) + raise WinError(get_last_error()) elif ret == WAIT_TIMEOUT: return None @@ -532,7 +535,7 @@ def wait(self, timeout: float | None) -> bool: timeout = int(timeout) ret = WaitForSingleObject(InHandle, timeout) if ret == WAIT_FAILED: - raise WinError(ctypes.get_last_error()) + raise WinError(get_last_error()) def repaint(self) -> None: raise NotImplementedError("No repaint support") From 4098e3d5de951fbac4b7db95fbf2f333366d1d8c Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:07:20 +0200 Subject: [PATCH 05/10] fix return value of wait() and use it in _read_input() --- Lib/_pyrepl/windows_console.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 284d772ab504d1..17942c8df0731a 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -416,12 +416,8 @@ def _getscrollbacksize(self) -> int: return info.srWindow.Bottom # type: ignore[no-any-return] def _read_input(self, block: bool = True) -> INPUT_RECORD | None: - if not block: - ret = WaitForSingleObject(InHandle, 0) - if ret == WAIT_FAILED: - raise WinError(get_last_error()) - elif ret == WAIT_TIMEOUT: - return None + if not block and not self.wait(timeout=0): + return None rec = INPUT_RECORD() read = DWORD() @@ -536,6 +532,9 @@ def wait(self, timeout: float | None) -> bool: ret = WaitForSingleObject(InHandle, timeout) if ret == WAIT_FAILED: raise WinError(get_last_error()) + elif ret == WAIT_TIMEOUT: + return False + return True def repaint(self) -> None: raise NotImplementedError("No repaint support") From a64f4c4887e79f6c6ffd74e6535ebaa835ccf0f4 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:38:19 +0200 Subject: [PATCH 06/10] read in chunks --- Lib/_pyrepl/windows_console.py | 42 ++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 17942c8df0731a..ca78d80b28f587 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -64,6 +64,8 @@ def __init__(self, err: int | None, descr: str | None = None) -> None: if TYPE_CHECKING: from typing import IO +INPUT_BUFFER_LEN = 10 * 1024 + # Virtual-Key Codes: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes VK_MAP: dict[int, str] = { 0x23: "end", # VK_END @@ -165,6 +167,10 @@ def __init__( # Console I/O is redirected, fallback... self.out = None + self.input_buffer = (INPUT_RECORD * INPUT_BUFFER_LEN)() + self.input_buffer_pos = 0 + self.input_buffer_count = 0 + def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ Refresh the console screen. @@ -415,16 +421,27 @@ def _getscrollbacksize(self) -> int: return info.srWindow.Bottom # type: ignore[no-any-return] + def more_in_buffer(self) -> bool: + return self.input_buffer_pos < self.input_buffer_count - 1 + def _read_input(self, block: bool = True) -> INPUT_RECORD | None: if not block and not self.wait(timeout=0): return None - rec = INPUT_RECORD() + if self.more_in_buffer(): + self.input_buffer_pos += 1 + return self.input_buffer[self.input_buffer_pos] + + # read next chunk + self.input_buffer_pos = 0 + self.input_buffer_count = 0 read = DWORD() - if not ReadConsoleInput(InHandle, rec, 1, read): + + if not ReadConsoleInput(InHandle, self.input_buffer, INPUT_BUFFER_LEN, read): raise WinError(GetLastError()) - return rec + self.input_buffer_count = read.value + return self.input_buffer[0] def get_event(self, block: bool = True) -> Event | None: """Return an Event instance. Returns None if |block| is false @@ -521,9 +538,14 @@ def forgetinput(self) -> None: def getpending(self) -> Event: """Return the characters that have been typed but not yet processed.""" - return Event("key", "", b"") + e = Event("key", "", b"") + + while not self.event_queue.empty(): + e2 = self.event_queue.get() + e.data += e2.data + e.raw += e.raw - def wait(self, timeout: float | None) -> bool: + def wait_for_event(self, timeout: float | None) -> bool: """Wait for an event.""" if timeout is None: timeout = INFINITE @@ -536,6 +558,16 @@ def wait(self, timeout: float | None) -> bool: return False return True + def wait(self, timeout: float | None = None) -> bool: + """ + Wait for events on the console. + """ + return ( + not self.event_queue.empty() + or self.more_in_buffer() + or self.wait_for_event(timeout) + ) + def repaint(self) -> None: raise NotImplementedError("No repaint support") From 937b6bd3efb520b50f5fae5c5737d7349701d147 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:49:12 +0200 Subject: [PATCH 07/10] fix missing return in getpending --- Lib/_pyrepl/windows_console.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index ca78d80b28f587..51597f7e15dee2 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -545,6 +545,8 @@ def getpending(self) -> Event: e.data += e2.data e.raw += e.raw + return e + def wait_for_event(self, timeout: float | None) -> bool: """Wait for an event.""" if timeout is None: From bba26c92aaf4286fc5034df1895a390188436eb6 Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:54:57 +0200 Subject: [PATCH 08/10] use self.paste_mode instead of self.in_bracketed_paste in class Reader --- Lib/_pyrepl/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 7fc2422dac9c3f..cbb3fb21b3aea3 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -633,7 +633,7 @@ def update_screen(self) -> None: def refresh(self) -> None: """Recalculate and refresh the screen.""" - if self.in_bracketed_paste and self.buffer and not self.buffer[-1] == "\n": + if self.paste_mode and self.buffer and not self.buffer[-1] == "\n": return # this call sets up self.cxy, so call it first. From 3c0cae902e6814ad1cdcd29aead0543442fc0cca Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Mon, 28 Apr 2025 19:55:10 +0200 Subject: [PATCH 09/10] Revert "use self.paste_mode instead of self.in_bracketed_paste" This reverts commit bba26c92aaf4286fc5034df1895a390188436eb6. --- Lib/_pyrepl/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index cbb3fb21b3aea3..7fc2422dac9c3f 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -633,7 +633,7 @@ def update_screen(self) -> None: def refresh(self) -> None: """Recalculate and refresh the screen.""" - if self.paste_mode and self.buffer and not self.buffer[-1] == "\n": + if self.in_bracketed_paste and self.buffer and not self.buffer[-1] == "\n": return # this call sets up self.cxy, so call it first. From 5b055f1071351edc039156a6476cbc91b7504a0a Mon Sep 17 00:00:00 2001 From: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Date: Fri, 2 May 2025 08:29:11 +0200 Subject: [PATCH 10/10] remove copying of the raw member of Event, because it isn't used in the whole code, and some code paths in WindowsConsole set it to str instead of bytes --- Lib/_pyrepl/windows_console.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 51597f7e15dee2..af68edff625192 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -543,7 +543,6 @@ def getpending(self) -> Event: while not self.event_queue.empty(): e2 = self.event_queue.get() e.data += e2.data - e.raw += e.raw return e