Skip to content

Commit 81959a0

Browse files
deepwzhadqm
andauthored
gh-133400: Fixed Ctrl+D (^D) behavior in :mod:_pyrepl module (GH-133883)
Co-authored-by: adam j hartz <[email protected]>
1 parent d2deb8f commit 81959a0

File tree

3 files changed

+95
-8
lines changed

3 files changed

+95
-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: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,3 +1825,86 @@ def test_detect_pip_usage_in_repl(self):
18251825
" outside of the Python REPL"
18261826
)
18271827
self.assertIn(hint, output)
1828+
1829+
class TestPyReplCtrlD(TestCase):
1830+
"""Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.
1831+
1832+
Ctrl+D should:
1833+
- Exit on empty buffer (raises EOFError)
1834+
- Delete character when cursor is in middle of line
1835+
- Perform no operation when cursor is at end of line without newline
1836+
- Exit multiline mode when cursor is at end with trailing newline
1837+
- Run code up to that point when pressed on blank line with preceding lines
1838+
"""
1839+
def prepare_reader(self, events):
1840+
console = FakeConsole(events)
1841+
config = ReadlineConfig(readline_completer=None)
1842+
reader = ReadlineAlikeReader(console=console, config=config)
1843+
return reader
1844+
1845+
def test_ctrl_d_empty_line(self):
1846+
"""Test that pressing Ctrl+D on empty line exits the program"""
1847+
events = [
1848+
Event(evt="key", data="\x04", raw=bytearray(b"\x04")), # Ctrl+D
1849+
]
1850+
reader = self.prepare_reader(events)
1851+
with self.assertRaises(EOFError):
1852+
multiline_input(reader)
1853+
1854+
def test_ctrl_d_multiline_with_new_line(self):
1855+
"""Test that pressing Ctrl+D in multiline mode with trailing newline exits multiline mode"""
1856+
events = itertools.chain(
1857+
code_to_events("def f():\n pass\n"), # Enter multiline mode with trailing newline
1858+
[
1859+
Event(evt="key", data="\x04", raw=bytearray(b"\x04")), # Ctrl+D
1860+
],
1861+
)
1862+
reader, _ = handle_all_events(events)
1863+
self.assertTrue(reader.finished)
1864+
self.assertEqual("def f():\n pass\n", "".join(reader.buffer))
1865+
1866+
def test_ctrl_d_multiline_middle_of_line(self):
1867+
"""Test that pressing Ctrl+D in multiline mode with cursor in middle deletes character"""
1868+
events = itertools.chain(
1869+
code_to_events("def f():\n hello world"), # Enter multiline mode
1870+
[
1871+
Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))
1872+
] * 5, # move cursor to 'w' in "world"
1873+
[
1874+
Event(evt="key", data="\x04", raw=bytearray(b"\x04"))
1875+
], # Ctrl+D should delete 'w'
1876+
)
1877+
reader, _ = handle_all_events(events)
1878+
self.assertFalse(reader.finished)
1879+
self.assertEqual("def f():\n hello orld", "".join(reader.buffer))
1880+
1881+
def test_ctrl_d_multiline_end_of_line_no_newline(self):
1882+
"""Test that pressing Ctrl+D at end of line without newline performs no operation"""
1883+
events = itertools.chain(
1884+
code_to_events("def f():\n hello"), # Enter multiline mode, no trailing newline
1885+
[
1886+
Event(evt="key", data="\x04", raw=bytearray(b"\x04"))
1887+
], # Ctrl+D should be no-op
1888+
)
1889+
reader, _ = handle_all_events(events)
1890+
self.assertFalse(reader.finished)
1891+
self.assertEqual("def f():\n hello", "".join(reader.buffer))
1892+
1893+
def test_ctrl_d_single_line_middle_of_line(self):
1894+
"""Test that pressing Ctrl+D in single line mode deletes current character"""
1895+
events = itertools.chain(
1896+
code_to_events("hello"),
1897+
[Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))], # move left
1898+
[Event(evt="key", data="\x04", raw=bytearray(b"\x04"))], # Ctrl+D
1899+
)
1900+
reader, _ = handle_all_events(events)
1901+
self.assertEqual("hell", "".join(reader.buffer))
1902+
1903+
def test_ctrl_d_single_line_end_no_newline(self):
1904+
"""Test that pressing Ctrl+D at end of single line without newline does nothing"""
1905+
events = itertools.chain(
1906+
code_to_events("hello"), # cursor at end of line
1907+
[Event(evt="key", data="\x04", raw=bytearray(b"\x04"))], # Ctrl+D
1908+
)
1909+
reader, _ = handle_all_events(events)
1910+
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)