Skip to content

Commit 2cc1899

Browse files
miss-islingtonaorcajoambv
authored
[3.13] pythongh-119310: Fix encoding when reading old history file (pythonGH-121779) (python#123784)
(cherry picked from commit e959848) Co-authored-by: aorcajo <589252+aorcajo@users.noreply.github.com> Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent c46ad20 commit 2cc1899

File tree

4 files changed

+59
-6
lines changed

4 files changed

+59
-6
lines changed

Lib/_pyrepl/readline.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -427,12 +427,16 @@ def read_history_file(self, filename: str = gethistoryfile()) -> None:
427427
history = self.get_reader().history
428428

429429
with open(os.path.expanduser(filename), 'rb') as f:
430-
lines = [line.decode('utf-8', errors='replace') for line in f.read().split(b'\n')]
430+
is_editline = f.readline().startswith(b"_HiStOrY_V2_")
431+
if is_editline:
432+
encoding = "unicode-escape"
433+
else:
434+
f.seek(0)
435+
encoding = "utf-8"
436+
437+
lines = [line.decode(encoding, errors='replace') for line in f.read().split(b'\n')]
431438
buffer = []
432439
for line in lines:
433-
# Ignore readline history file header
434-
if line.startswith("_HiStOrY_V2_"):
435-
continue
436440
if line.endswith("\r"):
437441
buffer.append(line+'\n')
438442
else:

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,12 +1216,34 @@ def run_repl(
12161216
*,
12171217
cmdline_args: list[str] | None = None,
12181218
cwd: str | None = None,
1219+
) -> tuple[str, int]:
1220+
temp_dir = None
1221+
if cwd is None:
1222+
temp_dir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
1223+
cwd = temp_dir.name
1224+
try:
1225+
return self._run_repl(
1226+
repl_input, env=env, cmdline_args=cmdline_args, cwd=cwd
1227+
)
1228+
finally:
1229+
if temp_dir is not None:
1230+
temp_dir.cleanup()
1231+
1232+
def _run_repl(
1233+
self,
1234+
repl_input: str | list[str],
1235+
*,
1236+
env: dict | None,
1237+
cmdline_args: list[str] | None,
1238+
cwd: str,
12191239
) -> tuple[str, int]:
12201240
assert pty
12211241
master_fd, slave_fd = pty.openpty()
12221242
cmd = [sys.executable, "-i", "-u"]
12231243
if env is None:
12241244
cmd.append("-I")
1245+
elif "PYTHON_HISTORY" not in env:
1246+
env["PYTHON_HISTORY"] = os.path.join(cwd, ".regrtest_history")
12251247
if cmdline_args is not None:
12261248
cmd.extend(cmdline_args)
12271249
process = subprocess.Popen(
@@ -1260,3 +1282,26 @@ def run_repl(
12601282
process.kill()
12611283
exit_code = process.wait()
12621284
return "".join(output), exit_code
1285+
1286+
def test_readline_history_file(self):
1287+
# skip, if readline module is not available
1288+
readline = import_module('readline')
1289+
if readline.backend != "editline":
1290+
self.skipTest("GNU readline is not affected by this issue")
1291+
1292+
hfile = tempfile.NamedTemporaryFile()
1293+
self.addCleanup(unlink, hfile.name)
1294+
env = os.environ.copy()
1295+
env["PYTHON_HISTORY"] = hfile.name
1296+
1297+
env["PYTHON_BASIC_REPL"] = "1"
1298+
output, exit_code = self.run_repl("spam \nexit()\n", env=env)
1299+
self.assertEqual(exit_code, 0)
1300+
self.assertIn("spam ", output)
1301+
self.assertNotEqual(pathlib.Path(hfile.name).stat().st_size, 0)
1302+
self.assertIn("spam\\040", pathlib.Path(hfile.name).read_text())
1303+
1304+
env.pop("PYTHON_BASIC_REPL", None)
1305+
output, exit_code = self.run_repl("exit\n", env=env)
1306+
self.assertEqual(exit_code, 0)
1307+
self.assertNotIn("\\040", pathlib.Path(hfile.name).read_text())

Lib/test/test_repl.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kw):
4141
# path may be used by Py_GetPath() to build the default module search
4242
# path.
4343
stdin_fname = os.path.join(os.path.dirname(sys.executable), "<stdin>")
44-
cmd_line = [stdin_fname, '-E', '-i']
44+
cmd_line = [stdin_fname, '-I', '-i']
4545
cmd_line.extend(args)
4646

4747
# Set TERM=vt100, for the rationale see the comments in spawn_python() of
@@ -228,6 +228,7 @@ def test_asyncio_repl_reaches_python_startup_script(self):
228228
f.write("exit(0)" + os.linesep)
229229

230230
env = os.environ.copy()
231+
env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".asyncio_history")
231232
env["PYTHONSTARTUP"] = script
232233
subprocess.check_call(
233234
[sys.executable, "-m", "asyncio"],
@@ -240,7 +241,7 @@ def test_asyncio_repl_reaches_python_startup_script(self):
240241
@unittest.skipUnless(pty, "requires pty")
241242
def test_asyncio_repl_is_ok(self):
242243
m, s = pty.openpty()
243-
cmd = [sys.executable, "-m", "asyncio"]
244+
cmd = [sys.executable, "-I", "-m", "asyncio"]
244245
proc = subprocess.Popen(
245246
cmd,
246247
stdin=s,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Allow the new interactive shell to read history files written with the
2+
editline library that use unicode-escaped entries. Patch by aorcajo and
3+
Łukasz Langa.

0 commit comments

Comments
 (0)