gh-128067: Fix pyrepl overriding printed output without newlines (#138732)
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py
index 09247de..937b5df 100644
--- a/Lib/_pyrepl/unix_console.py
+++ b/Lib/_pyrepl/unix_console.py
@@ -251,8 +251,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:
@@ -808,7 +809,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 f9f5988..a703a06 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("")
@@ -501,7 +502,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
diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py
index 65a252c..90cadd0 100644
--- a/Lib/test/test_pyrepl/test_pyrepl.py
+++ b/Lib/test/test_pyrepl/test_pyrepl.py
@@ -1846,6 +1846,44 @@ def test_detect_pip_usage_in_repl(self):
)
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"
+
+ commands = "print('Something pretty long', end='')\nexit()\n"
+ expected_output_sequence = "Something pretty long>>> exit()"
+
+ 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(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
+ 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}|'
+ 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)
+
+
class TestPyReplCtrlD(TestCase):
"""Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.
diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py
index f4fb923..680adbc 100644
--- a/Lib/test/test_pyrepl/test_unix_console.py
+++ b/Lib/test/test_pyrepl/test_unix_console.py
@@ -102,6 +102,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 f9607e0..0657064 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)
diff --git a/Misc/ACKS b/Misc/ACKS
index bb6b6bd..671fcf8 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1352,6 +1352,7 @@
Oscar Nierstrasz
Lysandros Nikolaou
Hrvoje Nikšić
+Jan-Eric Nitschke
Gregory Nofi
Jesse Noller
Bill Noon
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 0000000..f68cda2
--- /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.