Skip to content

Commit 2762525

Browse files
authored
gh-127495: Append to history file after every statement in PyREPL (GH-132294)
1 parent 614d792 commit 2762525

File tree

4 files changed

+47
-1
lines changed

4 files changed

+47
-1
lines changed

Lib/_pyrepl/readline.py

+16
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
# "set_pre_input_hook",
9191
"set_startup_hook",
9292
"write_history_file",
93+
"append_history_file",
9394
# ---- multiline extensions ----
9495
"multiline_input",
9596
]
@@ -453,6 +454,7 @@ def read_history_file(self, filename: str = gethistoryfile()) -> None:
453454
del buffer[:]
454455
if line:
455456
history.append(line)
457+
self.set_history_length(self.get_current_history_length())
456458

457459
def write_history_file(self, filename: str = gethistoryfile()) -> None:
458460
maxlength = self.saved_history_length
@@ -464,6 +466,19 @@ def write_history_file(self, filename: str = gethistoryfile()) -> None:
464466
entry = entry.replace("\n", "\r\n") # multiline history support
465467
f.write(entry + "\n")
466468

469+
def append_history_file(self, filename: str = gethistoryfile()) -> None:
470+
reader = self.get_reader()
471+
saved_length = self.get_history_length()
472+
length = self.get_current_history_length() - saved_length
473+
history = reader.get_trimmed_history(length)
474+
f = open(os.path.expanduser(filename), "a",
475+
encoding="utf-8", newline="\n")
476+
with f:
477+
for entry in history:
478+
entry = entry.replace("\n", "\r\n") # multiline history support
479+
f.write(entry + "\n")
480+
self.set_history_length(saved_length + length)
481+
467482
def clear_history(self) -> None:
468483
del self.get_reader().history[:]
469484

@@ -533,6 +548,7 @@ def insert_text(self, text: str) -> None:
533548
get_current_history_length = _wrapper.get_current_history_length
534549
read_history_file = _wrapper.read_history_file
535550
write_history_file = _wrapper.write_history_file
551+
append_history_file = _wrapper.append_history_file
536552
clear_history = _wrapper.clear_history
537553
get_history_item = _wrapper.get_history_item
538554
remove_history_item = _wrapper.remove_history_item

Lib/_pyrepl/simple_interact.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030
import os
3131
import sys
3232
import code
33+
import warnings
3334

34-
from .readline import _get_reader, multiline_input
35+
from .readline import _get_reader, multiline_input, append_history_file
3536

3637

3738
_error: tuple[type[Exception], ...] | type[Exception]
@@ -144,6 +145,10 @@ def maybe_run_command(statement: str) -> bool:
144145
input_name = f"<python-input-{input_n}>"
145146
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg]
146147
assert not more
148+
try:
149+
append_history_file()
150+
except (FileNotFoundError, PermissionError, OSError) as e:
151+
warnings.warn(f"failed to open the history file for writing: {e}")
147152
input_n += 1
148153
except KeyboardInterrupt:
149154
r = _get_reader()

Lib/test/test_pyrepl/test_pyrepl.py

+22
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def _run_repl(
112112
else:
113113
os.close(master_fd)
114114
process.kill()
115+
process.wait(timeout=SHORT_TIMEOUT)
115116
self.fail(f"Timeout while waiting for output, got: {''.join(output)}")
116117

117118
os.close(master_fd)
@@ -1564,6 +1565,27 @@ def test_readline_history_file(self):
15641565
self.assertEqual(exit_code, 0)
15651566
self.assertNotIn("\\040", pathlib.Path(hfile.name).read_text())
15661567

1568+
def test_history_survive_crash(self):
1569+
env = os.environ.copy()
1570+
commands = "1\nexit()\n"
1571+
output, exit_code = self.run_repl(commands, env=env)
1572+
if "can't use pyrepl" in output:
1573+
self.skipTest("pyrepl not available")
1574+
1575+
with tempfile.NamedTemporaryFile() as hfile:
1576+
env["PYTHON_HISTORY"] = hfile.name
1577+
commands = "spam\nimport time\ntime.sleep(1000)\npreved\n"
1578+
try:
1579+
self.run_repl(commands, env=env)
1580+
except AssertionError:
1581+
pass
1582+
1583+
history = pathlib.Path(hfile.name).read_text()
1584+
self.assertIn("spam", history)
1585+
self.assertIn("time", history)
1586+
self.assertNotIn("sleep", history)
1587+
self.assertNotIn("preved", history)
1588+
15671589
def test_keyboard_interrupt_after_isearch(self):
15681590
output, exit_code = self.run_repl(["\x12", "\x03", "exit"])
15691591
self.assertEqual(exit_code, 0)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In PyREPL, append a new entry to the ``PYTHON_HISTORY`` file *after* every
2+
statement. This should preserve command-line history after interpreter is
3+
terminated. Patch by Sergey B Kirpichev.

0 commit comments

Comments
 (0)