Skip to content

Commit f6186a3

Browse files
ambvdeepwzhadqm
authored
[3.13] gh-133400: Fixed Ctrl+D (^D) behavior in :mod:_pyrepl module (GH-133883) (GH-139851)
(cherry picked from commit 81959a0) Co-authored-by: DeepWzh <[email protected]> Co-authored-by: adam j hartz <[email protected]>
1 parent d12bd1b commit f6186a3

File tree

3 files changed

+96
-8
lines changed

3 files changed

+96
-8
lines changed

Lib/_pyrepl/commands.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -411,14 +411,17 @@ class delete(EditCommand):
411411
def do(self) -> None:
412412
r = self.reader
413413
b = r.buffer
414-
if (
415-
r.pos == 0
416-
and len(b) == 0 # this is something of a hack
417-
and self.event[-1] == "\004"
418-
):
419-
r.update_screen()
420-
r.console.finish()
421-
raise EOFError
414+
if self.event[-1] == "\004":
415+
if b and b[-1].endswith("\n"):
416+
self.finish = True
417+
elif (
418+
r.pos == 0
419+
and len(b) == 0 # this is something of a hack
420+
):
421+
r.update_screen()
422+
r.console.finish()
423+
raise EOFError
424+
422425
for i in range(r.get_arg()):
423426
if r.pos != len(b):
424427
del b[r.pos]

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,3 +1404,87 @@ def test_showrefcount(self):
14041404
output, _ = self.run_repl("1\n1+2\nexit()\n", cmdline_args=['-Xshowrefcount'], env=env)
14051405
matches = re.findall(r'\[-?\d+ refs, \d+ blocks\]', output)
14061406
self.assertEqual(len(matches), 3)
1407+
1408+
1409+
class TestPyReplCtrlD(TestCase):
1410+
"""Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.
1411+
1412+
Ctrl+D should:
1413+
- Exit on empty buffer (raises EOFError)
1414+
- Delete character when cursor is in middle of line
1415+
- Perform no operation when cursor is at end of line without newline
1416+
- Exit multiline mode when cursor is at end with trailing newline
1417+
- Run code up to that point when pressed on blank line with preceding lines
1418+
"""
1419+
def prepare_reader(self, events):
1420+
console = FakeConsole(events)
1421+
config = ReadlineConfig(readline_completer=None)
1422+
reader = ReadlineAlikeReader(console=console, config=config)
1423+
return reader
1424+
1425+
def test_ctrl_d_empty_line(self):
1426+
"""Test that pressing Ctrl+D on empty line exits the program"""
1427+
events = [
1428+
Event(evt="key", data="\x04", raw=bytearray(b"\x04")), # Ctrl+D
1429+
]
1430+
reader = self.prepare_reader(events)
1431+
with self.assertRaises(EOFError):
1432+
multiline_input(reader)
1433+
1434+
def test_ctrl_d_multiline_with_new_line(self):
1435+
"""Test that pressing Ctrl+D in multiline mode with trailing newline exits multiline mode"""
1436+
events = itertools.chain(
1437+
code_to_events("def f():\n pass\n"), # Enter multiline mode with trailing newline
1438+
[
1439+
Event(evt="key", data="\x04", raw=bytearray(b"\x04")), # Ctrl+D
1440+
],
1441+
)
1442+
reader, _ = handle_all_events(events)
1443+
self.assertTrue(reader.finished)
1444+
self.assertEqual("def f():\n pass\n", "".join(reader.buffer))
1445+
1446+
def test_ctrl_d_multiline_middle_of_line(self):
1447+
"""Test that pressing Ctrl+D in multiline mode with cursor in middle deletes character"""
1448+
events = itertools.chain(
1449+
code_to_events("def f():\n hello world"), # Enter multiline mode
1450+
[
1451+
Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))
1452+
] * 5, # move cursor to 'w' in "world"
1453+
[
1454+
Event(evt="key", data="\x04", raw=bytearray(b"\x04"))
1455+
], # Ctrl+D should delete 'w'
1456+
)
1457+
reader, _ = handle_all_events(events)
1458+
self.assertFalse(reader.finished)
1459+
self.assertEqual("def f():\n hello orld", "".join(reader.buffer))
1460+
1461+
def test_ctrl_d_multiline_end_of_line_no_newline(self):
1462+
"""Test that pressing Ctrl+D at end of line without newline performs no operation"""
1463+
events = itertools.chain(
1464+
code_to_events("def f():\n hello"), # Enter multiline mode, no trailing newline
1465+
[
1466+
Event(evt="key", data="\x04", raw=bytearray(b"\x04"))
1467+
], # Ctrl+D should be no-op
1468+
)
1469+
reader, _ = handle_all_events(events)
1470+
self.assertFalse(reader.finished)
1471+
self.assertEqual("def f():\n hello", "".join(reader.buffer))
1472+
1473+
def test_ctrl_d_single_line_middle_of_line(self):
1474+
"""Test that pressing Ctrl+D in single line mode deletes current character"""
1475+
events = itertools.chain(
1476+
code_to_events("hello"),
1477+
[Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))], # move left
1478+
[Event(evt="key", data="\x04", raw=bytearray(b"\x04"))], # Ctrl+D
1479+
)
1480+
reader, _ = handle_all_events(events)
1481+
self.assertEqual("hell", "".join(reader.buffer))
1482+
1483+
def test_ctrl_d_single_line_end_no_newline(self):
1484+
"""Test that pressing Ctrl+D at end of single line without newline does nothing"""
1485+
events = itertools.chain(
1486+
code_to_events("hello"), # cursor at end of line
1487+
[Event(evt="key", data="\x04", raw=bytearray(b"\x04"))], # Ctrl+D
1488+
)
1489+
reader, _ = handle_all_events(events)
1490+
self.assertEqual("hello", "".join(reader.buffer))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed Ctrl+D (^D) behavior in _pyrepl module to match old pre-3.13 REPL behavior.

0 commit comments

Comments
 (0)