Skip to content

Commit 09f6fb1

Browse files
deepwzhadqm
authored andcommitted
[3.14] gh-133400: Fixed Ctrl+D (^D) behavior in :mod:_pyrepl module (GH-133883)
(cherry picked from commit 81959a0) Co-authored-by: DeepWzh <[email protected]> Co-authored-by: adam j hartz <[email protected]>
1 parent 7c03e90 commit 09f6fb1

File tree

3 files changed

+107
-8
lines changed

3 files changed

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