Skip to content

Commit 8ede9b2

Browse files
committed
Address feedback
1 parent 0ef602c commit 8ede9b2

3 files changed

Lines changed: 44 additions & 57 deletions

File tree

‎Lib/_pyrepl/__init__.py‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,3 @@
1717
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
1818
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
1919
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20-
21-
from collections.abc import Callable as _Callable
22-
23-
24-
# Declared here so external tools can detect support and install a callback.
25-
pre_execution_hook: _Callable[[str], object] | None = None

‎Lib/_pyrepl/simple_interact.py‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,25 @@
3030
import code
3131
import warnings
3232

33-
import _pyrepl
34-
3533
from .readline import _get_reader, multiline_input, append_history_file
3634

3735

36+
# Called after the user submits a Python statement, but before it is executed.
37+
# The hook is not called for PyREPL commands such as ``clear``. Exceptions raised
38+
# by the hook and its return value are ignored. External tools such as IDEs can
39+
# install a hook to augment the behavior of the REPL.
40+
#
41+
# For example, VS Code can mark the start of command execution:
42+
#
43+
# from _pyrepl import simple_interact
44+
#
45+
# def vscode_statement_submitted(statement: str) -> None:
46+
# print("\x1b]633;C\x07", end="")
47+
#
48+
# simple_interact.statement_submitted_hook = vscode_statement_submitted
49+
statement_submitted_hook = None
50+
51+
3852
_error: tuple[type[Exception], ...] | type[Exception]
3953
try:
4054
from .unix_console import _error
@@ -147,10 +161,9 @@ def maybe_run_command(statement: str) -> bool:
147161
if maybe_run_command(statement):
148162
continue
149163

150-
pre_execution_hook = getattr(_pyrepl, "pre_execution_hook", None)
151-
if callable(pre_execution_hook):
164+
if statement_submitted_hook is not None:
152165
try:
153-
pre_execution_hook(statement)
166+
statement_submitted_hook(statement)
154167
except Exception:
155168
pass
156169

‎Lib/test/test_pyrepl/test_interact.py‎

Lines changed: 26 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
from unittest.mock import MagicMock, patch
66
from textwrap import dedent
77

8-
import _pyrepl
9-
108
from test.support import force_not_colorized
119

10+
from _pyrepl import simple_interact
1211
from _pyrepl.console import InteractiveColoredConsole
1312
from _pyrepl.simple_interact import _more_lines
1413

@@ -303,14 +302,9 @@ def f():
303302
self.assertEqual(count, 1)
304303

305304

306-
class TestPreExecutionHook(unittest.TestCase):
307-
308-
def test_hook_is_unset_by_default(self):
309-
self.assertIsNone(_pyrepl.pre_execution_hook)
310-
311-
def _run_interactive(self, statements, *, pre_execution_hook=None):
312-
from _pyrepl.simple_interact import run_multiline_interactive_console
305+
class TestStatementSubmittedHook(unittest.TestCase):
313306

307+
def _run_interactive(self, statements, hook):
314308
console = InteractiveColoredConsole()
315309
statement_iter = iter(statements)
316310

@@ -320,51 +314,37 @@ def fake_multiline_input(more_lines, ps1, ps2):
320314
except StopIteration:
321315
raise EOFError
322316

323-
patches = [
324-
patch(
325-
"_pyrepl.simple_interact.multiline_input",
317+
output = io.StringIO()
318+
with (
319+
patch.object(simple_interact, "statement_submitted_hook", hook),
320+
patch.object(
321+
simple_interact,
322+
"multiline_input",
326323
side_effect=fake_multiline_input,
327324
),
328-
patch("_pyrepl.simple_interact._get_reader"),
329-
patch("_pyrepl.simple_interact.append_history_file"),
325+
patch.object(simple_interact, "_get_reader"),
326+
patch.object(simple_interact, "append_history_file"),
330327
patch("_pyrepl.readline._setup"),
331-
]
332-
if pre_execution_hook is not None:
333-
patches.append(
334-
patch.object(
335-
_pyrepl,
336-
"pre_execution_hook",
337-
pre_execution_hook,
338-
)
339-
)
340-
341-
output = io.StringIO()
342-
with contextlib.ExitStack() as stack:
343-
for context_manager in patches:
344-
stack.enter_context(context_manager)
345-
stack.enter_context(contextlib.redirect_stdout(output))
346-
stack.enter_context(contextlib.redirect_stderr(output))
347-
run_multiline_interactive_console(console)
328+
contextlib.redirect_stdout(output),
329+
contextlib.redirect_stderr(output),
330+
):
331+
simple_interact.run_multiline_interactive_console(console)
348332

349333
return output.getvalue(), console.locals
350334

351-
def test_hook_called_with_statement(self):
352-
hook = MagicMock()
353-
self._run_interactive(["x = 1"], pre_execution_hook=hook)
354-
hook.assert_called_once_with("x = 1")
355-
356-
def test_hook_exception_does_not_break_repl(self):
357-
hook = MagicMock(side_effect=RuntimeError("hook error"))
358-
output, namespace = self._run_interactive(
359-
["x = 1", "y = 2"],
360-
pre_execution_hook=hook,
335+
def test_hook_called_before_statement_execution(self):
336+
statement = "print('statement executed')"
337+
escape_sequence = "\x1b]633;C\x07"
338+
hook = MagicMock(
339+
side_effect=lambda statement: print(escape_sequence, end="")
361340
)
362-
self.assertEqual(hook.call_count, 2)
363-
self.assertEqual(namespace["x"], 1)
364-
self.assertEqual(namespace["y"], 2)
365-
self.assertNotIn("hook error", output)
341+
output, _ = self._run_interactive([statement], hook)
342+
343+
hook.assert_called_once_with(statement)
344+
self.assertEqual(output, f"{escape_sequence}statement executed\n")
366345

367346
def test_hook_not_called_for_repl_commands(self):
368347
hook = MagicMock()
369-
self._run_interactive(["clear"], pre_execution_hook=hook)
348+
self._run_interactive(["clear"], hook)
349+
370350
hook.assert_not_called()

0 commit comments

Comments
 (0)