Skip to content

Commit a38b239

Browse files
committed
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 <[email protected]>
1 parent 4055577 commit a38b239

File tree

6 files changed

+56
-35
lines changed

6 files changed

+56
-35
lines changed

Lib/_pyrepl/commands.py

+1
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,4 @@ def do(self) -> None:
482482
self.reader.in_bracketed_paste = False
483483
self.reader.dirty = True
484484
self.reader.calc_screen = self.reader.calc_complete_screen
485+
self.reader.scroll_on_next_refresh = False

Lib/_pyrepl/console.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ def __init__(
6969
self.output_fd = f_out.fileno()
7070

7171
@abstractmethod
72-
def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ...
72+
def refresh(self, screen: list[str], xy: tuple[int, int],
73+
scroll: bool = False) -> None: ...
7374

7475
@abstractmethod
7576
def prepare(self) -> None: ...

Lib/_pyrepl/reader.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ class Reader:
240240
lxy: tuple[int, int] = field(init=False)
241241
calc_screen: CalcScreen = field(init=False)
242242
scheduled_commands: list[str] = field(default_factory=list)
243+
can_colorize: bool = False
244+
scroll_on_next_refresh: bool = True
243245

244246
def __post_init__(self) -> None:
245247
# Enable the use of `insert` without a `prepare` call - necessary to
@@ -253,13 +255,16 @@ def __post_init__(self) -> None:
253255
self.cxy = self.pos2xy()
254256
self.lxy = (self.pos, 0)
255257
self.calc_screen = self.calc_complete_screen
256-
258+
self.can_colorize = can_colorize()
259+
257260
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
258261
return default_keymap
259262

260263
def append_to_screen(self) -> list[str]:
261264
new_screen = self.screen.copy() or ['']
262265

266+
if not self.buffer:
267+
return []
263268
new_character = self.buffer[-1]
264269
new_character_len = wlen(new_character)
265270

@@ -468,7 +473,7 @@ def get_prompt(self, lineno: int, cursor_on_line: bool) -> str:
468473
else:
469474
prompt = self.ps1
470475

471-
if can_colorize():
476+
if self.can_colorize:
472477
prompt = f"{ANSIColors.BOLD_MAGENTA}{prompt}{ANSIColors.RESET}"
473478
return prompt
474479

@@ -606,8 +611,9 @@ def refresh(self) -> None:
606611
"""Recalculate and refresh the screen."""
607612
# this call sets up self.cxy, so call it first.
608613
self.screen = self.calc_screen()
609-
self.console.refresh(self.screen, self.cxy)
614+
self.console.refresh(self.screen, self.cxy, scroll=self.scroll_on_next_refresh)
610615
self.dirty = False
616+
self.scroll_on_next_refresh = True
611617

612618
def do_cmd(self, cmd: tuple[str, list[str]]) -> None:
613619
"""`cmd` is a tuple of "event_name" and "event", which in the current

Lib/_pyrepl/unix_console.py

+34-24
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import select
2828
import signal
2929
import struct
30-
import sys
3130
import termios
3231
import time
3332
from fcntl import ioctl
@@ -206,7 +205,7 @@ def change_encoding(self, encoding: str) -> None:
206205
"""
207206
self.encoding = encoding
208207

209-
def refresh(self, screen, c_xy):
208+
def refresh(self, screen, c_xy, scroll=True):
210209
"""
211210
Refresh the console screen.
212211
@@ -248,22 +247,23 @@ def refresh(self, screen, c_xy):
248247
newscr = screen[offset : offset + height]
249248

250249
# use hardware scrolling if we have it.
251-
if old_offset > offset and self._ri:
252-
self.__hide_cursor()
253-
self.__write_code(self._cup, 0, 0)
254-
self.__posxy = 0, old_offset
255-
for i in range(old_offset - offset):
256-
self.__write_code(self._ri)
257-
oldscr.pop(-1)
258-
oldscr.insert(0, "")
259-
elif old_offset < offset and self._ind:
260-
self.__hide_cursor()
261-
self.__write_code(self._cup, self.height - 1, 0)
262-
self.__posxy = 0, old_offset + self.height - 1
263-
for i in range(offset - old_offset):
264-
self.__write_code(self._ind)
265-
oldscr.pop(0)
266-
oldscr.append("")
250+
if scroll:
251+
if old_offset > offset and self._ri:
252+
self.__hide_cursor()
253+
self.__write_code(self._cup, 0, 0)
254+
self.__posxy = 0, old_offset
255+
for i in range(old_offset - offset):
256+
self.__write_code(self._ri)
257+
oldscr.pop(-1)
258+
oldscr.insert(0, "")
259+
elif old_offset < offset and self._ind:
260+
self.__hide_cursor()
261+
self.__write_code(self._cup, self.height - 1, 0)
262+
self.__posxy = 0, old_offset + self.height - 1
263+
for i in range(offset - old_offset):
264+
self.__write_code(self._ind)
265+
oldscr.pop(0)
266+
oldscr.append("")
267267

268268
self.__offset = offset
269269

@@ -310,14 +310,13 @@ def prepare(self):
310310
"""
311311
self.__svtermstate = tcgetattr(self.input_fd)
312312
raw = self.__svtermstate.copy()
313-
raw.iflag &= ~(termios.BRKINT | termios.INPCK | termios.ISTRIP | termios.IXON)
313+
raw.iflag &= ~(termios.INPCK | termios.ISTRIP | termios.IXON)
314314
raw.oflag &= ~(termios.OPOST)
315315
raw.cflag &= ~(termios.CSIZE | termios.PARENB)
316316
raw.cflag |= termios.CS8
317-
raw.lflag &= ~(
318-
termios.ICANON | termios.ECHO | termios.IEXTEN | (termios.ISIG * 1)
319-
)
320-
raw.cc[termios.VMIN] = 1
317+
raw.iflag |= termios.BRKINT
318+
raw.lflag &= ~(termios.ICANON | termios.ECHO | termios.IEXTEN)
319+
raw.lflag |= termios.ISIG
321320
raw.cc[termios.VTIME] = 0
322321
tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
323322

@@ -370,10 +369,21 @@ def get_event(self, block: bool = True) -> Event | None:
370369
Returns:
371370
- Event: Event object from the event queue.
372371
"""
372+
if self.wait(timeout=0):
373+
try:
374+
chars = os.read(self.input_fd, 1024)
375+
for char in chars:
376+
self.push_char(char)
377+
except OSError as err:
378+
if err.errno == errno.EINTR:
379+
raise
380+
373381
while self.event_queue.empty():
374382
while True:
375383
try:
376-
self.push_char(os.read(self.input_fd, 1))
384+
chars = os.read(self.input_fd, 1024)
385+
for char in chars:
386+
self.push_char(char)
377387
except OSError as err:
378388
if err.errno == errno.EINTR:
379389
if not self.event_queue.empty():

Lib/_pyrepl/windows_console.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def __init__(
136136
# Console I/O is redirected, fallback...
137137
self.out = None
138138

139-
def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
139+
def refresh(self, screen: list[str], c_xy: tuple[int, int], scroll: bool = True) -> None:
140140
"""
141141
Refresh the console screen.
142142
@@ -165,12 +165,13 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
165165
offset = cy - height + 1
166166
scroll_lines = offset - old_offset
167167

168-
# Scrolling the buffer as the current input is greater than the visible
169-
# portion of the window. We need to scroll the visible portion and the
170-
# entire history
171-
self._scroll(scroll_lines, self._getscrollbacksize())
172-
self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines
173-
self.__offset += scroll_lines
168+
if scroll:
169+
# Scrolling the buffer as the current input is greater than the visible
170+
# portion of the window. We need to scroll the visible portion and the
171+
# entire history
172+
self._scroll(scroll_lines, self._getscrollbacksize())
173+
self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines
174+
self.__offset += scroll_lines
174175

175176
for i in range(scroll_lines):
176177
self.screen.append("")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix extraneous new lines in the scroll buffer when pasting in the REPL and
2+
make signals work again to interrupt slow operations. Patch by Pablo Galindo

0 commit comments

Comments
 (0)