Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support interactive graphics in python scripts #17587

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bindings/pyroot/pythonizations/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 0 additions & 12 deletions bindings/pyroot/pythonizations/python/ROOT/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
18 changes: 4 additions & 14 deletions bindings/pyroot/pythonizations/python/ROOT/_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# 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. #
################################################################################

from . import pythonization

Comment on lines +35 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the usual copyright header here and I would also add some documentation that will end up in the Python Interface section of the docs https://root.cern.ch/doc/master/group__Pythonizations.html. Take for example https://github.com/root-project/root/blob/master/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tfile.py

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vepadulano

I apply your changes and rebase commits

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are still missing the documentation as I suggested, see the \pythondoc block in _tfile.py. I think it should describe the fact that the Update method has been pythonized and a simple example usage.

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 _TCanvas_Update(self, block = False):
"""
Updates the canvas.
Also blocks script execution and runs the ROOT graphics event loop until the <space> 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.
"""

from ROOT import gROOT
import os
import sys

self._Update()

# blocking flag is not set
if not block:
return

# 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 <space> key to continue")

if os.name == 'nt':
wait_press_windows()
else:
wait_press_posix()


@pythonization('TCanvas')
def pythonize_tcanvas(klass):
# Parameters:
# klass: class to be pythonized

klass._Update = klass.Update
klass.Update = _TCanvas_Update

Loading