diff --git a/bindings/pyroot/pythonizations/CMakeLists.txt b/bindings/pyroot/pythonizations/CMakeLists.txt index e9c9f4fb1cc65..7f64e9feb215c 100644 --- a/bindings/pyroot/pythonizations/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/CMakeLists.txt @@ -114,6 +114,7 @@ set(py_sources ROOT/_pythonization/_tfile.py ROOT/_pythonization/_tfilemerger.py ROOT/_pythonization/_tformula.py + ROOT/_pythonization/_tcanvas.py ROOT/_pythonization/_tgraph.py ROOT/_pythonization/_tgraph2d.py ROOT/_pythonization/_th1.py diff --git a/bindings/pyroot/pythonizations/python/ROOT/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/__init__.py index b8b0e04dfa7ad..c4063d835e10d 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/__init__.py +++ b/bindings/pyroot/pythonizations/python/ROOT/__init__.py @@ -173,15 +173,3 @@ def find_spec(self, fullname: str, path, target=None) -> ModuleSpec: import JupyROOT from . import JsMVA -# Register cleanup -import atexit - - -def cleanup(): - # If spawned, stop thread which processes ROOT events - facade = sys.modules[__name__] - if "app" in facade.__dict__ and hasattr(facade.__dict__["app"], "process_root_events"): - facade.__dict__["app"].keep_polling = False - facade.__dict__["app"].process_root_events.join() - -atexit.register(cleanup) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_application.py b/bindings/pyroot/pythonizations/python/ROOT/_application.py index a0b5655111eea..3dfb5838128f7 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_application.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_application.py @@ -78,28 +78,18 @@ def init_graphics(self): if self._is_ipython and 'IPython' in sys.modules and sys.modules['IPython'].version_info[0] >= 5: # ipython and notebooks, register our event processing with their hooks self._ipython_config() - elif sys.flags.interactive == 1 or not hasattr(__main__, '__file__') or gSystem.InheritsFrom('TMacOSXSystem'): + elif (sys.flags.interactive == 1 or not hasattr(__main__, '__file__')) and not gSystem.InheritsFrom('TWinNTSystem'): # Python in interactive mode, use the PyOS_InputHook to call our event processing # - sys.flags.interactive checks for the -i flags passed to python # - __main__ does not have the attribute __file__ if the Python prompt is started directly - # - MacOS does not allow to run a second thread to process events, fall back to the input hook + # - does not work properly on Windows self._inputhook_config() + gEnv.SetValue("WebGui.ExternalProcessEvents", "yes") else: - # Python in script mode, start a separate thread for the event processing + # Python in script mode, instead of separate thread methods like canvas.Update should run events # indicate that ProcessEvents called in different thread, let ignore thread id checks in RWebWindow gEnv.SetValue("WebGui.ExternalProcessEvents", "yes") - def _process_root_events(self): - while self.keep_polling: - gSystem.ProcessEvents() - time.sleep(0.01) - import threading - self.keep_polling = True # Used to shut down the thread safely at teardown time - update_thread = threading.Thread(None, _process_root_events, None, (self,)) - self.process_root_events = update_thread # The thread is joined at teardown time - update_thread.daemon = True - update_thread.start() - self._set_display_hook() diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcanvas.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcanvas.py new file mode 100644 index 0000000000000..c1a315f79e0cb --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcanvas.py @@ -0,0 +1,145 @@ +# Author: Sergey Linev GSI 01/2025 + +################################################################################ +# Copyright (C) 1995-2025, Rene Brun and Fons Rademakers. # +# All rights reserved. # +# # +# For the licensing terms see $ROOTSYS/LICENSE. # +# For the list of contributors see $ROOTSYS/README/CREDITS. # +################################################################################ + +r''' +\pythondoc TCanvas + +Functionality of TCanvas::Update() method was extended to support interactive +graphics in the python scripts. If extra block parameter is True, script execution +will be suspended until key pressed by user. Simple example: + +\code{.py} +\endcode +import ROOT + +c = ROOT.TCanvas() +h = ROOT.TH1I("h1", "h1", 100, -5, 5) +h.FillRandom("gaus", 10000) +h.Draw("") + +# block here until space is pressed +c.Update(True) + +# continues after is pressed +c.SaveAs("canvas.root") +\endpythondoc +''' + +from . import pythonization + +def wait_press_windows(): + from ROOT import gSystem + import msvcrt + import time + + while not gSystem.ProcessEvents(): + if msvcrt.kbhit(): + k = msvcrt.getch() + if k[0] == 32: + break + else: + time.sleep(0.01) + + +def wait_press_posix(): + from ROOT import gSystem + import sys + import select + import tty + import termios + import time + + old_settings = termios.tcgetattr(sys.stdin) + + tty.setcbreak(sys.stdin.fileno()) + + try: + + while not gSystem.ProcessEvents(): + c = '' + if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []): + c = sys.stdin.read(1) + if (c == '\x20'): + break + time.sleep(0.01) + + finally: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) + +def run_root_event_loop(): + from ROOT import gROOT + import os + import sys + + # no special handling in batch mode + if gROOT.IsBatch(): + return + + # no special handling in case of notebooks + if 'IPython' in sys.modules and sys.modules['IPython'].version_info[0] >= 5: + return + + print("Press key to continue") + + if os.name == 'nt': + wait_press_windows() + else: + wait_press_posix() + + +def _TCanvas_Update(self, block = False): + """ + Updates the canvas. + Also blocks script execution and runs the ROOT graphics event loop until the keyword is pressed, + but only if the following conditions are met: + * The `block` optional argument is set to `True`. + * ROOT graphics are enabled, i.e. `ROOT.gROOT.IsBatch() == False`. + * The script is running not in ipython notebooks. + """ + + self._Update() + + # run loop if block flag is set + if block: + run_root_event_loop() + + +def _TCanvas_Draw(self, block = False): + """ + Draw the canvas. + Also blocks script execution and runs the ROOT graphics event loop until the keyword is pressed, + but only if the following conditions are met: + * The `block` optional argument is set to `True`. + * ROOT graphics are enabled, i.e. `ROOT.gROOT.IsBatch() == False`. + * The script is running not in ipython notebooks. + """ + + if isinstance(block, str): + self._Draw(block) + return + + self._Draw() + + # run loop if block flag is set + if block == True: + self._Update() + run_root_event_loop() + + +@pythonization('TCanvas') +def pythonize_tcanvas(klass): + # Parameters: + # klass: class to be pythonized + + klass._Update = klass.Update + klass._Draw = klass.Draw + klass.Update = _TCanvas_Update + klass.Draw = _TCanvas_Draw +