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
6 changes: 6 additions & 0 deletions Lib/_pyrepl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

from collections.abc import Callable as _Callable


# Declared here so external tools can detect support and install a callback.
pre_execution_hook: _Callable[[str], object] | None = None
9 changes: 9 additions & 0 deletions Lib/_pyrepl/simple_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import code
import warnings

import _pyrepl

from .readline import _get_reader, multiline_input, append_history_file


Expand Down Expand Up @@ -145,6 +147,13 @@ def maybe_run_command(statement: str) -> bool:
if maybe_run_command(statement):
continue

pre_execution_hook = getattr(_pyrepl, "pre_execution_hook", None)
if callable(pre_execution_hook):
try:
pre_execution_hook(statement)
except Exception:
pass

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
71 changes: 70 additions & 1 deletion Lib/test/test_pyrepl/test_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
import io
import warnings
import unittest
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from textwrap import dedent

import _pyrepl

from test.support import force_not_colorized

from _pyrepl.console import InteractiveColoredConsole
Expand Down Expand Up @@ -299,3 +301,70 @@ def f():
count = sum("'return' in a 'finally' block" in str(w.message)
for w in caught)
self.assertEqual(count, 1)


class TestPreExecutionHook(unittest.TestCase):

def test_hook_is_unset_by_default(self):
self.assertIsNone(_pyrepl.pre_execution_hook)

def _run_interactive(self, statements, *, pre_execution_hook=None):
from _pyrepl.simple_interact import run_multiline_interactive_console

console = InteractiveColoredConsole()
statement_iter = iter(statements)

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

patches = [
patch(
"_pyrepl.simple_interact.multiline_input",
side_effect=fake_multiline_input,
),
patch("_pyrepl.simple_interact._get_reader"),
patch("_pyrepl.simple_interact.append_history_file"),
patch("_pyrepl.readline._setup"),
]
if pre_execution_hook is not None:
patches.append(
patch.object(
_pyrepl,
"pre_execution_hook",
pre_execution_hook,
)
)

output = io.StringIO()
with contextlib.ExitStack() as stack:
for context_manager in patches:
stack.enter_context(context_manager)
stack.enter_context(contextlib.redirect_stdout(output))
stack.enter_context(contextlib.redirect_stderr(output))
run_multiline_interactive_console(console)

return output.getvalue(), console.locals

def test_hook_called_with_statement(self):
hook = MagicMock()
self._run_interactive(["x = 1"], pre_execution_hook=hook)
hook.assert_called_once_with("x = 1")

def test_hook_exception_does_not_break_repl(self):
hook = MagicMock(side_effect=RuntimeError("hook error"))
output, namespace = self._run_interactive(
["x = 1", "y = 2"],
pre_execution_hook=hook,
)
self.assertEqual(hook.call_count, 2)
self.assertEqual(namespace["x"], 1)
self.assertEqual(namespace["y"], 2)
self.assertNotIn("hook error", output)

def test_hook_not_called_for_repl_commands(self):
hook = MagicMock()
self._run_interactive(["clear"], pre_execution_hook=hook)
hook.assert_not_called()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add an optional ``_pyrepl.pre_execution_hook`` callable that is invoked with
the submitted source before each interactive statement is executed. This
allows external tools to implement features such as shell integration.
Loading