Skip to content

Commit c09d6a4

Browse files
miss-islingtonJanEricNitschkeambv
authored
[3.13] gh-128067: Fix pyrepl overriding printed output without newlines (GH-138732) (GH-143351)
(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 08c5bfe commit c09d6a4

File tree

7 files changed

+104
-6
lines changed

7 files changed

+104
-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: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,72 @@ 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+
# Build patterns for escape sequences that don't affect cursor position
1429+
# or visual output. Use terminfo to get platform-specific sequences,
1430+
# falling back to hard-coded patterns for capabilities not in terminfo.
1431+
try:
1432+
from _pyrepl import curses
1433+
except ImportError:
1434+
self.skipTest("curses required for capability discovery")
1435+
1436+
curses.setupterm(os.environ.get("TERM", ""), 1)
1437+
safe_patterns = []
1438+
1439+
# smkx/rmkx - application cursor keys and keypad mode
1440+
smkx = curses.tigetstr("smkx")
1441+
rmkx = curses.tigetstr("rmkx")
1442+
if smkx:
1443+
safe_patterns.append(re.escape(smkx.decode("ascii")))
1444+
if rmkx:
1445+
safe_patterns.append(re.escape(rmkx.decode("ascii")))
1446+
if not smkx and not rmkx:
1447+
safe_patterns.append(r'\x1b\[\?1[hl]') # application cursor keys
1448+
safe_patterns.append(r'\x1b[=>]') # application keypad mode
1449+
1450+
# ich1 - insert character (only safe form that inserts exactly 1 char)
1451+
ich1 = curses.tigetstr("ich1")
1452+
if ich1:
1453+
safe_patterns.append(re.escape(ich1.decode("ascii")) + r'(?=[ -~])')
1454+
else:
1455+
safe_patterns.append(r'\x1b\[(?:1)?@(?=[ -~])')
1456+
1457+
# civis/cnorm - cursor visibility (may include cursor blinking control)
1458+
civis = curses.tigetstr("civis")
1459+
cnorm = curses.tigetstr("cnorm")
1460+
if civis:
1461+
safe_patterns.append(re.escape(civis.decode("ascii")))
1462+
if cnorm:
1463+
safe_patterns.append(re.escape(cnorm.decode("ascii")))
1464+
if not civis and not cnorm:
1465+
safe_patterns.append(r'\x1b\[\?25[hl]') # cursor visibility
1466+
safe_patterns.append(r'\x1b\[\?12[hl]') # cursor blinking
1467+
1468+
# Modern extensions not in standard terminfo - always use patterns
1469+
safe_patterns.append(r'\x1b\[\?2004[hl]') # bracketed paste mode
1470+
safe_patterns.append(r'\x1b\[\?12[hl]') # cursor blinking (may be separate)
1471+
safe_patterns.append(r'\x1b\[\?[01]c') # device attributes
1472+
1473+
safe_escapes = re.compile('|'.join(safe_patterns))
1474+
cleaned_output = safe_escapes.sub('', output)
1475+
self.assertIn(expected_output_sequence, cleaned_output)
1476+
1477+
14121478
class TestPyReplCtrlD(TestCase):
14131479
"""Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.
14141480

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)