From aba294ac97a2b1bf2e6b8066bca4507fc3632127 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Tue, 13 Oct 2026 09:24:19 +0000 Subject: [PATCH 1/2] [core] Add ROOT::Internal::SetWarningHandler() for the Python bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PyROOT turns ROOT warnings into Python warnings with an error handler in its CPython extension module. It must not take the GIL while ROOT runs multithreaded: the reporting thread may hold ROOT's lock, which a thread holding the GIL may be waiting for. That check has to run before any Python code, so it can't move to Python with the rest of the handler. SetWarningHandler() installs an error handler that does the check and passes warnings to a plain function pointer. The handler returns whether it took the warning, so the Python bindings can decline one while a Python exception is pending; declined warnings go to DefaultErrorHandler() like every other message. 🤖 Done with the help of AI --- core/base/src/TErrorDefaultHandler.cxx | 27 ++++++++++++++++++++++++++ core/foundation/inc/TError.h | 9 +++++++++ 2 files changed, 36 insertions(+) diff --git a/core/base/src/TErrorDefaultHandler.cxx b/core/base/src/TErrorDefaultHandler.cxx index 1c64288a71e16..83d009adaa5fc 100644 --- a/core/base/src/TErrorDefaultHandler.cxx +++ b/core/base/src/TErrorDefaultHandler.cxx @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -181,3 +182,29 @@ void DefaultErrorHandler(Int_t level, Bool_t abort_bool, const char *location, c } } } + +namespace { +ROOT::Internal::WarningHandlerFunc_t gWarningHandler = nullptr; + +void WarningHandlerErrorHandler(Int_t level, Bool_t abort, const char *location, const char *msg) +{ + // Initialize gErrorIgnoreLevel from gEnv: the default handler prints + // nothing for a level that low + if (gErrorIgnoreLevel == kUnset) + DefaultErrorHandler(kUnset - 1, kFALSE, "", ""); + + if (level < gErrorIgnoreLevel) + return; + + if (level >= kWarning && level < kError && !gGlobalMutex && gWarningHandler(location ? location : "", msg)) + return; + + DefaultErrorHandler(level, abort, location, msg); +} +} // namespace + +void ROOT::Internal::SetWarningHandler(WarningHandlerFunc_t handler) +{ + gWarningHandler = handler; + SetErrorHandler(&WarningHandlerErrorHandler); +} diff --git a/core/foundation/inc/TError.h b/core/foundation/inc/TError.h index 696567c2c2e96..209701cdc665b 100644 --- a/core/foundation/inc/TError.h +++ b/core/foundation/inc/TError.h @@ -65,6 +65,15 @@ ErrorSystemMsgHandlerFunc_t SetErrorSystemMsgHandler(ErrorSystemMsgHandlerFunc_t void MinimalErrorHandler(int level, Bool_t abort, const char *location, const char *msg); +using WarningHandlerFunc_t = bool (*)(const char *location, const char *msg); + +/// For the Python bindings: install an error handler that passes warnings to +/// `handler`, unless it declines them by returning false, and everything else +/// to DefaultErrorHandler(). While ROOT runs multithreaded (gGlobalMutex is +/// set) warnings skip the handler: it takes the GIL, which a thread waiting +/// for ROOT's lock may hold while the reporting thread holds that lock. +void SetWarningHandler(WarningHandlerFunc_t handler); + } // namespace Internal } // namespace ROOT From 51efb9cc3c498f0f67d81c5418a8d27597d4b4b2 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Tue, 13 Oct 2026 09:24:21 +0000 Subject: [PATCH 2/2] [Python] Move the PyROOT application setup to Python MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RPyROOTApplication.cxx created the TApplication from the command line options, set up ROOT globals, and installed the error handler that turns ROOT warnings into Python warnings and the PyOS_InputHook that processes GUI events while the Python prompt waits for input. The TApplication is now constructed in _application.py through cppyy, with the same options from sys.argv; its constructor already saves the interpreter context, which the subclass did a second time. Of the ROOT globals only gBenchmark still needs setting: TROOT builds gStyle and TApplication sets gProgName, and evaluating gStyle created the proxies of TStyle and its base classes during every import. The warning handler and the input hook stay compiled, in PyROOTModule.cxx: - a Python callable passed to ROOT::Internal::SetWarningHandler() makes cppyy compile a trampoline during every import, which parses Python.h and takes half a second. The handler now declines warnings while a Python exception is pending, e.g. after a previous warning turned into an error in the same C++ call: PyErr_WarnExplicit() then replaced the exception with a SystemError. - a ctypes callback as input hook swallows the KeyboardInterrupt of a Ctrl-C arriving while it runs, so the Ctrl-C is lost. The setup runs while the ROOT module is being set up, so it goes through cppyy.gbl rather than the ROOT module, and the pythonization decorator test excludes the classes it now creates proxies of. 🤖 Done with the help of AI --- bindings/pyroot/pythonizations/CMakeLists.txt | 1 - .../python/ROOT/_application.py | 67 ++++- .../pythonizations/src/PyROOTModule.cxx | 62 ++++- .../pythonizations/src/RPyROOTApplication.cxx | 228 ------------------ .../pythonizations/src/RPyROOTApplication.h | 47 ---- .../test/pythonization_decorator.py | 4 +- 6 files changed, 125 insertions(+), 284 deletions(-) delete mode 100644 bindings/pyroot/pythonizations/src/RPyROOTApplication.cxx delete mode 100644 bindings/pyroot/pythonizations/src/RPyROOTApplication.h diff --git a/bindings/pyroot/pythonizations/CMakeLists.txt b/bindings/pyroot/pythonizations/CMakeLists.txt index c9e0cef95d546..bf247a7a32def 100644 --- a/bindings/pyroot/pythonizations/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/CMakeLists.txt @@ -10,7 +10,6 @@ set(cpp_sources src/PyROOTModule.cxx - src/RPyROOTApplication.cxx src/GenericPyz.cxx src/CPPInstancePyz.cxx ) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_application.py b/bindings/pyroot/pythonizations/python/ROOT/_application.py index cd77bd1375cf9..255636a0bffc2 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_application.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_application.py @@ -8,6 +8,8 @@ # For the list of contributors see $ROOTSYS/README/CREDITS. # ################################################################################ +import ctypes +import os import sys import time @@ -16,16 +18,75 @@ class PyROOTApplication(object): """ Application class for PyROOT. Configures the interactive usage of ROOT from Python. + + It is created while the ROOT module is set up, so it reaches C++ through + cppyy.gbl, as attribute lookups on the ROOT module would restart the setup. """ def __init__(self, config, is_ipython): - from ROOT.libROOTPythonizations import InitApplication + import cppyy + + if not cppyy.gbl.gApplication: + from ROOT.libROOTPythonizations import InstallWarningHandler - # Construct a TApplication for PyROOT - InitApplication(config.IgnoreCommandLineOptions) + self._create_application(config.IgnoreCommandLineOptions) + self._init_root_globals() + InstallWarningHandler() self._is_ipython = is_ipython + @staticmethod + def _create_application(ignore_cmd_line_opts): + """ + Create the TApplication. Unless ignore_cmd_line_opts is set, it gets + the command line options in sys.argv, up to a "-" or "--" that + separates them from the options for the Python script. For example, + to enable batch mode from the command line: + + python script_name.py -b -- user_arg1 ... user_argn + + or, if the script takes no options: + + python script_name.py -b + """ + import cppyy + + gbl = cppyy.gbl + + args = ["python"] + if not ignore_cmd_line_opts: + for arg in getattr(sys, "argv", [])[1:]: + if arg in ("-", "--"): + break + args.append(arg) + + # Bytes as given on the command line, and argv[argc] null like for main() + args = [os.fsencode(arg) for arg in args] + argc = ctypes.c_int(len(args)) + argv = (ctypes.c_char_p * (len(args) + 1))(*args, None) + app = gbl.TApplication("PyROOT", argc, ctypes.cast(argv, ctypes.POINTER(ctypes.c_char_p))) + # It is gApplication from now on + cppyy._backend.SetOwnership(app, False) + + # Prevent crashes on accessing history + gbl.Gl_histinit("-") + + # Prevent ROOT from exiting Python + app.SetReturnFromRun(True) + + @staticmethod + def _init_root_globals(): + """Set up gBenchmark, the one ROOT global that neither TROOT nor TApplication sets""" + import cppyy + + gbl = cppyy.gbl + + if not gbl.gBenchmark: + # The global owns the object, so Python must not delete it + benchmark = gbl.TBenchmark() + cppyy._backend.SetOwnership(benchmark, False) + gbl.gBenchmark = benchmark + @staticmethod def _ipython_config(): # Integrate IPython >= 5 with ROOT's event loop diff --git a/bindings/pyroot/pythonizations/src/PyROOTModule.cxx b/bindings/pyroot/pythonizations/src/PyROOTModule.cxx index d6da288a09050..6dce34a0e970e 100644 --- a/bindings/pyroot/pythonizations/src/PyROOTModule.cxx +++ b/bindings/pyroot/pythonizations/src/PyROOTModule.cxx @@ -11,15 +11,16 @@ // Bindings #include "PyROOTPythonize.h" -#include "RPyROOTApplication.h" // Cppyy #include "cpyrt/API.h" // ROOT +#include "TError.h" #include "TInterpreter.h" #include "TROOT.h" #include "TSystem.h" +#include "TVirtualPad.h" #include "RConfigure.h" // Standard @@ -98,15 +99,68 @@ PyObject *PyObjRefCounterAsStdAny(PyObject * /*self*/, PyObject *args) } // namespace PyROOT +namespace { + +/// Turns a ROOT warning into a Python warning, see ROOT::Internal::SetWarningHandler(). +bool WarningHandler(const char *location, const char *msg) +{ + PyGILState_STATE state = PyGILState_Ensure(); + // Python code can't run while an exception is pending, e.g. a previous + // warning turned into an error: decline, and ROOT prints the warning. + bool handled = !PyErr_Occurred(); + if (handled) { + // Prints or, per the warnings filters, raises: the exception stays + // pending until the call into C++ returns to Python. + PyErr_WarnExplicit(nullptr, msg, location, 0, "ROOT", nullptr); + } + PyGILState_Release(state); + return handled; +} + +int (*gPreviousInputHook)() = nullptr; + +/// Processes GUI events while the Python prompt waits for input. Runs no +/// Python code, so a Ctrl-C arriving meanwhile stays pending for the prompt. +int GUIEventInputHook() +{ + PyGILState_STATE state = PyGILState_Ensure(); + if (gPad && gPad->IsWeb()) + gPad->UpdateAsync(); + gSystem->ProcessEvents(); + PyGILState_Release(state); + + return gPreviousInputHook ? gPreviousInputHook() : 0; +} + +/// Turn ROOT warnings into Python warnings. +PyObject *InstallWarningHandler(PyObject * /*self*/, PyObject * /*args*/) +{ + ROOT::Internal::SetWarningHandler(&WarningHandler); + Py_RETURN_NONE; +} + +/// Install the PyOS_InputHook that processes GUI events, chaining to the +/// previous hook. +PyObject *InstallGUIEventInputHook(PyObject * /*self*/, PyObject * /*args*/) +{ + if (PyOS_InputHook != &GUIEventInputHook) { + gPreviousInputHook = PyOS_InputHook; + PyOS_InputHook = &GUIEventInputHook; + } + Py_RETURN_NONE; +} + +} // namespace + // Methods offered by the interface static PyMethodDef gPyROOTMethods[] = { {"AddCPPInstancePickling", (PyCFunction)PyROOT::AddCPPInstancePickling, METH_NOARGS, "Add a custom pickling mechanism for Cppyy Python proxy objects"}, {"AddPrettyPrintingPyz", (PyCFunction)PyROOT::AddPrettyPrintingPyz, METH_VARARGS, "Add pretty printing pythonization"}, - {"InitApplication", (PyCFunction)PyROOT::RPyROOTApplication::InitApplication, METH_VARARGS, - "Initialize interactive ROOT use from Python"}, - {"InstallGUIEventInputHook", (PyCFunction)PyROOT::RPyROOTApplication::InstallGUIEventInputHook, METH_NOARGS, + {"InstallWarningHandler", (PyCFunction)InstallWarningHandler, METH_NOARGS, + "Turn ROOT warnings into Python warnings"}, + {"InstallGUIEventInputHook", (PyCFunction)InstallGUIEventInputHook, METH_NOARGS, "Install an input hook to process GUI events"}, {"_CPPInstance__expand__", (PyCFunction)PyROOT::CPPInstanceExpand, METH_VARARGS, "Deserialize a pickled object"}, diff --git a/bindings/pyroot/pythonizations/src/RPyROOTApplication.cxx b/bindings/pyroot/pythonizations/src/RPyROOTApplication.cxx deleted file mode 100644 index 48fa9b8dc9bea..0000000000000 --- a/bindings/pyroot/pythonizations/src/RPyROOTApplication.cxx +++ /dev/null @@ -1,228 +0,0 @@ -// Author: Enric Tejedor CERN 04/2019 -// Original PyROOT code by Wim Lavrijsen, LBL - -/************************************************************************* - * Copyright (C) 1995-2019, Rene Brun and Fons Rademakers. * - * All rights reserved. * - * * - * For the licensing terms see $ROOTSYS/LICENSE. * - * For the list of contributors see $ROOTSYS/README/CREDITS. * - *************************************************************************/ - -// Bindings -#include -#include "RPyROOTApplication.h" - -// ROOT -#include "TInterpreter.h" -#include "TSystem.h" -#include "TBenchmark.h" -#include "TStyle.h" -#include "TError.h" -#include "Getline.h" -#include "TVirtualMutex.h" -#include "TVirtualPad.h" -#include "TROOT.h" - -//////////////////////////////////////////////////////////////////////////// -/// \brief Create an RPyROOTApplication. -/// \param[in] ignoreCmdLineOpts True if Python command line options should -/// be ignored. -/// \return false if gApplication is not null, true otherwise. -/// -/// If ignoreCmdLineOpts is false, this method processes the command line -/// arguments from sys.argv. A distinction between arguments for -/// TApplication and user arguments can be made by using "-" or "--" as a -/// separator on the command line. -/// -/// For example, to enable batch mode from the command line: -/// > python script_name.py -b -- user_arg1 ... user_argn -/// or, if the user script receives no arguments: -/// > python script_name.py -b -bool PyROOT::RPyROOTApplication::CreateApplication(int ignoreCmdLineOpts) -{ - if (!gApplication) { - int argc = 1; - char **argv = nullptr; - - if (ignoreCmdLineOpts) { - // Last argv must be null (see https://en.cppreference.com/cpp/language/main_function) - argv = new char *[argc + 1] {}; - } else { - // Retrieve sys.argv list from Python - PyObject *argl = PySys_GetObject("argv"); - - if (argl) { - Py_ssize_t size = PyList_Size(argl); - if (size > 0) - argc = static_cast(size); - } - - // Last argv must be null (see https://en.cppreference.com/cpp/language/main_function) - argv = new char *[argc + 1] {}; - - for (int i = 1; i < argc; ++i) { - PyObject *item = PyList_GetItem(argl, i); - const char *argi = PyUnicode_AsUTF8AndSize(item, nullptr); - - if (strcmp(argi, "-") == 0 || strcmp(argi, "--") == 0) { - // Stop collecting options, the remaining are for the Python script - argc = i; // includes program name - break; - } - argv[i] = const_cast(argi); - } - } - - argv[0] = (char *)"python"; - - gApplication = new RPyROOTApplication("PyROOT", &argc, argv); - delete[] argv; // TApplication ctor has copied argv, so done with it - - return true; - } - - return false; -} - -//////////////////////////////////////////////////////////////////////////// -/// \brief Setup the basic ROOT globals gBenchmark, gStyle and gProgname, -/// if not already set. -void PyROOT::RPyROOTApplication::InitROOTGlobals() -{ - if (!gBenchmark) - gBenchmark = new TBenchmark(); - if (!gStyle) - gStyle = new TStyle(); - - if (!gProgName) // should have been set by TApplication - gSystem->SetProgname("python"); -} - -//////////////////////////////////////////////////////////////////////////// -/// \brief Translate ROOT error/warning to Python. -static void ErrMsgHandler(int level, Bool_t abort, const char *location, const char *msg) -{ - // Initialization from gEnv (the default handler will return w/o msg b/c level too low) - if (gErrorIgnoreLevel == kUnset) - ::DefaultErrorHandler(kUnset - 1, kFALSE, "", ""); - - if (level < gErrorIgnoreLevel) - return; - - // Turn warnings into Python warnings - if (level >= kError) { - ::DefaultErrorHandler(level, abort, location, msg); - } else if (level >= kWarning) { - static const char *emptyString = ""; - if (!location) - location = emptyString; - // This warning might be triggered while holding the ROOT lock, while - // some other thread is holding the GIL and waiting for the ROOT lock. - // That will trigger a deadlock. - // So if ROOT is in MT mode, use ROOT's error handler that doesn't take - // the GIL. - if (!gGlobalMutex) { - // Either printout or raise exception, depending on user settings - auto state = PyGILState_Ensure(); - PyErr_WarnExplicit(NULL, (char *)msg, (char *)location, 0, (char *)"ROOT", NULL); - PyGILState_Release(state); - } else { - ::DefaultErrorHandler(level, abort, location, msg); - } - } else { - ::DefaultErrorHandler(level, abort, location, msg); - } -} - -//////////////////////////////////////////////////////////////////////////// -/// \brief Install the ROOT message handler which will turn ROOT error -/// messages into Python exceptions. -void PyROOT::RPyROOTApplication::InitROOTMessageCallback() -{ - SetErrorHandler((ErrorHandlerFunc_t)&ErrMsgHandler); -} - -//////////////////////////////////////////////////////////////////////////// -/// \brief Initialize an RPyROOTApplication. -/// \param[in] self Always null, since this is a module function. -/// \param[in] args [0] Boolean that tells whether to ignore the command line options. -PyObject *PyROOT::RPyROOTApplication::InitApplication(PyObject * /*self*/, PyObject *args) -{ - int argc = PyTuple_Size(args); - if (argc == 1) { - PyObject *ignoreCmdLineOpts = PyTuple_GetItem(args, 0); - - if (!PyBool_Check(ignoreCmdLineOpts)) { - PyErr_SetString(PyExc_TypeError, "Expected boolean type as argument."); - return nullptr; - } - - if (CreateApplication(PyObject_IsTrue(ignoreCmdLineOpts))) { - InitROOTGlobals(); - InitROOTMessageCallback(); - } - } else { - PyErr_Format(PyExc_TypeError, "Expected 1 argument, %d passed.", argc); - return nullptr; - } - - Py_RETURN_NONE; -} - -//////////////////////////////////////////////////////////////////////////// -/// \brief Construct a TApplication for PyROOT. -/// \param[in] name Application class name. -/// \param[in] argc Number of arguments. -/// \param[in] argv Arguments. -PyROOT::RPyROOTApplication::RPyROOTApplication(const char *name, int *argc, char **argv) - : TApplication(name, argc, argv) -{ - // Save current interpreter context - gInterpreter->SaveContext(); - gInterpreter->SaveGlobalsContext(); - - // Prevent crashes on accessing history - Gl_histinit((char *)"-"); - - // Prevent ROOT from exiting python - SetReturnFromRun(true); -} - -namespace { -static int (*sOldInputHook)() = nullptr; -static PyThreadState *sInputHookEventThreadState = nullptr; - -static int EventInputHook() -{ - // This method is supposed to be called from CPython's command line and - // drives the GUI - PyEval_RestoreThread(sInputHookEventThreadState); - if (gPad && gPad->IsWeb()) - gPad->UpdateAsync(); - gSystem->ProcessEvents(); - PyEval_SaveThread(); - - if (sOldInputHook) - return sOldInputHook(); - - return 0; -} - -} // unnamed namespace - -//////////////////////////////////////////////////////////////////////////// -/// \brief Install a method hook for sending events to the GUI. -/// \param[in] self Always null, since this is a module function. -/// \param[in] args Pointer to an empty Python tuple. -PyObject *PyROOT::RPyROOTApplication::InstallGUIEventInputHook(PyObject * /* self */, PyObject * /* args */) -{ - if (PyOS_InputHook && PyOS_InputHook != &EventInputHook) - sOldInputHook = PyOS_InputHook; - - sInputHookEventThreadState = PyThreadState_Get(); - - PyOS_InputHook = (int (*)()) & EventInputHook; - - Py_RETURN_NONE; -} diff --git a/bindings/pyroot/pythonizations/src/RPyROOTApplication.h b/bindings/pyroot/pythonizations/src/RPyROOTApplication.h deleted file mode 100644 index 109f0b731ec29..0000000000000 --- a/bindings/pyroot/pythonizations/src/RPyROOTApplication.h +++ /dev/null @@ -1,47 +0,0 @@ -// Author: Enric Tejedor CERN 04/2019 -// Original PyROOT code by Wim Lavrijsen, LBL - -/************************************************************************* - * Copyright (C) 1995-2019, Rene Brun and Fons Rademakers. * - * All rights reserved. * - * * - * For the licensing terms see $ROOTSYS/LICENSE. * - * For the list of contributors see $ROOTSYS/README/CREDITS. * - *************************************************************************/ - -#ifndef ROOT_PyROOTApplication -#define ROOT_PyROOTApplication - -// ROOT -#include "TApplication.h" - -namespace PyROOT { - -// clang-format off -/** -\class PyROOT::RPyROOTApplication -\brief Interactive application for Python. - - The RPyROOTApplication sets up the nuts and bolts for interactive ROOT use - from Python, closely following TRint. Note that not everything is done here, - some bits are more easily done in Python and can be found in _application.py. -*/ -// clang-format on - -class RPyROOTApplication : public TApplication { -public: - static PyObject *InitApplication(PyObject *self, PyObject *args); - static PyObject *InstallGUIEventInputHook(PyObject *self, PyObject *args); - - RPyROOTApplication(const char *name, int *argc, char **argv); - virtual ~RPyROOTApplication() {} - -private: - static bool CreateApplication(int ignoreCmdLineOpts); - static void InitROOTGlobals(); - static void InitROOTMessageCallback(); -}; - -} // namespace PyROOT - -#endif diff --git a/bindings/pyroot/pythonizations/test/pythonization_decorator.py b/bindings/pyroot/pythonizations/test/pythonization_decorator.py index 78172fb68b8d6..513c5c38f628f 100644 --- a/bindings/pyroot/pythonizations/test/pythonization_decorator.py +++ b/bindings/pyroot/pythonizations/test/pythonization_decorator.py @@ -14,7 +14,9 @@ class PythonizationDecorator(unittest.TestCase): # processed by the pythonizors. Just ignore them exclude = [ 'TClass', 'TSystem', 'TUnixSystem', 'TMacOSXSystem', 'TWinNTSystem', 'TDictionary', 'TEnv', 'TInterpreter', 'TApplication', - 'TObject', 'TNamed', 'TROOT', 'TIter', 'TDirectory', 'TString' ] + 'TObject', 'TNamed', 'TROOT', 'TIter', 'TDirectory', 'TString', + # Used when the PyROOT application is set up + 'TQObject', 'TBenchmark' ] # Helpers def _define_class(self, class_name, namespace=None):