From b510df3355ca3edbbbdcd4e797c13f0a6b468bf6 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 13 Oct 2026 11:41:01 -0700 Subject: [PATCH] gh-159225: PyREPL input should not write to history (GH-159263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Pablo Galindo Salgado --- Lib/_pyrepl/historical_reader.py | 7 ++- Lib/_pyrepl/readline.py | 3 +- Lib/test/test_pyrepl/test_pyrepl.py | 58 +++++++++++++++++++ ...-10-12-21-20-00.gh-issue-159225.T8jlor.rst | 1 + 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-12-21-20-00.gh-issue-159225.T8jlor.rst diff --git a/Lib/_pyrepl/historical_reader.py b/Lib/_pyrepl/historical_reader.py index 09b969d80bc231f..1edd86f2f1888a7 100644 --- a/Lib/_pyrepl/historical_reader.py +++ b/Lib/_pyrepl/historical_reader.py @@ -294,9 +294,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 e4370b0d1462eaf..f4ae70405a9acfb 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -385,7 +385,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 1e0f99f5f97c83a..d748c07d60d2a4b 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -690,6 +690,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.