https://github.com/python/cpython/commit/ae833ab14c132853a81b654bfc91a649b21f1532
commit: ae833ab14c132853a81b654bfc91a649b21f1532
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-10-13T21:29:35+01:00
summary:

[3.14] gh-159225: PyREPL input should not write to history (GH-159263) (#159370)

gh-159225: PyREPL input should not write to history (GH-159263)

* gh-159225: PyREPL input should not write to history

Values entered in an input statement are not Python statements and thus the 
reader should suspend history recording when in input mode

* 📜🤖 Added by blurb_it.

* Remove comment

* Only suspend writing

* gh-159225: Cover input history isolation and interrupted reads

---------
(cherry picked from commit 6f92424d8ef0eb16ffa502098b26b91f4607be61)

Co-authored-by: Eduardo Villalpando Mello <[email protected]>
Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Pablo Galindo Salgado <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-10-12-21-20-00.gh-issue-159225.T8jlor.rst
M Lib/_pyrepl/historical_reader.py
M Lib/_pyrepl/readline.py
M Lib/test/test_pyrepl/test_pyrepl.py

diff --git a/Lib/_pyrepl/historical_reader.py b/Lib/_pyrepl/historical_reader.py
index c4b95fa2e81ee60..27737ee8da875b0 100644
--- a/Lib/_pyrepl/historical_reader.py
+++ b/Lib/_pyrepl/historical_reader.py
@@ -295,9 +295,14 @@ def suspend(self) -> SimpleContextManager:
 
     @contextmanager
     def suspend_history(self) -> SimpleContextManager:
+        with self.suspend_history_writes():
+            del self.history[:]
+            yield
+
+    @contextmanager
+    def suspend_history_writes(self) -> SimpleContextManager:
         try:
             old_history = self.history[:]
-            del self.history[:]
             yield
         finally:
             self.history[:] = old_history
diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py
index 23b8fa6b9c7625e..495592f75a2e178 100644
--- a/Lib/_pyrepl/readline.py
+++ b/Lib/_pyrepl/readline.py
@@ -374,7 +374,8 @@ def input(self, prompt: object = "") -> str:
         prompt_str = str(prompt)
         reader.ps1 = prompt_str
         sys.audit("builtins.input", prompt_str)
-        result = reader.readline(startup_hook=self.startup_hook)
+        with reader.suspend_history_writes():
+            result = reader.readline(startup_hook=self.startup_hook)
         sys.audit("builtins.input/result", result)
         return result
 
diff --git a/Lib/test/test_pyrepl/test_pyrepl.py 
b/Lib/test/test_pyrepl/test_pyrepl.py
index 9ea74971e2e89d7..df2e0f4af1ac769 100644
--- a/Lib/test/test_pyrepl/test_pyrepl.py
+++ b/Lib/test/test_pyrepl/test_pyrepl.py
@@ -673,6 +673,64 @@ def test_get_line_buffer_returns_str(self):
         wrapper = _ReadlineWrapper(f_in=None, f_out=None, reader=reader)
         self.assertIs(type(wrapper.get_line_buffer()), str)
 
+    def test_input_does_not_add_history(self):
+        for history in ([], ["previous input"]):
+            with self.subTest(history=history):
+                reader = self.prepare_reader(
+                    code_to_events("first input\nsecond input\n1+1\n")
+                )
+                reader.history[:] = history
+                wrapper = _ReadlineWrapper(f_in=None, f_out=None, 
reader=reader)
+
+                self.assertEqual(wrapper.input(), "first input")
+                self.assertEqual(reader.history, history)
+                self.assertEqual(wrapper.input(), "second input")
+                self.assertEqual(reader.history, history)
+                self.assertEqual(multiline_input(reader), "1+1")
+                self.assertEqual(reader.history, [*history, "1+1"])
+
+    def test_input_can_read_history(self):
+        events = [
+            Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
+            Event(evt="key", data="\n", raw=bytearray(b"\n")),
+        ]
+        reader = self.prepare_reader(events)
+        reader.history.append("previous input")
+        wrapper = _ReadlineWrapper(f_in=None, f_out=None, reader=reader)
+
+        self.assertEqual(wrapper.input(), "previous input")
+        self.assertEqual(reader.history, ["previous input"])
+
+    def test_input_does_not_modify_history(self):
+        events = itertools.chain(
+            [Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))],
+            code_to_events(" edited"),
+            [Event(evt="key", data="down", raw=bytearray(b"\x1bOB"))],
+            code_to_events("new input\n"),
+        )
+        reader = self.prepare_reader(events)
+        reader.history.append("previous input")
+        wrapper = _ReadlineWrapper(f_in=None, f_out=None, reader=reader)
+
+        self.assertEqual(wrapper.input(), "new input")
+        self.assertEqual(reader.history, ["previous input"])
+
+    def test_input_error_does_not_modify_history(self):
+        for text, exception in (
+            ("partial input\x03", KeyboardInterrupt),
+            ("\x04", EOFError),
+        ):
+            with self.subTest(exception=exception):
+                reader = self.prepare_reader(code_to_events(text + "1+1\n"))
+                reader.history.append("previous input")
+                wrapper = _ReadlineWrapper(f_in=None, f_out=None, 
reader=reader)
+
+                with self.assertRaises(exception):
+                    wrapper.input()
+                self.assertEqual(reader.history, ["previous input"])
+                self.assertEqual(multiline_input(reader), "1+1")
+                self.assertEqual(reader.history, ["previous input", "1+1"])
+
     def test_multiline_edit(self):
         events = itertools.chain(
             code_to_events("def f():\n...\n\n"),
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-12-21-20-00.gh-issue-159225.T8jlor.rst 
b/Misc/NEWS.d/next/Library/2026-10-12-21-20-00.gh-issue-159225.T8jlor.rst
new file mode 100644
index 000000000000000..a9ee0e6a7eb97d2
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-12-21-20-00.gh-issue-159225.T8jlor.rst
@@ -0,0 +1 @@
+Prevent ``input`` calls in PyREPL from modifying readline history.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to