From a38b2390ccb106521cf58c8c124520e6bd497fe0 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Fri, 7 Jun 2024 16:39:25 +0200 Subject: [PATCH 1/3] gh-119517: Fix several issues when pasting lot of text in the REPL * Restore signal handlers for SIGINT and SIGSTOP (Ctrl-C and Ctrl-Z) * Ensure that signals are processed as soon as possible by making reads more efficient. * Protect against invalid state in internal REPL functions when interrumpted. * Do not show extraneous newlines above the scroll buffer when pasting text in the REPL Signed-off-by: Pablo Galindo --- Lib/_pyrepl/commands.py | 1 + Lib/_pyrepl/console.py | 3 +- Lib/_pyrepl/reader.py | 12 +++- Lib/_pyrepl/unix_console.py | 58 +++++++++++-------- Lib/_pyrepl/windows_console.py | 15 ++--- ...-06-07-16-42-17.gh-issue-119517.GXgQNl.rst | 2 + 6 files changed, 56 insertions(+), 35 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-06-07-16-42-17.gh-issue-119517.GXgQNl.rst diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index e94e8c25d379c1..36f6a37971e08a 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -482,3 +482,4 @@ def do(self) -> None: self.reader.in_bracketed_paste = False self.reader.dirty = True self.reader.calc_screen = self.reader.calc_complete_screen + self.reader.scroll_on_next_refresh = False diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index a8d3f520340dcf..97d82b27cde21c 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -69,7 +69,8 @@ def __init__( self.output_fd = f_out.fileno() @abstractmethod - def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ... + def refresh(self, screen: list[str], xy: tuple[int, int], + scroll: bool = False) -> None: ... @abstractmethod def prepare(self) -> None: ... diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index beee7764e0eb84..7fde51d2b08d9f 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -240,6 +240,8 @@ class Reader: lxy: tuple[int, int] = field(init=False) calc_screen: CalcScreen = field(init=False) scheduled_commands: list[str] = field(default_factory=list) + can_colorize: bool = False + scroll_on_next_refresh: bool = True def __post_init__(self) -> None: # Enable the use of `insert` without a `prepare` call - necessary to @@ -253,13 +255,16 @@ def __post_init__(self) -> None: self.cxy = self.pos2xy() self.lxy = (self.pos, 0) self.calc_screen = self.calc_complete_screen - + self.can_colorize = can_colorize() + def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]: return default_keymap def append_to_screen(self) -> list[str]: new_screen = self.screen.copy() or [''] + if not self.buffer: + return [] new_character = self.buffer[-1] new_character_len = wlen(new_character) @@ -468,7 +473,7 @@ def get_prompt(self, lineno: int, cursor_on_line: bool) -> str: else: prompt = self.ps1 - if can_colorize(): + if self.can_colorize: prompt = f"{ANSIColors.BOLD_MAGENTA}{prompt}{ANSIColors.RESET}" return prompt @@ -606,8 +611,9 @@ def refresh(self) -> None: """Recalculate and refresh the screen.""" # this call sets up self.cxy, so call it first. self.screen = self.calc_screen() - self.console.refresh(self.screen, self.cxy) + self.console.refresh(self.screen, self.cxy, scroll=self.scroll_on_next_refresh) self.dirty = False + self.scroll_on_next_refresh = True def do_cmd(self, cmd: tuple[str, list[str]]) -> None: """`cmd` is a tuple of "event_name" and "event", which in the current diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 2f73a59dd1fced..4a03a4cc2baf82 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -27,7 +27,6 @@ import select import signal import struct -import sys import termios import time from fcntl import ioctl @@ -206,7 +205,7 @@ def change_encoding(self, encoding: str) -> None: """ self.encoding = encoding - def refresh(self, screen, c_xy): + def refresh(self, screen, c_xy, scroll=True): """ Refresh the console screen. @@ -248,22 +247,23 @@ def refresh(self, screen, c_xy): newscr = screen[offset : offset + height] # use hardware scrolling if we have it. - if old_offset > offset and self._ri: - self.__hide_cursor() - self.__write_code(self._cup, 0, 0) - self.__posxy = 0, old_offset - for i in range(old_offset - offset): - self.__write_code(self._ri) - oldscr.pop(-1) - oldscr.insert(0, "") - elif old_offset < offset and self._ind: - self.__hide_cursor() - self.__write_code(self._cup, self.height - 1, 0) - self.__posxy = 0, old_offset + self.height - 1 - for i in range(offset - old_offset): - self.__write_code(self._ind) - oldscr.pop(0) - oldscr.append("") + if scroll: + if old_offset > offset and self._ri: + self.__hide_cursor() + self.__write_code(self._cup, 0, 0) + self.__posxy = 0, old_offset + for i in range(old_offset - offset): + self.__write_code(self._ri) + oldscr.pop(-1) + oldscr.insert(0, "") + elif old_offset < offset and self._ind: + self.__hide_cursor() + self.__write_code(self._cup, self.height - 1, 0) + self.__posxy = 0, old_offset + self.height - 1 + for i in range(offset - old_offset): + self.__write_code(self._ind) + oldscr.pop(0) + oldscr.append("") self.__offset = offset @@ -310,14 +310,13 @@ def prepare(self): """ self.__svtermstate = tcgetattr(self.input_fd) raw = self.__svtermstate.copy() - raw.iflag &= ~(termios.BRKINT | termios.INPCK | termios.ISTRIP | termios.IXON) + raw.iflag &= ~(termios.INPCK | termios.ISTRIP | termios.IXON) raw.oflag &= ~(termios.OPOST) raw.cflag &= ~(termios.CSIZE | termios.PARENB) raw.cflag |= termios.CS8 - raw.lflag &= ~( - termios.ICANON | termios.ECHO | termios.IEXTEN | (termios.ISIG * 1) - ) - raw.cc[termios.VMIN] = 1 + raw.iflag |= termios.BRKINT + raw.lflag &= ~(termios.ICANON | termios.ECHO | termios.IEXTEN) + raw.lflag |= termios.ISIG raw.cc[termios.VTIME] = 0 tcsetattr(self.input_fd, termios.TCSADRAIN, raw) @@ -370,10 +369,21 @@ def get_event(self, block: bool = True) -> Event | None: Returns: - Event: Event object from the event queue. """ + if self.wait(timeout=0): + try: + chars = os.read(self.input_fd, 1024) + for char in chars: + self.push_char(char) + except OSError as err: + if err.errno == errno.EINTR: + raise + while self.event_queue.empty(): while True: try: - self.push_char(os.read(self.input_fd, 1)) + chars = os.read(self.input_fd, 1024) + for char in chars: + self.push_char(char) except OSError as err: if err.errno == errno.EINTR: if not self.event_queue.empty(): diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index f691ca3fbb07b8..90cdb73bcf86b0 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -136,7 +136,7 @@ def __init__( # Console I/O is redirected, fallback... self.out = None - def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: + def refresh(self, screen: list[str], c_xy: tuple[int, int], scroll: bool = True) -> None: """ Refresh the console screen. @@ -165,12 +165,13 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: offset = cy - height + 1 scroll_lines = offset - old_offset - # Scrolling the buffer as the current input is greater than the visible - # portion of the window. We need to scroll the visible portion and the - # entire history - self._scroll(scroll_lines, self._getscrollbacksize()) - self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines - self.__offset += scroll_lines + if scroll: + # Scrolling the buffer as the current input is greater than the visible + # portion of the window. We need to scroll the visible portion and the + # entire history + self._scroll(scroll_lines, self._getscrollbacksize()) + self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines + self.__offset += scroll_lines for i in range(scroll_lines): self.screen.append("") diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-06-07-16-42-17.gh-issue-119517.GXgQNl.rst b/Misc/NEWS.d/next/Core and Builtins/2024-06-07-16-42-17.gh-issue-119517.GXgQNl.rst new file mode 100644 index 00000000000000..3c4677f786c07e --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-06-07-16-42-17.gh-issue-119517.GXgQNl.rst @@ -0,0 +1,2 @@ +Fix extraneous new lines in the scroll buffer when pasting in the REPL and +make signals work again to interrupt slow operations. Patch by Pablo Galindo From d5823c157e9f1e82a57e3a71e8153b172de6b582 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Fri, 7 Jun 2024 16:47:03 +0200 Subject: [PATCH 2/3] Increase buffer size Signed-off-by: Pablo Galindo --- Lib/_pyrepl/reader.py | 2 +- Lib/_pyrepl/unix_console.py | 5 +++-- Lib/test/test_pyrepl/support.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 7fde51d2b08d9f..b42f79392b5023 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -256,7 +256,7 @@ def __post_init__(self) -> None: self.lxy = (self.pos, 0) self.calc_screen = self.calc_complete_screen self.can_colorize = can_colorize() - + def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]: return default_keymap diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 4a03a4cc2baf82..ec7039ca9f1076 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -369,9 +369,10 @@ def get_event(self, block: bool = True) -> Event | None: Returns: - Event: Event object from the event queue. """ + BUFFER_SIZE = 1024*100 if self.wait(timeout=0): try: - chars = os.read(self.input_fd, 1024) + chars = os.read(self.input_fd, BUFFER_SIZE) for char in chars: self.push_char(char) except OSError as err: @@ -381,7 +382,7 @@ def get_event(self, block: bool = True) -> Event | None: while self.event_queue.empty(): while True: try: - chars = os.read(self.input_fd, 1024) + chars = os.read(self.input_fd, BUFFER_SIZE) for char in chars: self.push_char(char) except OSError as err: diff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py index 70e12286f7d781..618e5681882703 100644 --- a/Lib/test/test_pyrepl/support.py +++ b/Lib/test/test_pyrepl/support.py @@ -103,7 +103,7 @@ def getpending(self) -> Event: def getheightwidth(self) -> tuple[int, int]: return self.height, self.width - def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: + def refresh(self, screen: list[str], xy: tuple[int, int], scroll:bool = True) -> None: pass def prepare(self) -> None: From 7808b634f50cf805b1f78fa362788d82f6796293 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sat, 8 Jun 2024 11:57:25 +0200 Subject: [PATCH 3/3] Update support.py Co-authored-by: Nikita Sobolev --- Lib/test/test_pyrepl/support.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py index 618e5681882703..03414b3e88972f 100644 --- a/Lib/test/test_pyrepl/support.py +++ b/Lib/test/test_pyrepl/support.py @@ -103,7 +103,7 @@ def getpending(self) -> Event: def getheightwidth(self) -> tuple[int, int]: return self.height, self.width - def refresh(self, screen: list[str], xy: tuple[int, int], scroll:bool = True) -> None: + def refresh(self, screen: list[str], xy: tuple[int, int], scroll: bool = True) -> None: pass def prepare(self) -> None: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy