gh-127495: Append to history file after every statement in PyREPL (GH-132294)

This commit is contained in:
Sergey B Kirpichev 2025-04-27 16:32:37 +03:00 committed by GitHub
parent 614d79231d
commit 276252565c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 47 additions and 1 deletions

View file

@ -90,6 +90,7 @@ __all__ = [
# "set_pre_input_hook", # "set_pre_input_hook",
"set_startup_hook", "set_startup_hook",
"write_history_file", "write_history_file",
"append_history_file",
# ---- multiline extensions ---- # ---- multiline extensions ----
"multiline_input", "multiline_input",
] ]
@ -453,6 +454,7 @@ class _ReadlineWrapper:
del buffer[:] del buffer[:]
if line: if line:
history.append(line) history.append(line)
self.set_history_length(self.get_current_history_length())
def write_history_file(self, filename: str = gethistoryfile()) -> None: def write_history_file(self, filename: str = gethistoryfile()) -> None:
maxlength = self.saved_history_length maxlength = self.saved_history_length
@ -464,6 +466,19 @@ class _ReadlineWrapper:
entry = entry.replace("\n", "\r\n") # multiline history support entry = entry.replace("\n", "\r\n") # multiline history support
f.write(entry + "\n") f.write(entry + "\n")
def append_history_file(self, filename: str = gethistoryfile()) -> None:
reader = self.get_reader()
saved_length = self.get_history_length()
length = self.get_current_history_length() - saved_length
history = reader.get_trimmed_history(length)
f = open(os.path.expanduser(filename), "a",
encoding="utf-8", newline="\n")
with f:
for entry in history:
entry = entry.replace("\n", "\r\n") # multiline history support
f.write(entry + "\n")
self.set_history_length(saved_length + length)
def clear_history(self) -> None: def clear_history(self) -> None:
del self.get_reader().history[:] del self.get_reader().history[:]
@ -533,6 +548,7 @@ set_history_length = _wrapper.set_history_length
get_current_history_length = _wrapper.get_current_history_length get_current_history_length = _wrapper.get_current_history_length
read_history_file = _wrapper.read_history_file read_history_file = _wrapper.read_history_file
write_history_file = _wrapper.write_history_file write_history_file = _wrapper.write_history_file
append_history_file = _wrapper.append_history_file
clear_history = _wrapper.clear_history clear_history = _wrapper.clear_history
get_history_item = _wrapper.get_history_item get_history_item = _wrapper.get_history_item
remove_history_item = _wrapper.remove_history_item remove_history_item = _wrapper.remove_history_item

View file

@ -30,8 +30,9 @@ import functools
import os import os
import sys import sys
import code import code
import warnings
from .readline import _get_reader, multiline_input from .readline import _get_reader, multiline_input, append_history_file
_error: tuple[type[Exception], ...] | type[Exception] _error: tuple[type[Exception], ...] | type[Exception]
@ -144,6 +145,10 @@ def run_multiline_interactive_console(
input_name = f"<python-input-{input_n}>" input_name = f"<python-input-{input_n}>"
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg] more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg]
assert not more assert not more
try:
append_history_file()
except (FileNotFoundError, PermissionError, OSError) as e:
warnings.warn(f"failed to open the history file for writing: {e}")
input_n += 1 input_n += 1
except KeyboardInterrupt: except KeyboardInterrupt:
r = _get_reader() r = _get_reader()

View file

@ -112,6 +112,7 @@ class ReplTestCase(TestCase):
else: else:
os.close(master_fd) os.close(master_fd)
process.kill() process.kill()
process.wait(timeout=SHORT_TIMEOUT)
self.fail(f"Timeout while waiting for output, got: {''.join(output)}") self.fail(f"Timeout while waiting for output, got: {''.join(output)}")
os.close(master_fd) os.close(master_fd)
@ -1564,6 +1565,27 @@ class TestMain(ReplTestCase):
self.assertEqual(exit_code, 0) self.assertEqual(exit_code, 0)
self.assertNotIn("\\040", pathlib.Path(hfile.name).read_text()) self.assertNotIn("\\040", pathlib.Path(hfile.name).read_text())
def test_history_survive_crash(self):
env = os.environ.copy()
commands = "1\nexit()\n"
output, exit_code = self.run_repl(commands, env=env)
if "can't use pyrepl" in output:
self.skipTest("pyrepl not available")
with tempfile.NamedTemporaryFile() as hfile:
env["PYTHON_HISTORY"] = hfile.name
commands = "spam\nimport time\ntime.sleep(1000)\npreved\n"
try:
self.run_repl(commands, env=env)
except AssertionError:
pass
history = pathlib.Path(hfile.name).read_text()
self.assertIn("spam", history)
self.assertIn("time", history)
self.assertNotIn("sleep", history)
self.assertNotIn("preved", history)
def test_keyboard_interrupt_after_isearch(self): def test_keyboard_interrupt_after_isearch(self):
output, exit_code = self.run_repl(["\x12", "\x03", "exit"]) output, exit_code = self.run_repl(["\x12", "\x03", "exit"])
self.assertEqual(exit_code, 0) self.assertEqual(exit_code, 0)

View file

@ -0,0 +1,3 @@
In PyREPL, append a new entry to the ``PYTHON_HISTORY`` file *after* every
statement. This should preserve command-line history after interpreter is
terminated. Patch by Sergey B Kirpichev.