Skip to content

Commit a141b84

Browse files
miss-islingtonJanEricNitschkeambv
authored
[3.14] gh-128067: Fix pyrepl overriding printed output without newlines (GH-138732) (GH-143350)
(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 a141b84

File tree

7 files changed

+101
-6
lines changed

7 files changed

+101
-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: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1836,6 +1836,69 @@ 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+
# Build patterns for escape sequences that don't affect cursor position
1856+
# or visual output. Use terminfo to get platform-specific sequences,
1857+
# falling back to hard-coded patterns for capabilities not in terminfo.
1858+
from _pyrepl.terminfo import TermInfo
1859+
ti = TermInfo(os.environ.get("TERM", ""))
1860+
1861+
safe_patterns = []
1862+
1863+
# smkx/rmkx - application cursor keys and keypad mode
1864+
smkx = ti.get("smkx")
1865+
rmkx = ti.get("rmkx")
1866+
if smkx:
1867+
safe_patterns.append(re.escape(smkx.decode("ascii")))
1868+
if rmkx:
1869+
safe_patterns.append(re.escape(rmkx.decode("ascii")))
1870+
if not smkx and not rmkx:
1871+
safe_patterns.append(r'\x1b\[\?1[hl]') # application cursor keys
1872+
safe_patterns.append(r'\x1b[=>]') # application keypad mode
1873+
1874+
# ich1 - insert character (only safe form that inserts exactly 1 char)
1875+
ich1 = ti.get("ich1")
1876+
if ich1:
1877+
safe_patterns.append(re.escape(ich1.decode("ascii")) + r'(?=[ -~])')
1878+
else:
1879+
safe_patterns.append(r'\x1b\[(?:1)?@(?=[ -~])')
1880+
1881+
# civis/cnorm - cursor visibility (may include cursor blinking control)
1882+
civis = ti.get("civis")
1883+
cnorm = ti.get("cnorm")
1884+
if civis:
1885+
safe_patterns.append(re.escape(civis.decode("ascii")))
1886+
if cnorm:
1887+
safe_patterns.append(re.escape(cnorm.decode("ascii")))
1888+
if not civis and not cnorm:
1889+
safe_patterns.append(r'\x1b\[\?25[hl]') # cursor visibility
1890+
safe_patterns.append(r'\x1b\[\?12[hl]') # cursor blinking
1891+
1892+
# Modern extensions not in standard terminfo - always use patterns
1893+
safe_patterns.append(r'\x1b\[\?2004[hl]') # bracketed paste mode
1894+
safe_patterns.append(r'\x1b\[\?12[hl]') # cursor blinking (may be separate)
1895+
safe_patterns.append(r'\x1b\[\?[01]c') # device attributes
1896+
1897+
safe_escapes = re.compile('|'.join(safe_patterns))
1898+
cleaned_output = safe_escapes.sub('', output)
1899+
self.assertIn(expected_output_sequence, cleaned_output)
1900+
1901+
18391902
class TestPyReplCtrlD(TestCase):
18401903
"""Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.
18411904

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)