From 074d743eb19a29a5287c4cbe2755dd4a3e1a1b53 Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke Date: Wed, 10 Sep 2025 09:41:42 +0200 Subject: [PATCH 1/7] Fix pyrepl overriding printed output without newlines --- Lib/_pyrepl/unix_console.py | 7 ++++--- Lib/_pyrepl/windows_console.py | 5 +++-- Lib/test/test_pyrepl/test_pyrepl.py | 30 +++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index a7e49923191c07..950ecabc925911 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -235,8 +235,9 @@ def refresh(self, screen, c_xy): if not self.__gone_tall: while len(self.screen) < min(len(screen), self.height): self.__hide_cursor() - self.__move(0, len(self.screen) - 1) - self.__write("\n") + if self.screen: + self.__move(0, len(self.screen) - 1) + self.__write("\n") self.posxy = 0, len(self.screen) self.screen.append("") else: @@ -785,7 +786,7 @@ def __tputs(self, fmt, prog=delayprog): will never do anyone any good.""" # using .get() means that things will blow up # only if the bps is actually needed (which I'm - # betting is pretty unlkely) + # betting is pretty unlikely) bps = ratedict.get(self.__svtermstate.ospeed) while True: m = prog.search(fmt) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index c56dcd6d7dd434..8dd203bba08436 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -183,8 +183,9 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: while len(self.screen) < min(len(screen), self.height): self._hide_cursor() - self._move_relative(0, len(self.screen) - 1) - self.__write("\n") + if self.screen: + self._move_relative(0, len(self.screen) - 1) + self.__write("\n") self.posxy = 0, len(self.screen) self.screen.append("") diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 8e4450fdf99ecd..23db179a064af5 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1787,3 +1787,33 @@ def test_detect_pip_usage_in_repl(self): " outside of the Python REPL" ) self.assertIn(hint, output) + + @force_not_colorized + def test_no_newline(self): + env = os.environ.copy() + env.pop("PYTHON_BASIC_REPL", "") + env["PYTHON_BASIC_REPL"] = "1" + expected_output_sequence = "Something pretty long>>> exit()" + basic_output, basic_exit_code = self.run_repl("print('Something pretty long', end='')\nexit()\n", env=env) + self.assertEqual(basic_exit_code, 0) + self.assertIn(expected_output_sequence, basic_output) + + output, exit_code = self.run_repl("print('Something pretty long', end='')\nexit()\n") + self.assertEqual(exit_code, 0) + + # Define escape sequences that don't affect cursor position or visual output + bracketed_paste_mode = r'\x1b\[\?2004[hl]' # Enable/disable bracketed paste + application_cursor_keys = r'\x1b\[\?1[hl]' # Enable/disable application cursor keys + application_keypad_mode = r'\x1b[=>]' # Enable/disable application keypad + insert_character = r'\x1b\[\d+@' # Insert character sequences + cursor_visibility = r'\x1b\[\?25[hl]' # Show/hide cursor + cursor_blinking = r'\x1b\[\?12[hl]' # Start/stop cursor blinking + + # Remove only non-visual terminal control sequences (NOT cursor movement) + cleaned_output = re.sub( + f'{bracketed_paste_mode}|{application_cursor_keys}|{application_keypad_mode}|{insert_character}|{cursor_visibility}|{cursor_blinking}', + '', + output + ) + + self.assertIn(expected_output_sequence, cleaned_output) From 771b387d9abe0d2cef6b8dd68da2fd5bc3175925 Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke Date: Wed, 10 Sep 2025 13:41:02 +0200 Subject: [PATCH 2/7] Define more save escape sequences --- Lib/test/test_pyrepl/test_pyrepl.py | 33 +++++++++++++++++------------ 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 23db179a064af5..920786452c4b76 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1793,27 +1793,34 @@ def test_no_newline(self): env = os.environ.copy() env.pop("PYTHON_BASIC_REPL", "") env["PYTHON_BASIC_REPL"] = "1" + + commands = "print('Something pretty long', end='')\nexit()\n" expected_output_sequence = "Something pretty long>>> exit()" - basic_output, basic_exit_code = self.run_repl("print('Something pretty long', end='')\nexit()\n", env=env) + + basic_output, basic_exit_code = self.run_repl(commands, env=env) self.assertEqual(basic_exit_code, 0) self.assertIn(expected_output_sequence, basic_output) - output, exit_code = self.run_repl("print('Something pretty long', end='')\nexit()\n") + output, exit_code = self.run_repl(commands) self.assertEqual(exit_code, 0) # Define escape sequences that don't affect cursor position or visual output - bracketed_paste_mode = r'\x1b\[\?2004[hl]' # Enable/disable bracketed paste + bracketed_paste_mode = r'\x1b\[\?2004[hl]' # Enable/disable bracketed paste application_cursor_keys = r'\x1b\[\?1[hl]' # Enable/disable application cursor keys - application_keypad_mode = r'\x1b[=>]' # Enable/disable application keypad - insert_character = r'\x1b\[\d+@' # Insert character sequences + application_keypad_mode = r'\x1b[=>]' # Enable/disable application keypad + insert_character = r'\x1b\[1@(?=[ -~])' # Insert exactly 1 char (safe form) cursor_visibility = r'\x1b\[\?25[hl]' # Show/hide cursor cursor_blinking = r'\x1b\[\?12[hl]' # Start/stop cursor blinking - - # Remove only non-visual terminal control sequences (NOT cursor movement) - cleaned_output = re.sub( - f'{bracketed_paste_mode}|{application_cursor_keys}|{application_keypad_mode}|{insert_character}|{cursor_visibility}|{cursor_blinking}', - '', - output - ) - + device_attributes = r'\x1b\[\?[01]c' # Device Attributes (DA) queries/responses + + safe_escapes = re.compile( + f'{bracketed_paste_mode}|' + f'{application_cursor_keys}|' + f'{application_keypad_mode}|' + f'{insert_character}|' + f'{cursor_visibility}|' + f'{cursor_blinking}|' + f'{device_attributes}' + ) + cleaned_output = safe_escapes.sub('', output) self.assertIn(expected_output_sequence, cleaned_output) From 011a7ca13b5145073f8b07ccce34fe34ccc251c7 Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke Date: Wed, 10 Sep 2025 15:13:14 +0200 Subject: [PATCH 3/7] Fix allowed character insert escape --- Lib/test/test_pyrepl/test_pyrepl.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 920786452c4b76..87ceea47f93280 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1805,13 +1805,13 @@ def test_no_newline(self): self.assertEqual(exit_code, 0) # Define escape sequences that don't affect cursor position or visual output - bracketed_paste_mode = r'\x1b\[\?2004[hl]' # Enable/disable bracketed paste - application_cursor_keys = r'\x1b\[\?1[hl]' # Enable/disable application cursor keys - application_keypad_mode = r'\x1b[=>]' # Enable/disable application keypad - insert_character = r'\x1b\[1@(?=[ -~])' # Insert exactly 1 char (safe form) - cursor_visibility = r'\x1b\[\?25[hl]' # Show/hide cursor - cursor_blinking = r'\x1b\[\?12[hl]' # Start/stop cursor blinking - device_attributes = r'\x1b\[\?[01]c' # Device Attributes (DA) queries/responses + bracketed_paste_mode = r'\x1b\[\?2004[hl]' # Enable/disable bracketed paste + application_cursor_keys = r'\x1b\[\?1[hl]' # Enable/disable application cursor keys + application_keypad_mode = r'\x1b[=>]' # Enable/disable application keypad + insert_character = r'\x1b\[(?:1)?@(?=[ -~])' # Insert exactly 1 char (safe form) + cursor_visibility = r'\x1b\[\?25[hl]' # Show/hide cursor + cursor_blinking = r'\x1b\[\?12[hl]' # Start/stop cursor blinking + device_attributes = r'\x1b\[\?[01]c' # Device Attributes (DA) queries/responses safe_escapes = re.compile( f'{bracketed_paste_mode}|' From 713b649b4b763f90f382eb4b2fb4f714294a83d9 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:35:45 +0000 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Windows/2025-09-14-13-35-44.gh-issue-128067.BGdP_A.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Windows/2025-09-14-13-35-44.gh-issue-128067.BGdP_A.rst diff --git a/Misc/NEWS.d/next/Windows/2025-09-14-13-35-44.gh-issue-128067.BGdP_A.rst b/Misc/NEWS.d/next/Windows/2025-09-14-13-35-44.gh-issue-128067.BGdP_A.rst new file mode 100644 index 00000000000000..f68cda21db7f03 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2025-09-14-13-35-44.gh-issue-128067.BGdP_A.rst @@ -0,0 +1 @@ +Fix a bug in PyREPL on Windows where output without a trailing newline was overwritten by the next prompt. From a43a319b0468773061344cacdb90e002f06d0c56 Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke Date: Sun, 14 Sep 2025 16:05:11 +0200 Subject: [PATCH 5/7] Add dedicated unittests for unix and windows console --- Lib/test/test_pyrepl/test_unix_console.py | 14 ++++++++++++++ Lib/test/test_pyrepl/test_windows_console.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py index ab1236768cfb3e..992f46ddf41c06 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -96,6 +96,20 @@ def unix_console(events, **kwargs): @patch("os.write") @force_not_colorized_test_class class TestConsole(TestCase): + def test_no_newline(self, _os_write): + code = "1" + events = code_to_events(code) + _, con = handle_events_unix_console(events) + self.assertNotIn(call(ANY, b'\n'), _os_write.mock_calls) + con.restore() + + def test_newline(self, _os_write): + code = "\n" + events = code_to_events(code) + _, con = handle_events_unix_console(events) + _os_write.assert_any_call(ANY, b"\n") + con.restore() + def test_simple_addition(self, _os_write): code = "12+34" events = code_to_events(code) diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index f9607e02c604ff..065706472e52be 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -72,6 +72,20 @@ def handle_events_short(self, events, **kwargs): def handle_events_height_3(self, events): return self.handle_events(events, height=3) + def test_no_newline(self): + code = "1" + events = code_to_events(code) + _, con = self.handle_events(events) + self.assertNotIn(call(b'\n'), con.out.write.mock_calls) + con.restore() + + def test_newline(self): + code = "\n" + events = code_to_events(code) + _, con = self.handle_events(events) + con.out.write.assert_any_call(b"\n") + con.restore() + def test_simple_addition(self): code = "12+34" events = code_to_events(code) From 8c75345370cec7d1a90afe4da368fa6d171e6fd5 Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke Date: Sun, 14 Sep 2025 19:57:40 +0200 Subject: [PATCH 6/7] Harmonize clear between windows and unix --- Lib/_pyrepl/windows_console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 8dd203bba08436..1c4c7d434afc80 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -515,7 +515,7 @@ def clear(self) -> None: """Wipe the screen""" self.__write(CLEAR) self.posxy = 0, 0 - self.screen = [""] + self.screen = [] def finish(self) -> None: """Move the cursor to the end of the display and otherwise get From 3308891a1970d3d618d626c9debb3ba2fb6c7a9a Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke Date: Tue, 16 Sep 2025 19:35:34 +0200 Subject: [PATCH 7/7] Add myself to ACKS --- Misc/ACKS | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/ACKS b/Misc/ACKS index 37b7988606fa99..beee9188c10ac6 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1346,6 +1346,7 @@ Gustavo Niemeyer Oscar Nierstrasz Lysandros Nikolaou Hrvoje Nikšić +Jan-Eric Nitschke Gregory Nofi Jesse Noller Bill Noon