]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-133400: Fixed Ctrl+D (^D) behavior in :mod:`_pyrepl` module (GH-133883)
authorDeepWzh <wzh2012@outlook.com>
Thu, 9 Oct 2025 14:16:48 +0000 (22:16 +0800)
committerGitHub <noreply@github.com>
Thu, 9 Oct 2025 14:16:48 +0000 (16:16 +0200)
Co-authored-by: adam j hartz <adam@smatz.net>
Lib/_pyrepl/commands.py
Lib/test/test_pyrepl/test_pyrepl.py
Misc/NEWS.d/next/Core_and_Builtins/2025-05-11-09-40-19.gh-issue-133400.zkWla8.rst [new file with mode: 0644]

index 50c824995d85b8bc411f6613fe18b3766de02be3..10127e58897a580232c3f432f1ce2aeba2a357b8 100644 (file)
@@ -420,14 +420,17 @@ class delete(EditCommand):
     def do(self) -> None:
         r = self.reader
         b = r.buffer
-        if (
-            r.pos == 0
-            and len(b) == 0  # this is something of a hack
-            and self.event[-1] == "\004"
-        ):
-            r.update_screen()
-            r.console.finish()
-            raise EOFError
+        if self.event[-1] == "\004":
+            if b and b[-1].endswith("\n"):
+                self.finish = True
+            elif (
+                r.pos == 0
+                and len(b) == 0  # this is something of a hack
+            ):
+                r.update_screen()
+                r.console.finish()
+                raise EOFError
+
         for i in range(r.get_arg()):
             if r.pos != len(b):
                 del b[r.pos]
index 47d384a209e9acadffc10caf30e7391a250038fc..ad7464dc3d37c6b46458b3a8e4ee959985a8f6d5 100644 (file)
@@ -1825,3 +1825,86 @@ class TestMain(ReplTestCase):
                     " outside of the Python REPL"
                 )
                 self.assertIn(hint, output)
+
+class TestPyReplCtrlD(TestCase):
+    """Test Ctrl+D behavior in _pyrepl to match old pre-3.13 REPL behavior.
+
+    Ctrl+D should:
+    - Exit on empty buffer (raises EOFError)
+    - Delete character when cursor is in middle of line
+    - Perform no operation when cursor is at end of line without newline
+    - Exit multiline mode when cursor is at end with trailing newline
+    - Run code up to that point when pressed on blank line with preceding lines
+    """
+    def prepare_reader(self, events):
+        console = FakeConsole(events)
+        config = ReadlineConfig(readline_completer=None)
+        reader = ReadlineAlikeReader(console=console, config=config)
+        return reader
+
+    def test_ctrl_d_empty_line(self):
+        """Test that pressing Ctrl+D on empty line exits the program"""
+        events = [
+            Event(evt="key", data="\x04", raw=bytearray(b"\x04")),  # Ctrl+D
+        ]
+        reader = self.prepare_reader(events)
+        with self.assertRaises(EOFError):
+            multiline_input(reader)
+
+    def test_ctrl_d_multiline_with_new_line(self):
+        """Test that pressing Ctrl+D in multiline mode with trailing newline exits multiline mode"""
+        events = itertools.chain(
+            code_to_events("def f():\n    pass\n"),  # Enter multiline mode with trailing newline
+            [
+                Event(evt="key", data="\x04", raw=bytearray(b"\x04")),  # Ctrl+D
+            ],
+        )
+        reader, _ = handle_all_events(events)
+        self.assertTrue(reader.finished)
+        self.assertEqual("def f():\n    pass\n", "".join(reader.buffer))
+
+    def test_ctrl_d_multiline_middle_of_line(self):
+        """Test that pressing Ctrl+D in multiline mode with cursor in middle deletes character"""
+        events = itertools.chain(
+            code_to_events("def f():\n    hello world"),  # Enter multiline mode
+            [
+                Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))
+            ] * 5,  # move cursor to 'w' in "world"
+            [
+                Event(evt="key", data="\x04", raw=bytearray(b"\x04"))
+            ], # Ctrl+D should delete 'w'
+        )
+        reader, _ = handle_all_events(events)
+        self.assertFalse(reader.finished)
+        self.assertEqual("def f():\n    hello orld", "".join(reader.buffer))
+
+    def test_ctrl_d_multiline_end_of_line_no_newline(self):
+        """Test that pressing Ctrl+D at end of line without newline performs no operation"""
+        events = itertools.chain(
+            code_to_events("def f():\n    hello"),  # Enter multiline mode, no trailing newline
+            [
+                Event(evt="key", data="\x04", raw=bytearray(b"\x04"))
+            ],  # Ctrl+D should be no-op
+        )
+        reader, _ = handle_all_events(events)
+        self.assertFalse(reader.finished)
+        self.assertEqual("def f():\n    hello", "".join(reader.buffer))
+
+    def test_ctrl_d_single_line_middle_of_line(self):
+        """Test that pressing Ctrl+D in single line mode deletes current character"""
+        events = itertools.chain(
+            code_to_events("hello"),
+            [Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))],  # move left
+            [Event(evt="key", data="\x04", raw=bytearray(b"\x04"))],    # Ctrl+D
+        )
+        reader, _ = handle_all_events(events)
+        self.assertEqual("hell", "".join(reader.buffer))
+
+    def test_ctrl_d_single_line_end_no_newline(self):
+        """Test that pressing Ctrl+D at end of single line without newline does nothing"""
+        events = itertools.chain(
+            code_to_events("hello"),  # cursor at end of line
+            [Event(evt="key", data="\x04", raw=bytearray(b"\x04"))],  # Ctrl+D
+        )
+        reader, _ = handle_all_events(events)
+        self.assertEqual("hello", "".join(reader.buffer))
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-11-09-40-19.gh-issue-133400.zkWla8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-11-09-40-19.gh-issue-133400.zkWla8.rst
new file mode 100644 (file)
index 0000000..2498d6e
--- /dev/null
@@ -0,0 +1 @@
+Fixed Ctrl+D (^D) behavior in _pyrepl module to match old pre-3.13 REPL behavior.