Skip to content

Commit 8a2deea

Browse files
gh-128067: Fix pyrepl overriding printed output without newlines (#138732)
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent 09ce592 commit 8a2deea

File tree

7 files changed

+76
-6
lines changed

7 files changed

+76
-6
lines changed

Lib/_pyrepl/unix_console.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,9 @@ def refresh(self, screen, c_xy):
251251
if not self.__gone_tall:
252252
while len(self.screen) < min(len(screen), self.height):
253253
self.__hide_cursor()
254-
self.__move(0, len(self.screen) - 1)
255-
self.__write("\n")
254+
if self.screen:
255+
self.__move(0, len(self.screen) - 1)
256+
self.__write("\n")
256257
self.posxy = 0, len(self.screen)
257258
self.screen.append("")
258259
else:
@@ -808,7 +809,7 @@ def __tputs(self, fmt, prog=delayprog):
808809
will never do anyone any good."""
809810
# using .get() means that things will blow up
810811
# only if the bps is actually needed (which I'm
811-
# betting is pretty unlkely)
812+
# betting is pretty unlikely)
812813
bps = ratedict.get(self.__svtermstate.ospeed)
813814
while True:
814815
m = prog.search(fmt)

Lib/_pyrepl/windows_console.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,9 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
183183

184184
while len(self.screen) < min(len(screen), self.height):
185185
self._hide_cursor()
186-
self._move_relative(0, len(self.screen) - 1)
187-
self.__write("\n")
186+
if self.screen:
187+
self._move_relative(0, len(self.screen) - 1)
188+
self.__write("\n")
188189
self.posxy = 0, len(self.screen)
189190
self.screen.append("")
190191

@@ -501,7 +502,7 @@ def clear(self) -> None:
501502
"""Wipe the screen"""
502503
self.__write(CLEAR)
503504
self.posxy = 0, 0
504-
self.screen = [""]
505+
self.screen = []
505506

506507
def finish(self) -> None:
507508
"""Move the cursor to the end of the display and otherwise get

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,6 +1846,44 @@ def test_detect_pip_usage_in_repl(self):
18461846
)
18471847
self.assertIn(hint, output)
18481848

1849+
@force_not_colorized
1850+
def test_no_newline(self):
1851+
env = os.environ.copy()
1852+
env.pop("PYTHON_BASIC_REPL", "")
1853+
env["PYTHON_BASIC_REPL"] = "1"
1854+
1855+
commands = "print('Something pretty long', end='')\nexit()\n"
1856+
expected_output_sequence = "Something pretty long>>> exit()"
1857+
1858+
basic_output, basic_exit_code = self.run_repl(commands, env=env)
1859+
self.assertEqual(basic_exit_code, 0)
1860+
self.assertIn(expected_output_sequence, basic_output)
1861+
1862+
output, exit_code = self.run_repl(commands)
1863+
self.assertEqual(exit_code, 0)
1864+
1865+
# Define escape sequences that don't affect cursor position or visual output
1866+
bracketed_paste_mode = r'\x1b\[\?2004[hl]' # Enable/disable bracketed paste
1867+
application_cursor_keys = r'\x1b\[\?1[hl]' # Enable/disable application cursor keys
1868+
application_keypad_mode = r'\x1b[=>]' # Enable/disable application keypad
1869+
insert_character = r'\x1b\[(?:1)?@(?=[ -~])' # Insert exactly 1 char (safe form)
1870+
cursor_visibility = r'\x1b\[\?25[hl]' # Show/hide cursor
1871+
cursor_blinking = r'\x1b\[\?12[hl]' # Start/stop cursor blinking
1872+
device_attributes = r'\x1b\[\?[01]c' # Device Attributes (DA) queries/responses
1873+
1874+
safe_escapes = re.compile(
1875+
f'{bracketed_paste_mode}|'
1876+
f'{application_cursor_keys}|'
1877+
f'{application_keypad_mode}|'
1878+
f'{insert_character}|'
1879+
f'{cursor_visibility}|'
1880+
f'{cursor_blinking}|'
1881+
f'{device_attributes}'
1882+
)
1883+
cleaned_output = safe_escapes.sub('', output)
1884+
self.assertIn(expected_output_sequence, cleaned_output)
1885+
1886+
18491887
class TestPyReplCtrlD(TestCase):
18501888
"""Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.
18511889

Lib/test/test_pyrepl/test_unix_console.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,20 @@ def unix_console(events, **kwargs):
102102
@patch("os.write")
103103
@force_not_colorized_test_class
104104
class TestConsole(TestCase):
105+
def test_no_newline(self, _os_write):
106+
code = "1"
107+
events = code_to_events(code)
108+
_, con = handle_events_unix_console(events)
109+
self.assertNotIn(call(ANY, b'\n'), _os_write.mock_calls)
110+
con.restore()
111+
112+
def test_newline(self, _os_write):
113+
code = "\n"
114+
events = code_to_events(code)
115+
_, con = handle_events_unix_console(events)
116+
_os_write.assert_any_call(ANY, b"\n")
117+
con.restore()
118+
105119
def test_simple_addition(self, _os_write):
106120
code = "12+34"
107121
events = code_to_events(code)

Lib/test/test_pyrepl/test_windows_console.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,20 @@ def handle_events_short(self, events, **kwargs):
7272
def handle_events_height_3(self, events):
7373
return self.handle_events(events, height=3)
7474

75+
def test_no_newline(self):
76+
code = "1"
77+
events = code_to_events(code)
78+
_, con = self.handle_events(events)
79+
self.assertNotIn(call(b'\n'), con.out.write.mock_calls)
80+
con.restore()
81+
82+
def test_newline(self):
83+
code = "\n"
84+
events = code_to_events(code)
85+
_, con = self.handle_events(events)
86+
con.out.write.assert_any_call(b"\n")
87+
con.restore()
88+
7589
def test_simple_addition(self):
7690
code = "12+34"
7791
events = code_to_events(code)

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,7 @@ Gustavo Niemeyer
13521352
Oscar Nierstrasz
13531353
Lysandros Nikolaou
13541354
Hrvoje Nikšić
1355+
Jan-Eric Nitschke
13551356
Gregory Nofi
13561357
Jesse Noller
13571358
Bill Noon
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug in PyREPL on Windows where output without a trailing newline was overwritten by the next prompt.

0 commit comments

Comments
 (0)