Skip to content

Commit 1686927

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 4a47751 commit 1686927

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
@@ -258,8 +258,9 @@ def refresh(self, screen, c_xy):
258258
if not self.__gone_tall:
259259
while len(self.screen) < min(len(screen), self.height):
260260
self.__hide_cursor()
261-
self.__move(0, len(self.screen) - 1)
262-
self.__write("\n")
261+
if self.screen:
262+
self.__move(0, len(self.screen) - 1)
263+
self.__write("\n")
263264
self.posxy = 0, len(self.screen)
264265
self.screen.append("")
265266
else:
@@ -817,7 +818,7 @@ def __tputs(self, fmt, prog=delayprog):
817818
will never do anyone any good."""
818819
# using .get() means that things will blow up
819820
# only if the bps is actually needed (which I'm
820-
# betting is pretty unlkely)
821+
# betting is pretty unlikely)
821822
bps = ratedict.get(self.__svtermstate.ospeed)
822823
while 1:
823824
m = prog.search(fmt)

Lib/_pyrepl/windows_console.py

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

172172
while len(self.screen) < min(len(screen), self.height):
173173
self._hide_cursor()
174-
self._move_relative(0, len(self.screen) - 1)
175-
self.__write("\n")
174+
if self.screen:
175+
self._move_relative(0, len(self.screen) - 1)
176+
self.__write("\n")
176177
self.posxy = 0, len(self.screen)
177178
self.screen.append("")
178179

@@ -498,7 +499,7 @@ def clear(self) -> None:
498499
"""Wipe the screen"""
499500
self.__write(CLEAR)
500501
self.posxy = 0, 0
501-
self.screen = [""]
502+
self.screen = []
502503

503504
def finish(self) -> None:
504505
"""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
@@ -1409,6 +1409,44 @@ def test_showrefcount(self):
14091409
self.assertEqual(len(matches), 3)
14101410

14111411

1412+
@force_not_colorized
1413+
def test_no_newline(self):
1414+
env = os.environ.copy()
1415+
env.pop("PYTHON_BASIC_REPL", "")
1416+
env["PYTHON_BASIC_REPL"] = "1"
1417+
1418+
commands = "print('Something pretty long', end='')\nexit()\n"
1419+
expected_output_sequence = "Something pretty long>>> exit()"
1420+
1421+
basic_output, basic_exit_code = self.run_repl(commands, env=env)
1422+
self.assertEqual(basic_exit_code, 0)
1423+
self.assertIn(expected_output_sequence, basic_output)
1424+
1425+
output, exit_code = self.run_repl(commands)
1426+
self.assertEqual(exit_code, 0)
1427+
1428+
# Define escape sequences that don't affect cursor position or visual output
1429+
bracketed_paste_mode = r'\x1b\[\?2004[hl]' # Enable/disable bracketed paste
1430+
application_cursor_keys = r'\x1b\[\?1[hl]' # Enable/disable application cursor keys
1431+
application_keypad_mode = r'\x1b[=>]' # Enable/disable application keypad
1432+
insert_character = r'\x1b\[(?:1)?@(?=[ -~])' # Insert exactly 1 char (safe form)
1433+
cursor_visibility = r'\x1b\[\?25[hl]' # Show/hide cursor
1434+
cursor_blinking = r'\x1b\[\?12[hl]' # Start/stop cursor blinking
1435+
device_attributes = r'\x1b\[\?[01]c' # Device Attributes (DA) queries/responses
1436+
1437+
safe_escapes = re.compile(
1438+
f'{bracketed_paste_mode}|'
1439+
f'{application_cursor_keys}|'
1440+
f'{application_keypad_mode}|'
1441+
f'{insert_character}|'
1442+
f'{cursor_visibility}|'
1443+
f'{cursor_blinking}|'
1444+
f'{device_attributes}'
1445+
)
1446+
cleaned_output = safe_escapes.sub('', output)
1447+
self.assertIn(expected_output_sequence, cleaned_output)
1448+
1449+
14121450
class TestPyReplCtrlD(TestCase):
14131451
"""Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.
14141452

Lib/test/test_pyrepl/test_unix_console.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,20 @@ def unix_console(events, **kwargs):
125125
@patch("termios.tcsetattr", lambda a, b, c: None)
126126
@patch("os.write")
127127
class TestConsole(TestCase):
128+
def test_no_newline(self, _os_write):
129+
code = "1"
130+
events = code_to_events(code)
131+
_, con = handle_events_unix_console(events)
132+
self.assertNotIn(call(ANY, b'\n'), _os_write.mock_calls)
133+
con.restore()
134+
135+
def test_newline(self, _os_write):
136+
code = "\n"
137+
events = code_to_events(code)
138+
_, con = handle_events_unix_console(events)
139+
_os_write.assert_any_call(ANY, b"\n")
140+
con.restore()
141+
128142
def test_simple_addition(self, _os_write):
129143
code = "12+34"
130144
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
@@ -59,6 +59,20 @@ def handle_events_short(self, events):
5959
def handle_events_height_3(self, events):
6060
return self.handle_events(events, height=3)
6161

62+
def test_no_newline(self):
63+
code = "1"
64+
events = code_to_events(code)
65+
_, con = self.handle_events(events)
66+
self.assertNotIn(call(b'\n'), con.out.write.mock_calls)
67+
con.restore()
68+
69+
def test_newline(self):
70+
code = "\n"
71+
events = code_to_events(code)
72+
_, con = self.handle_events(events)
73+
con.out.write.assert_any_call(b"\n")
74+
con.restore()
75+
6276
def test_simple_addition(self):
6377
code = "12+34"
6478
events = code_to_events(code)

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,7 @@ Gustavo Niemeyer
13241324
Oscar Nierstrasz
13251325
Lysandros Nikolaou
13261326
Hrvoje Nikšić
1327+
Jan-Eric Nitschke
13271328
Gregory Nofi
13281329
Jesse Noller
13291330
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)