Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Lib/_pyrepl/simple_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@
from .readline import _get_reader, multiline_input, append_history_file


# Called with the complete statement, which may span multiple lines, after the
# user submits it but before it is executed. The hook is not called for PyREPL
# commands such as ``clear``. Exceptions raised by the hook are displayed but
# do not prevent the statement from running. Its return value is ignored.
# External tools such as IDEs can install a hook to augment the behavior of the
# REPL.
#
# For example, VS Code can mark the start of command execution:
#
# from _pyrepl import simple_interact
#
# def vscode_statement_submitted(statement: str) -> None:
# print("\x1b]633;C\x07", end="")
#
# simple_interact.statement_submitted_hook = vscode_statement_submitted
statement_submitted_hook = None


_error: tuple[type[Exception], ...] | type[Exception]
try:
from .unix_console import _error
Expand Down Expand Up @@ -145,6 +163,12 @@ def maybe_run_command(statement: str) -> bool:
if maybe_run_command(statement):
continue

if statement_submitted_hook is not None:
try:
statement_submitted_hook(statement)
except Exception:
console.showtraceback()

input_name = f"<python-input-{input_n}>"
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg]
assert not more
Expand Down
60 changes: 59 additions & 1 deletion Lib/test/test_pyrepl/test_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import io
import warnings
import unittest
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from textwrap import dedent

from test.support import force_not_colorized

from _pyrepl import simple_interact
from _pyrepl.console import InteractiveColoredConsole
from _pyrepl.simple_interact import _more_lines

Expand Down Expand Up @@ -299,3 +300,60 @@ def f():
count = sum("'return' in a 'finally' block" in str(w.message)
for w in caught)
self.assertEqual(count, 1)


class TestStatementSubmittedHook(unittest.TestCase):

def _run_interactive(self, statements, hook):
console = InteractiveColoredConsole()
statement_iter = iter(statements)

def fake_multiline_input(more_lines, ps1, ps2):
try:
return next(statement_iter)
except StopIteration:
raise EOFError

output = io.StringIO()
with (
patch.object(simple_interact, "statement_submitted_hook", hook),
patch.object(
simple_interact,
"multiline_input",
side_effect=fake_multiline_input,
),
patch.object(simple_interact, "_get_reader"),
patch.object(simple_interact, "append_history_file"),
patch("_pyrepl.readline._setup"),
contextlib.redirect_stdout(output),
contextlib.redirect_stderr(output),
):
simple_interact.run_multiline_interactive_console(console)

return output.getvalue(), console.locals

def test_hook_called_before_statement_execution(self):
statement = "if True:\n print('statement executed')\n"
escape_sequence = "\x1b]633;C\x07"
hook = MagicMock(
side_effect=lambda statement: print(escape_sequence, end="")
)
output, _ = self._run_interactive([statement], hook)

hook.assert_called_once_with(statement)
self.assertEqual(output, f"{escape_sequence}statement executed\n")

@force_not_colorized
def test_hook_exception_is_displayed(self):
hook = MagicMock(side_effect=RuntimeError("hook error"))

output, namespace = self._run_interactive(["x = 1"], hook)

self.assertIn("RuntimeError: hook error", output)
self.assertEqual(namespace["x"], 1)

def test_hook_not_called_for_repl_commands(self):
hook = MagicMock()
self._run_interactive(["clear"], hook)

hook.assert_not_called()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add an optional ``_pyrepl.simple_interact.statement_submitted_hook`` callback
that runs after a complete statement is submitted and before it is executed,
allowing external tools to implement features such as shell integration.
Loading