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): 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