From ed0e12df3ab5a2aea4070de7eb6689738001a5e6 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 12 Oct 2026 14:07:35 -0700 Subject: [PATCH 1/5] 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 --- Lib/_pyrepl/readline.py | 4 +++- Lib/test/test_pyrepl/test_pyrepl.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 1d00de3683db231..a59c4b7e1c3c0b0 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -380,7 +380,9 @@ 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) + # Suspend history temporarily + with reader.suspend_history(): + 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..cfd99b0593d40dc 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -690,6 +690,14 @@ 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_modify_history(self): + reader = self.prepare_reader(code_to_events("new input\n")) + 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_multiline_edit(self): events = itertools.chain( code_to_events("def f():\n...\n\n"), From 089471e28b34f513ce5bfaeaf43f2bc6dd5c2fa8 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:20:02 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-10-12-21-20-00.gh-issue-159225.T8jlor.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-12-21-20-00.gh-issue-159225.T8jlor.rst 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. From 2eea91f9c8183b05b749b12dd67e5f2dcd2fc4c3 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 12 Oct 2026 14:20:39 -0700 Subject: [PATCH 3/5] Remove comment --- Lib/_pyrepl/readline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index a59c4b7e1c3c0b0..2dcf1707ce337d2 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -380,7 +380,6 @@ def input(self, prompt: object = "") -> str: prompt_str = str(prompt) reader.ps1 = prompt_str sys.audit("builtins.input", prompt_str) - # Suspend history temporarily with reader.suspend_history(): result = reader.readline(startup_hook=self.startup_hook) sys.audit("builtins.input/result", result) From e2b79ed5cbbfb2cbdfb9297230a22d6600dd58aa Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 13 Oct 2026 10:26:09 -0700 Subject: [PATCH 4/5] Only suspend writing --- Lib/_pyrepl/historical_reader.py | 7 ++++++- Lib/_pyrepl/readline.py | 2 +- Lib/test/test_pyrepl/test_pyrepl.py | 8 ++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/historical_reader.py b/Lib/_pyrepl/historical_reader.py index e0d315915987651..b5cdc82d826ddb4 100644 --- a/Lib/_pyrepl/historical_reader.py +++ b/Lib/_pyrepl/historical_reader.py @@ -291,9 +291,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 2dcf1707ce337d2..a8af6cb425d0c99 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -380,7 +380,7 @@ def input(self, prompt: object = "") -> str: prompt_str = str(prompt) reader.ps1 = prompt_str sys.audit("builtins.input", prompt_str) - with reader.suspend_history(): + 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 cfd99b0593d40dc..82b70345ac6c384 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -691,11 +691,15 @@ def test_get_line_buffer_returns_str(self): self.assertIs(type(wrapper.get_line_buffer()), str) def test_input_does_not_modify_history(self): - reader = self.prepare_reader(code_to_events("new input\n")) + 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(), "new input") + self.assertEqual(wrapper.input(), "previous input") self.assertEqual(reader.history, ["previous input"]) def test_multiline_edit(self): From f445762aa9dc858536148cbead8852414531f686 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Tue, 13 Oct 2026 19:10:51 +0100 Subject: [PATCH 5/5] gh-159225: Cover input history isolation and interrupted reads --- Lib/test/test_pyrepl/test_pyrepl.py | 48 ++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 82b70345ac6c384..d748c07d60d2a4b 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -690,7 +690,23 @@ 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_modify_history(self): + 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")), @@ -702,6 +718,36 @@ def test_input_does_not_modify_history(self): 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"),