diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index cf8b1a6c1e4ec4d..ec712253672fe97 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -504,15 +504,19 @@ def do(self) -> None: class perform_bracketed_paste(Command): def do(self) -> None: - done = "\x1b[201~" - data = "" + done = b"\x1b[201~" + data = b"" start = time.time() while done not in data: ev = self.reader.console.getpending() - data += ev.data + data += ev.raw trace( - "bracketed pasting of {l} chars done in {s:.2f}s", + "bracketed pasting of {l} bytes done in {s:.2f}s", l=len(data), s=time.time() - start, ) - self.reader.insert(data.replace(done, "")) + pasted, _, rest = data.partition(done) + self.reader.insert(pasted.decode(self.reader.console.encoding, "replace")) + + for byte in rest: + self.reader.console.push_char(byte) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 65eacf90fde685d..bbd9b48e21116f4 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -627,7 +627,8 @@ def push_char(self, char: int | bytes) -> None: """ Push a character to the console event queue. """ - raise NotImplementedError("push_char not supported on Windows") + trace("push char {char!r}", char=char) + self.event_queue.push(char) def beep(self) -> None: self.__write("\x07") @@ -671,6 +672,7 @@ def getpending(self) -> Event: e2 = self.event_queue.get() if e2: e.data += e2.data + e.raw += e2.raw recs, rec_count = self._read_input_bulk(1024) for i in range(rec_count): @@ -689,6 +691,7 @@ def getpending(self) -> Event: if ch == "\r": ch = "\n" e.data += ch + e.raw += ch.encode(self.event_queue.encoding, "replace") return e def wait_for_event(self, timeout: float | None) -> bool: diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 7cc178f19df6f9d..f5d131aa3e63e55 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -2202,6 +2202,20 @@ def test_python_basic_repl(self): self.assertNotIn("Exception", output) self.assertNotIn("Traceback", output) + def test_bracketed_paste_newline_after_end_marker(self): + # A newline arriving in the same read as the closing marker must + # execute the pasted block instead of being inserted as text. + # See #156186. + env = os.environ.copy() + commands = ("\x1b[200~x = 1\nprint(f'^{x=}')\n\x1b[201~" + "\n" + "exit()\n") + output, exit_code = self.run_repl(commands, env=env, skip=True) + self.assertEqual(exit_code, 0) + self.assertIn("^x=1", output) + self.assertNotIn("Exception", output) + self.assertNotIn("Traceback", output) + @force_not_colorized def test_no_pyrepl_source_in_exc(self): # Avoid using _pyrepl/__main__.py in traceback reports diff --git a/Misc/NEWS.d/next/Library/2026-09-10-09-43-11.gh-issue-156186.yeOh4o.rst b/Misc/NEWS.d/next/Library/2026-09-10-09-43-11.gh-issue-156186.yeOh4o.rst new file mode 100644 index 000000000000000..3df5568b444ee03 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-10-09-43-11.gh-issue-156186.yeOh4o.rst @@ -0,0 +1,2 @@ +Fix PyREPL so that a newline sent right after a pasted block runs the block, +instead of being added to the pasted text.