diff --git a/Include/pythonrun.h b/Include/pythonrun.h index 154c7450cb934f..85e3f070ab6169 100644 --- a/Include/pythonrun.h +++ b/Include/pythonrun.h @@ -17,9 +17,9 @@ PyAPI_FUNC(void) PyErr_Display(PyObject *, PyObject *, PyObject *); PyAPI_FUNC(void) PyErr_DisplayException(PyObject *); #endif - /* Stuff with no proper home (yet) */ PyAPI_DATA(int) (*PyOS_InputHook)(void); +PyAPI_FUNC(int) _PyOS_CallInputHook(void); /* Stack size, in "pointers" (so we get extra safety margins on 64-bit platforms). On a 32-bit platform, this translates diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 0045425cdddb79..7e171df8fd2409 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -24,6 +24,7 @@ from contextlib import contextmanager from dataclasses import dataclass, field, fields import unicodedata +import ctypes from _colorize import can_colorize, ANSIColors # type: ignore[import-not-found] @@ -239,6 +240,7 @@ class Reader: lxy: tuple[int, int] = field(init=False) calc_screen: CalcScreen = field(init=False) scheduled_commands: list[str] = field(default_factory=list) + input_hook_addr: ctypes.c_void_p | None = None def __post_init__(self) -> None: # Enable the use of `insert` without a `prepare` call - necessary to @@ -643,6 +645,7 @@ def handle1(self, block: bool = True) -> bool: self.dirty = True while True: + ctypes.pythonapi._PyOS_CallInputHook() event = self.console.get_event(block) if not event: # can only happen if we're not blocking return False diff --git a/Lib/test/test_pyrepl/test_reader.py b/Lib/test/test_pyrepl/test_reader.py index c9b03d5e711539..c601a2ccc03840 100644 --- a/Lib/test/test_pyrepl/test_reader.py +++ b/Lib/test/test_pyrepl/test_reader.py @@ -1,8 +1,10 @@ import itertools import functools from unittest import TestCase +from unittest.mock import MagicMock from .support import handle_all_events, handle_events_narrow_console, code_to_events, prepare_reader +from test.support import import_helper from _pyrepl.console import Event @@ -176,3 +178,24 @@ def test_newline_within_block_trailing_whitespace(self): ) self.assert_screen_equals(reader, expected) self.assertTrue(reader.finished) + + def test_input_hook_is_called_if_set(self): + import_helper.import_module('ctypes') + from ctypes import pythonapi, c_int, CFUNCTYPE, c_void_p, POINTER, cast + + CMPFUNC = CFUNCTYPE(c_int) + the_mock = MagicMock() + + def input_hook(): + the_mock() + return 0 + + the_input_hook = CMPFUNC(input_hook) + prev_value = c_void_p.in_dll(pythonapi, "PyOS_InputHook").value + try: + c_void_p.in_dll(pythonapi, "PyOS_InputHook").value = cast(the_input_hook, c_void_p).value + events = code_to_events("a") + reader, _ = handle_all_events(events) + the_mock.assert_called() + finally: + c_void_p.in_dll(pythonapi, "PyOS_InputHook").value = prev_value diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-05-31-12-06-11.gh-issue-119842.tCGVsv.rst b/Misc/NEWS.d/next/Core and Builtins/2024-05-31-12-06-11.gh-issue-119842.tCGVsv.rst new file mode 100644 index 00000000000000..2fcb170f6226e5 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-05-31-12-06-11.gh-issue-119842.tCGVsv.rst @@ -0,0 +1 @@ +Honor :c:func:`PyOS_InputHook` in the new REPL. Patch by Pablo Galindo diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 67bbbd01ca0c48..9948de32c6052a 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -3329,3 +3329,15 @@ PyOS_setsig(int sig, PyOS_sighandler_t handler) return oldhandler; #endif } + +int _PyOS_CallInputHook(void) +{ + // The return value is ignored, but let's handle it anyway + int result = 0; + if (PyOS_InputHook) { + Py_BEGIN_ALLOW_THREADS; + result = PyOS_InputHook(); + Py_END_ALLOW_THREADS; + } + return result; +}