Skip to content

Commit 2a7dcf0

Browse files
JanEricNitschkeambv
authored andcommitted
gh-128067: Fix pyrepl overriding printed output without newlines (GH-138732)
(cherry picked from commit 8a2deea) Co-authored-by: Jan-Eric Nitschke <47750513+JanEricNitschke@users.noreply.github.com> Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent dc16c7e commit 2a7dcf0

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

@@ -514,7 +515,7 @@ def clear(self) -> None:
514515
"""Wipe the screen"""
515516
self.__write(CLEAR)
516517
self.posxy = 0, 0
517-
self.screen = [""]
518+
self.screen = []
518519

519520
def finish(self) -> None:
520521
"""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
@@ -1836,6 +1836,44 @@ def test_showrefcount(self):
18361836
self.assertEqual(len(matches), 3)
18371837

18381838

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

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
@@ -1344,6 +1344,7 @@ Gustavo Niemeyer
13441344
Oscar Nierstrasz
13451345
Lysandros Nikolaou
13461346
Hrvoje Nikšić
1347+
Jan-Eric Nitschke
13471348
Gregory Nofi
13481349
Jesse Noller
13491350
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)