From 28bfdfe34ba8889c04dfc1d4c7089958affdab89 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Fri, 31 May 2024 12:06:23 +0100 Subject: [PATCH 1/2] gh-119842: Honor PyOS_InputHook in the new REPL Signed-off-by: Pablo Galindo --- Lib/_pyrepl/reader.py | 10 ++++++++ Lib/test/test_pyrepl/test_reader.py | 23 +++++++++++++++++++ ...-05-31-12-06-11.gh-issue-119842.tCGVsv.rst | 1 + 3 files changed, 34 insertions(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-05-31-12-06-11.gh-issue-119842.tCGVsv.rst diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 0045425cdddb79..81409fd2af389f 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: + self.call_PyOS_InputHook() event = self.console.get_event(block) if not event: # can only happen if we're not blocking return False @@ -701,3 +704,10 @@ def bind(self, spec: KeySpec, command: CommandName) -> None: def get_unicode(self) -> str: """Return the current buffer as a unicode string.""" return "".join(self.buffer) + + def call_PyOS_InputHook(self): + if self.input_hook_addr is None: + self.input_hook_addr = ctypes.c_void_p.in_dll(ctypes.pythonapi, 'PyOS_InputHook').value + if not self.input_hook_addr: + return + ctypes.PYFUNCTYPE(ctypes.c_int)(self.input_hook_addr)() 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 From 30960e7ee3cd16b4659f5aea5ee1bede0c6eae6b Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 31 May 2024 15:50:28 -0400 Subject: [PATCH 2/2] Fix threading in calling PyOS_InputHook from new REPL --- Include/pythonrun.h | 2 +- Lib/_pyrepl/reader.py | 9 +-------- Python/pylifecycle.c | 12 ++++++++++++ 3 files changed, 14 insertions(+), 9 deletions(-) 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 81409fd2af389f..7e171df8fd2409 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -645,7 +645,7 @@ def handle1(self, block: bool = True) -> bool: self.dirty = True while True: - self.call_PyOS_InputHook() + ctypes.pythonapi._PyOS_CallInputHook() event = self.console.get_event(block) if not event: # can only happen if we're not blocking return False @@ -704,10 +704,3 @@ def bind(self, spec: KeySpec, command: CommandName) -> None: def get_unicode(self) -> str: """Return the current buffer as a unicode string.""" return "".join(self.buffer) - - def call_PyOS_InputHook(self): - if self.input_hook_addr is None: - self.input_hook_addr = ctypes.c_void_p.in_dll(ctypes.pythonapi, 'PyOS_InputHook').value - if not self.input_hook_addr: - return - ctypes.PYFUNCTYPE(ctypes.c_int)(self.input_hook_addr)() 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; +}