Skip to content
7 changes: 4 additions & 3 deletions Lib/_pyrepl/unix_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,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 @@ -808,7 +809,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 True:
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 @@ -183,8 +183,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spot on, len(self.screen) - 1 should not get negative here.

self.__write("\n")
self.posxy = 0, len(self.screen)
self.screen.append("")

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "" here before was a clear attempt at working around the same issue that you're solving here, but I agree the if statement is cleaner and more explicit.

In unix_console.py clear() is setting self.screen to an empty list, but as you noticed this wasn't triggering any bug because the __gone_tall boolean would prevent this path being taken in refresh(). In unit tests that wasn't the case and so they were failing before your fix.

I'm okay with being defensive here, especially that this makes the behavior symmetrical between Windows and Unix.


def finish(self) -> None:
"""Move the cursor to the end of the display and otherwise get
Expand Down
38 changes: 38 additions & 0 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,44 @@ def test_detect_pip_usage_in_repl(self):
)
self.assertIn(hint, output)

@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)

# Define escape sequences that don't affect cursor position or visual output
bracketed_paste_mode = r'\x1b\[\?2004[hl]' # Enable/disable bracketed paste
application_cursor_keys = r'\x1b\[\?1[hl]' # Enable/disable application cursor keys
application_keypad_mode = r'\x1b[=>]' # Enable/disable application keypad
insert_character = r'\x1b\[(?:1)?@(?=[ -~])' # Insert exactly 1 char (safe form)
cursor_visibility = r'\x1b\[\?25[hl]' # Show/hide cursor
cursor_blinking = r'\x1b\[\?12[hl]' # Start/stop cursor blinking
device_attributes = r'\x1b\[\?[01]c' # Device Attributes (DA) queries/responses

safe_escapes = re.compile(
f'{bracketed_paste_mode}|'
f'{application_cursor_keys}|'
f'{application_keypad_mode}|'
f'{insert_character}|'
f'{cursor_visibility}|'
f'{cursor_blinking}|'
f'{device_attributes}'
)
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 @@ -102,6 +102,20 @@ def unix_console(events, **kwargs):
@patch("os.write")
@force_not_colorized_test_class
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 @@ -72,6 +72,20 @@ def handle_events_short(self, events, **kwargs):
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 @@ -1349,6 +1349,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