Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Lib/_pyrepl/unix_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,9 @@ def refresh(self, screen, c_xy):
if not self.__gone_tall:
while len(self.screen) < min(len(screen), self.height):
self.__hide_cursor()
self.__move(0, len(self.screen) - 1)
self.__write("\n")
if self.screen:
self.__move(0, len(self.screen) - 1)
self.__write("\n")
self.posxy = 0, len(self.screen)
self.screen.append("")
else:
Expand Down Expand Up @@ -817,7 +818,7 @@ def __tputs(self, fmt, prog=delayprog):
will never do anyone any good."""
# using .get() means that things will blow up
# only if the bps is actually needed (which I'm
# betting is pretty unlkely)
# betting is pretty unlikely)
bps = ratedict.get(self.__svtermstate.ospeed)
while 1:
m = prog.search(fmt)
Expand Down
7 changes: 4 additions & 3 deletions Lib/_pyrepl/windows_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,9 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:

while len(self.screen) < min(len(screen), self.height):
self._hide_cursor()
self._move_relative(0, len(self.screen) - 1)
self.__write("\n")
if self.screen:
self._move_relative(0, len(self.screen) - 1)
self.__write("\n")
self.posxy = 0, len(self.screen)
self.screen.append("")

Expand Down Expand Up @@ -498,7 +499,7 @@ def clear(self) -> None:
"""Wipe the screen"""
self.__write(CLEAR)
self.posxy = 0, 0
self.screen = [""]
self.screen = []

def finish(self) -> None:
"""Move the cursor to the end of the display and otherwise get
Expand Down
66 changes: 66 additions & 0 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,72 @@ def test_showrefcount(self):
self.assertEqual(len(matches), 3)


@force_not_colorized
def test_no_newline(self):
env = os.environ.copy()
env.pop("PYTHON_BASIC_REPL", "")
env["PYTHON_BASIC_REPL"] = "1"

commands = "print('Something pretty long', end='')\nexit()\n"
expected_output_sequence = "Something pretty long>>> exit()"

basic_output, basic_exit_code = self.run_repl(commands, env=env)
self.assertEqual(basic_exit_code, 0)
self.assertIn(expected_output_sequence, basic_output)

output, exit_code = self.run_repl(commands)
self.assertEqual(exit_code, 0)

# Build patterns for escape sequences that don't affect cursor position
# or visual output. Use terminfo to get platform-specific sequences,
# falling back to hard-coded patterns for capabilities not in terminfo.
try:
from _pyrepl import curses
except ImportError:
self.skipTest("curses required for capability discovery")

curses.setupterm(os.environ.get("TERM", ""), 1)
safe_patterns = []

# smkx/rmkx - application cursor keys and keypad mode
smkx = curses.tigetstr("smkx")
rmkx = curses.tigetstr("rmkx")
if smkx:
safe_patterns.append(re.escape(smkx.decode("ascii")))
if rmkx:
safe_patterns.append(re.escape(rmkx.decode("ascii")))
if not smkx and not rmkx:
safe_patterns.append(r'\x1b\[\?1[hl]') # application cursor keys
safe_patterns.append(r'\x1b[=>]') # application keypad mode

# ich1 - insert character (only safe form that inserts exactly 1 char)
ich1 = curses.tigetstr("ich1")
if ich1:
safe_patterns.append(re.escape(ich1.decode("ascii")) + r'(?=[ -~])')
else:
safe_patterns.append(r'\x1b\[(?:1)?@(?=[ -~])')

# civis/cnorm - cursor visibility (may include cursor blinking control)
civis = curses.tigetstr("civis")
cnorm = curses.tigetstr("cnorm")
if civis:
safe_patterns.append(re.escape(civis.decode("ascii")))
if cnorm:
safe_patterns.append(re.escape(cnorm.decode("ascii")))
if not civis and not cnorm:
safe_patterns.append(r'\x1b\[\?25[hl]') # cursor visibility
safe_patterns.append(r'\x1b\[\?12[hl]') # cursor blinking

# Modern extensions not in standard terminfo - always use patterns
safe_patterns.append(r'\x1b\[\?2004[hl]') # bracketed paste mode
safe_patterns.append(r'\x1b\[\?12[hl]') # cursor blinking (may be separate)
safe_patterns.append(r'\x1b\[\?[01]c') # device attributes

safe_escapes = re.compile('|'.join(safe_patterns))
cleaned_output = safe_escapes.sub('', output)
self.assertIn(expected_output_sequence, cleaned_output)


class TestPyReplCtrlD(TestCase):
"""Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.

Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_pyrepl/test_unix_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,20 @@ def unix_console(events, **kwargs):
@patch("termios.tcsetattr", lambda a, b, c: None)
@patch("os.write")
class TestConsole(TestCase):
def test_no_newline(self, _os_write):
code = "1"
events = code_to_events(code)
_, con = handle_events_unix_console(events)
self.assertNotIn(call(ANY, b'\n'), _os_write.mock_calls)
con.restore()

def test_newline(self, _os_write):
code = "\n"
events = code_to_events(code)
_, con = handle_events_unix_console(events)
_os_write.assert_any_call(ANY, b"\n")
con.restore()

def test_simple_addition(self, _os_write):
code = "12+34"
events = code_to_events(code)
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_pyrepl/test_windows_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ def handle_events_short(self, events):
def handle_events_height_3(self, events):
return self.handle_events(events, height=3)

def test_no_newline(self):
code = "1"
events = code_to_events(code)
_, con = self.handle_events(events)
self.assertNotIn(call(b'\n'), con.out.write.mock_calls)
con.restore()

def test_newline(self):
code = "\n"
events = code_to_events(code)
_, con = self.handle_events(events)
con.out.write.assert_any_call(b"\n")
con.restore()

def test_simple_addition(self):
code = "12+34"
events = code_to_events(code)
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,7 @@ Gustavo Niemeyer
Oscar Nierstrasz
Lysandros Nikolaou
Hrvoje Nikšić
Jan-Eric Nitschke
Gregory Nofi
Jesse Noller
Bill Noon
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a bug in PyREPL on Windows where output without a trailing newline was overwritten by the next prompt.
Loading