Skip to content

Commit 9ce4887

Browse files
committed
gh-122622: Add sys._preexec hook for REPL
1 parent e682141 commit 9ce4887

File tree

2 files changed

+59
-1
lines changed

2 files changed

+59
-1
lines changed

Lib/_pyrepl/simple_interact.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ def maybe_run_command(statement: str) -> bool:
146146
if maybe_run_command(statement):
147147
continue
148148

149+
preexec = getattr(sys, "_preexec", None)
150+
if callable(preexec):
151+
try:
152+
preexec(statement)
153+
except Exception:
154+
pass
155+
149156
input_name = f"<python-input-{input_n}>"
150157
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg]
151158
assert not more

Lib/test/test_pyrepl/test_interact.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import contextlib
22
import io
3+
import sys
34
import warnings
45
import unittest
5-
from unittest.mock import patch
6+
from unittest.mock import patch, MagicMock
67
from textwrap import dedent
78

89
from test.support import force_not_colorized
@@ -299,3 +300,53 @@ def f():
299300
count = sum("'return' in a 'finally' block" in str(w.message)
300301
for w in caught)
301302
self.assertEqual(count, 1)
303+
304+
305+
class TestPreexecHook(unittest.TestCase):
306+
307+
def _run_interactive(self, statements, *, preexec=None):
308+
from _pyrepl.simple_interact import run_multiline_interactive_console
309+
310+
console = InteractiveColoredConsole()
311+
statement_iter = iter(statements)
312+
313+
def fake_multiline_input(more_lines, ps1, ps2):
314+
try:
315+
return next(statement_iter)
316+
except StopIteration:
317+
raise EOFError
318+
319+
patches = [
320+
patch("_pyrepl.simple_interact.multiline_input", side_effect=fake_multiline_input),
321+
patch("_pyrepl.simple_interact._get_reader"),
322+
patch("_pyrepl.simple_interact.append_history_file"),
323+
patch("_pyrepl.readline._setup"),
324+
]
325+
if preexec is not None:
326+
patches.append(patch.object(sys, "_preexec", preexec, create=True))
327+
328+
f = io.StringIO()
329+
with contextlib.ExitStack() as stack:
330+
for p in patches:
331+
stack.enter_context(p)
332+
stack.enter_context(contextlib.redirect_stdout(f))
333+
stack.enter_context(contextlib.redirect_stderr(f))
334+
run_multiline_interactive_console(console)
335+
336+
return f.getvalue()
337+
338+
def test_preexec_called_with_statement(self):
339+
preexec = MagicMock()
340+
self._run_interactive(["x = 1"], preexec=preexec)
341+
preexec.assert_called_once_with("x = 1")
342+
343+
def test_preexec_exception_does_not_break_repl(self):
344+
def bad_preexec(cmd):
345+
raise RuntimeError("hook error")
346+
347+
self._run_interactive(["x = 1", "y = 2"], preexec=bad_preexec)
348+
349+
def test_preexec_not_called_for_repl_commands(self):
350+
preexec = MagicMock()
351+
self._run_interactive(["clear"], preexec=preexec)
352+
preexec.assert_not_called()

0 commit comments

Comments
 (0)