Skip to content

Commit de87549

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

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
@@ -420,14 +420,17 @@ class delete(EditCommand):
420420
def do(self) -> None:
421421
r = self.reader
422422
b = r.buffer
423-
if (
424-
r.pos == 0
425-
and len(b) == 0 # this is something of a hack
426-
and self.event[-1] == "\004"
427-
):
428-
r.update_screen()
429-
r.console.finish()
430-
raise EOFError
423+
if self.event[-1] == "\004":
424+
if b and b[-1].endswith("\n"):
425+
self.finish = True
426+
elif (
427+
r.pos == 0
428+
and len(b) == 0 # this is something of a hack
429+
):
430+
r.update_screen()
431+
r.console.finish()
432+
raise EOFError
433+
431434
for i in range(r.get_arg()):
432435
if r.pos != len(b):
433436
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
@@ -1814,3 +1814,87 @@ def test_showrefcount(self):
18141814
output, _ = self.run_repl("1\n1+2\nexit()\n", cmdline_args=['-Xshowrefcount'], env=env)
18151815
matches = re.findall(r'\[-?\d+ refs, \d+ blocks\]', output)
18161816
self.assertEqual(len(matches), 3)
1817+
1818+
1819+
class TestPyReplCtrlD(TestCase):
1820+
"""Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.
1821+
1822+
Ctrl+D should:
1823+
- Exit on empty buffer (raises EOFError)
1824+
- Delete character when cursor is in middle of line
1825+
- Perform no operation when cursor is at end of line without newline
1826+
- Exit multiline mode when cursor is at end with trailing newline
1827+
- Run code up to that point when pressed on blank line with preceding lines
1828+
"""
1829+
def prepare_reader(self, events):
1830+
console = FakeConsole(events)
1831+
config = ReadlineConfig(readline_completer=None)
1832+
reader = ReadlineAlikeReader(console=console, config=config)
1833+
return reader
1834+
1835+
def test_ctrl_d_empty_line(self):
1836+
"""Test that pressing Ctrl+D on empty line exits the program"""
1837+
events = [
1838+
Event(evt="key", data="\x04", raw=bytearray(b"\x04")), # Ctrl+D
1839+
]
1840+
reader = self.prepare_reader(events)
1841+
with self.assertRaises(EOFError):
1842+
multiline_input(reader)
1843+
1844+
def test_ctrl_d_multiline_with_new_line(self):
1845+
"""Test that pressing Ctrl+D in multiline mode with trailing newline exits multiline mode"""
1846+
events = itertools.chain(
1847+
code_to_events("def f():\n pass\n"), # Enter multiline mode with trailing newline
1848+
[
1849+
Event(evt="key", data="\x04", raw=bytearray(b"\x04")), # Ctrl+D
1850+
],
1851+
)
1852+
reader, _ = handle_all_events(events)
1853+
self.assertTrue(reader.finished)
1854+
self.assertEqual("def f():\n pass\n", "".join(reader.buffer))
1855+
1856+
def test_ctrl_d_multiline_middle_of_line(self):
1857+
"""Test that pressing Ctrl+D in multiline mode with cursor in middle deletes character"""
1858+
events = itertools.chain(
1859+
code_to_events("def f():\n hello world"), # Enter multiline mode
1860+
[
1861+
Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))
1862+
] * 5, # move cursor to 'w' in "world"
1863+
[
1864+
Event(evt="key", data="\x04", raw=bytearray(b"\x04"))
1865+
], # Ctrl+D should delete 'w'
1866+
)
1867+
reader, _ = handle_all_events(events)
1868+
self.assertFalse(reader.finished)
1869+
self.assertEqual("def f():\n hello orld", "".join(reader.buffer))
1870+
1871+
def test_ctrl_d_multiline_end_of_line_no_newline(self):
1872+
"""Test that pressing Ctrl+D at end of line without newline performs no operation"""
1873+
events = itertools.chain(
1874+
code_to_events("def f():\n hello"), # Enter multiline mode, no trailing newline
1875+
[
1876+
Event(evt="key", data="\x04", raw=bytearray(b"\x04"))
1877+
], # Ctrl+D should be no-op
1878+
)
1879+
reader, _ = handle_all_events(events)
1880+
self.assertFalse(reader.finished)
1881+
self.assertEqual("def f():\n hello", "".join(reader.buffer))
1882+
1883+
def test_ctrl_d_single_line_middle_of_line(self):
1884+
"""Test that pressing Ctrl+D in single line mode deletes current character"""
1885+
events = itertools.chain(
1886+
code_to_events("hello"),
1887+
[Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))], # move left
1888+
[Event(evt="key", data="\x04", raw=bytearray(b"\x04"))], # Ctrl+D
1889+
)
1890+
reader, _ = handle_all_events(events)
1891+
self.assertEqual("hell", "".join(reader.buffer))
1892+
1893+
def test_ctrl_d_single_line_end_no_newline(self):
1894+
"""Test that pressing Ctrl+D at end of single line without newline does nothing"""
1895+
events = itertools.chain(
1896+
code_to_events("hello"), # cursor at end of line
1897+
[Event(evt="key", data="\x04", raw=bytearray(b"\x04"))], # Ctrl+D
1898+
)
1899+
reader, _ = handle_all_events(events)
1900+
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)