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 1d00de3683db231..a8af6cb425d0c99 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -380,7 +380,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.