diff --git a/README/ReleaseNotes/v638/index.md b/README/ReleaseNotes/v638/index.md index 167addf280164..8e3575a524483 100644 --- a/README/ReleaseNotes/v638/index.md +++ b/README/ReleaseNotes/v638/index.md @@ -61,6 +61,11 @@ RooAbsData *dataForChan = found != splits.end() ? found->get() : nullptr; to numbers such as 8 would share one 3-d histogram among 8 threads, greatly reducing the memory consumption. This might slow down execution if the histograms are filled at very high rates. Use lower number in this case. + +## JavaScript ROOT +- A new configuration option `Jupyter.JSRoot` was added in .rootrc to set the default mode for JSROOT in Jupyter notebooks (on or off). + + ## Versions of built-in packages * The version of openssl has been updated to 3.5.0 diff --git a/bindings/jupyroot/python/JupyROOT/helpers/utils.py b/bindings/jupyroot/python/JupyROOT/helpers/utils.py index 82836a07951b3..f845d8128913b 100644 --- a/bindings/jupyroot/python/JupyROOT/helpers/utils.py +++ b/bindings/jupyroot/python/JupyROOT/helpers/utils.py @@ -1,8 +1,8 @@ # -*- coding:utf-8 -*- -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Author: Danilo Piparo CERN -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- ################################################################################ # Copyright (C) 1995-2020, Rene Brun and Fons Rademakers. # @@ -15,27 +15,27 @@ from __future__ import print_function +import fnmatch import os +import re import sys -import select import tempfile -import itertools -import re -import fnmatch import time +from contextlib import contextmanager from datetime import datetime from hashlib import sha1 -from contextlib import contextmanager from subprocess import check_output -from IPython import get_ipython, display -from IPython.core.extensions import ExtensionManager + import ROOT +from IPython import display, get_ipython +from IPython.core.extensions import ExtensionManager + from JupyROOT.helpers import handlers # We want iPython to take over the graphics ROOT.gROOT.SetBatch() -cppMIME = 'text/x-c++src' +cppMIME = "text/x-c++src" # Keep display handle for canvases to be able update them _canvasHandles = {} @@ -106,69 +106,87 @@ """ -TBufferJSONErrorMessage="The TBufferJSON class is necessary for JS visualisation to work and cannot be found. Did you enable the http module (-D http=ON for CMake)?" +TBufferJSONErrorMessage = "The TBufferJSON class is necessary for JS visualisation to work and cannot be found. Did you enable the http module (-D http=ON for CMake)?" + def TBufferJSONAvailable(): - if hasattr(ROOT,"TBufferJSON"): - return True - print(TBufferJSONErrorMessage, file=sys.stderr) - return False + if hasattr(ROOT, "TBufferJSON"): + return True + print(TBufferJSONErrorMessage, file=sys.stderr) + return False + def TWebCanvasAvailable(): - if hasattr(ROOT,"TWebCanvas"): - return True - return False + if hasattr(ROOT, "TWebCanvas"): + return True + return False + def RCanvasAvailable(): - if not hasattr(ROOT,"Experimental"): - return False - if not hasattr(ROOT.Experimental,"RCanvas"): - return False - return True + if not hasattr(ROOT, "Experimental"): + return False + if not hasattr(ROOT.Experimental, "RCanvas"): + return False + return True + -_enableJSVis = True +def _initializeJSVis(): + global _enableJSVis + jupyter_jsroot = ROOT.gEnv.GetValue("Jupyter.JSRoot", "on").lower() + if jupyter_jsroot not in {"on", "off"}: + print(f"Invalid Jupyter.JSRoot value '{jupyter_jsroot}' in .rootrc. Using default 'on'.") + jupyter_jsroot = "on" + _enableJSVis = jupyter_jsroot == "on" + + +_initializeJSVis() _enableJSVisDebug = False + + def enableJSVis(): if not TBufferJSONAvailable(): - return + return global _enableJSVis _enableJSVis = True + def disableJSVis(): global _enableJSVis _enableJSVis = False + def enableJSVisDebug(): if not TBufferJSONAvailable(): - return + return global _enableJSVis global _enableJSVisDebug _enableJSVis = True _enableJSVisDebug = True + def disableJSVisDebug(): global _enableJSVis global _enableJSVisDebug _enableJSVis = False _enableJSVisDebug = False + def _getPlatform(): return sys.platform + def _getLibExtension(thePlatform): - '''Return appropriate file extension for a shared library + """Return appropriate file extension for a shared library >>> _getLibExtension('darwin') '.dylib' >>> _getLibExtension('win32') '.dll' >>> _getLibExtension('OddPlatform') '.so' - ''' - pExtMap = { - 'darwin' : '.dylib', - 'win32' : '.dll' - } - return pExtMap.get(thePlatform, '.so') + """ + pExtMap = {"darwin": ".dylib", "win32": ".dll"} + return pExtMap.get(thePlatform, ".so") + @contextmanager def _setIgnoreLevel(level): @@ -177,81 +195,89 @@ def _setIgnoreLevel(level): yield ROOT.gErrorIgnoreLevel = originalLevel -def commentRemover( text ): - ''' - >>> s="// hello" - >>> commentRemover(s) - '' - >>> s="int /** Test **/ main() {return 0;}" - >>> commentRemover(s) - 'int main() {return 0;}' - ''' - def blotOutNonNewlines( strIn ) : # Return a string containing only the newline chars contained in strIn - return "" + ("\n" * strIn.count('\n')) - def replacer( match ) : - s = match.group(0) - if s.startswith('/'): # Matched string is //...EOL or /*...*/ ==> Blot out all non-newline chars - return blotOutNonNewlines(s) - else: # Matched string is '...' or "..." ==> Keep unchanged - return s +def commentRemover(text): + """ + >>> s="// hello" + >>> commentRemover(s) + '' + >>> s="int /** Test **/ main() {return 0;}" + >>> commentRemover(s) + 'int main() {return 0;}' + """ + + def blotOutNonNewlines(strIn): # Return a string containing only the newline chars contained in strIn + return "" + ("\n" * strIn.count("\n")) + + def replacer(match): + s = match.group(0) + if s.startswith("/"): # Matched string is //...EOL or /*...*/ ==> Blot out all non-newline chars + return blotOutNonNewlines(s) + else: # Matched string is '...' or "..." ==> Keep unchanged + return s - pattern = re.compile(\ - r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', - re.DOTALL | re.MULTILINE) + pattern = re.compile(r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE) - return re.sub(pattern, replacer, text) + return re.sub(pattern, replacer, text) # Here functions are defined to process C++ code def processCppCodeImpl(code): - #code = commentRemover(code) + # code = commentRemover(code) ROOT.gInterpreter.ProcessLine(code) + def processMagicCppCodeImpl(code): err = ROOT.ProcessLineWrapper(code) if err == ROOT.TInterpreter.kProcessing: - ROOT.gInterpreter.ProcessLine('.@') + ROOT.gInterpreter.ProcessLine(".@") ROOT.gInterpreter.ProcessLine('cerr << "Unbalanced braces. This cell was not processed." << endl;') + def declareCppCodeImpl(code): - #code = commentRemover(code) + # code = commentRemover(code) ROOT.gInterpreter.Declare(code) + def processCppCode(code): processCppCodeImpl(code) + def processMagicCppCode(code): processMagicCppCodeImpl(code) + def declareCppCode(code): declareCppCodeImpl(code) -def _checkOutput(command,errMsg=None): + +def _checkOutput(command, errMsg=None): out = "" try: out = check_output(command.split()) - except: + except Exception: if errMsg: - sys.stderr.write("%s (command was %s)\n" %(errMsg,command)) + sys.stderr.write("%s (command was %s)\n" % (errMsg, command)) return out + def _invokeAclicMac(fileName): - '''FIXME! + """FIXME! This function is a workaround. On osx, it is impossible to link against libzmq.so, among the others. The error is known and is "ld: can't link with bundle (MH_BUNDLE) only dylibs (MH_DYLIB)" We cannot at the moment force Aclic to change the linker command in order to exclude these libraries, so we launch a second root session to compile the library, which we then load. - ''' - command = 'root -l -q -b -e gSystem->CompileMacro(\"%s\",\"k\")*0'%fileName - out = _checkOutput(command, "Error ivoking ACLiC") - libNameBase = fileName.replace(".C","_C") + """ + command = 'root -l -q -b -e gSystem->CompileMacro("%s","k")*0' % fileName + out = _checkOutput(command, "Error ivoking ACLiC") # noqa: F841 + libNameBase = fileName.replace(".C", "_C") ROOT.gSystem.Load(libNameBase) + def _codeToFilename(code): - '''Convert code to a unique file name + """Convert code to a unique file name >>> code = "int f(i){return i*i;}" >>> _codeToFilename(code)[0:9] @@ -260,14 +286,15 @@ def _codeToFilename(code): True >>> _codeToFilename(code)[-2:] '.C' - ''' - code_enc = code if type(code) == bytes else code.encode('utf-8') + """ + code_enc = code if type(code) is bytes else code.encode("utf-8") fileNameBase = sha1(code_enc).hexdigest()[0:8] timestamp = datetime.now().strftime("%H%M%S%f") return fileNameBase + "_" + timestamp + ".C" + def _dumpToUniqueFile(code): - '''Dump code to file whose name is unique + """Dump code to file whose name is unique >>> code = "int f(i){return i*i;}" >>> _dumpToUniqueFile(code)[0:9] @@ -276,76 +303,84 @@ def _dumpToUniqueFile(code): True >>> _dumpToUniqueFile(code)[-2:] '.C' - ''' + """ fileName = _codeToFilename(code) - with open (fileName,'w') as ofile: - code_dec = code if type(code) != bytes else code.decode('utf-8') - ofile.write(code_dec) + with open(fileName, "w") as ofile: + code_dec = code if type(code) is not bytes else code.decode("utf-8") + ofile.write(code_dec) return fileName + def isPlatformApple(): - return _getPlatform() == 'darwin'; + return _getPlatform() == "darwin" + def invokeAclic(cell): fileName = _dumpToUniqueFile(cell) if isPlatformApple(): _invokeAclicMac(fileName) else: - processCppCode(".L %s+" %fileName) + processCppCode(".L %s+" % fileName) + def produceCanvasJson(canvas): + if canvas.IsUpdated() and not canvas.IsDrawn(): + canvas.Draw() - if canvas.IsUpdated() and not canvas.IsDrawn(): - canvas.Draw() + if TWebCanvasAvailable(): + return ROOT.TWebCanvas.CreateCanvasJSON(canvas, 23, True) - if TWebCanvasAvailable(): - return ROOT.TWebCanvas.CreateCanvasJSON(canvas, 23, True) + # Add extra primitives to canvas with custom colors, palette, gStyle - # Add extra primitives to canvas with custom colors, palette, gStyle + prim = canvas.GetListOfPrimitives() - prim = canvas.GetListOfPrimitives() + style = ROOT.gStyle + colors = ROOT.gROOT.GetListOfColors() + palette = None - style = ROOT.gStyle - colors = ROOT.gROOT.GetListOfColors() - palette = None + # always provide gStyle object + if prim.FindObject(style): + style = None + else: + prim.Add(style) - # always provide gStyle object - if prim.FindObject(style): - style = None - else: - prim.Add(style) + cnt = 0 + for n in range(colors.GetLast() + 1): + if colors.At(n): + cnt = cnt + 1 - cnt = 0 - for n in range(colors.GetLast()+1): - if colors.At(n): cnt = cnt+1 + # add all colors if there are more than 598 colors defined + if cnt < 599 or prim.FindObject(colors): + colors = None + else: + prim.Add(colors) - # add all colors if there are more than 598 colors defined - if cnt < 599 or prim.FindObject(colors): - colors = None - else: - prim.Add(colors) + if colors: + pal = ROOT.TColor.GetPalette() + palette = ROOT.TObjArray() + palette.SetName("CurrentColorPalette") + for i in range(pal.GetSize()): + palette.Add(colors.At(pal[i])) + prim.Add(palette) - if colors: - pal = ROOT.TColor.GetPalette() - palette = ROOT.TObjArray() - palette.SetName("CurrentColorPalette") - for i in range(pal.GetSize()): - palette.Add(colors.At(pal[i])) - prim.Add(palette) + ROOT.TColor.DefinedColors() - ROOT.TColor.DefinedColors() + canvas_json = ROOT.TBufferJSON.ConvertToJSON(canvas, 23) - canvas_json = ROOT.TBufferJSON.ConvertToJSON(canvas, 23) + # Cleanup primitives after conversion + if style is not None: + prim.Remove(style) + if colors is not None: + prim.Remove(colors) + if palette is not None: + prim.Remove(palette) - # Cleanup primitives after conversion - if style is not None: prim.Remove(style) - if colors is not None: prim.Remove(colors) - if palette is not None: prim.Remove(palette) + return canvas_json - return canvas_json transformers = [] + class StreamCapture(object): def __init__(self, ip=get_ipython()): # For the registration @@ -363,17 +398,18 @@ def __init__(self, ip=get_ipython()): self.isFirstPreExecute = True self.isFirstPostExecute = True - def syncCapture(self, defout = ''): + def syncCapture(self, defout=""): self.outString = defout self.errString = defout - waitTimes = [.01, .01, .02, .04, .06, .08, .1] + waitTimes = [0.01, 0.01, 0.02, 0.04, 0.06, 0.08, 0.1] lenWaitTimes = 7 iterIndex = 0 while self.flag: self.ioHandler.Poll() - if not self.flag: return - waitTime = .1 if iterIndex >= lenWaitTimes else waitTimes[iterIndex] + if not self.flag: + return + waitTime = 0.1 if iterIndex >= lenWaitTimes else waitTimes[iterIndex] time.sleep(waitTime) def pre_execute(self): @@ -384,7 +420,7 @@ def pre_execute(self): self.flag = True self.ioHandler.Clear() self.ioHandler.InitCapture() - self.asyncCapturer.AsyncRun('') + self.asyncCapturer.AsyncRun("") def post_execute(self): if self.isFirstPostExecute: @@ -405,65 +441,80 @@ def post_execute(self): else: for t in transformers: (out, err, otype) = t(out, err) - if otype == 'html': + if otype == "html": display.display(display.HTML(out)) display.display(display.HTML(err)) return 0 def register(self): - self.shell.events.register('pre_execute', self.pre_execute) - self.shell.events.register('post_execute', self.post_execute) + self.shell.events.register("pre_execute", self.pre_execute) + self.shell.events.register("post_execute", self.post_execute) def __del__(self): self.poller.Stop() + def GetCanvasDrawers(): lOfC = ROOT.gROOT.GetListOfCanvases() return [NotebookDrawer(can) for can in lOfC if can.IsDrawn() or can.IsUpdated()] + def GetRCanvasDrawers(): - if not RCanvasAvailable(): return [] + if not RCanvasAvailable(): + return [] lOfC = ROOT.Experimental.RCanvas.GetCanvases() return [NotebookDrawer(can.__smartptr__().get()) for can in lOfC if can.IsShown() or can.IsUpdated()] + def GetGeometryDrawer(): - if not hasattr(ROOT,'gGeoManager'): return - if not ROOT.gGeoManager: return - if not ROOT.gGeoManager.GetUserPaintVolume(): return + if not hasattr(ROOT, "gGeoManager"): + return + if not ROOT.gGeoManager: + return + if not ROOT.gGeoManager.GetUserPaintVolume(): + return vol = ROOT.gGeoManager.GetTopVolume() if vol: return NotebookDrawer(vol) + def GetDrawers(): drawers = GetCanvasDrawers() + GetRCanvasDrawers() geometryDrawer = GetGeometryDrawer() - if geometryDrawer: drawers.append(geometryDrawer) + if geometryDrawer: + drawers.append(geometryDrawer) return drawers + def DrawGeometry(): drawer = GetGeometryDrawer() if drawer: drawer.Draw() + def DrawCanvases(): drawers = GetCanvasDrawers() for drawer in drawers: drawer.Draw() + def DrawRCanvases(): rdrawers = GetRCanvasDrawers() for drawer in rdrawers: drawer.Draw() + def NotebookDraw(): DrawGeometry() DrawCanvases() DrawRCanvases() + class CaptureDrawnPrimitives(object): - ''' + """ Capture the canvas which is drawn to display it. - ''' + """ + def __init__(self, ip=get_ipython()): self.shell = ip @@ -471,20 +522,21 @@ def _post_execute(self): NotebookDraw() def register(self): - self.shell.events.register('post_execute', self._post_execute) + self.shell.events.register("post_execute", self._post_execute) + class NotebookDrawer(object): - ''' + """ Capture the canvas which is drawn and decide if it should be displayed using jsROOT. - ''' + """ def __init__(self, theObject): self.drawableObject = theObject self.isRCanvas = False self.isCanvas = False self.drawableId = str(ROOT.AddressOf(theObject)[0]) - if hasattr(self.drawableObject,"ResolveSharedPtrs"): + if hasattr(self.drawableObject, "ResolveSharedPtrs"): self.isRCanvas = True else: self.isCanvas = self.drawableObject.ClassName() == "TCanvas" @@ -500,29 +552,29 @@ def __del__(self): ROOT.gGeoManager.SetUserPaintVolume(None) def _getListOfPrimitivesNamesAndTypes(self): - """ - Get the list of primitives in the pad, recursively descending into - histograms and graphs looking for fitted functions. - """ - primitives = self.drawableObject.GetListOfPrimitives() - primitivesNames = map(lambda p: p.ClassName(), primitives) - return sorted(primitivesNames) + """ + Get the list of primitives in the pad, recursively descending into + histograms and graphs looking for fitted functions. + """ + primitives = self.drawableObject.GetListOfPrimitives() + primitivesNames = map(lambda p: p.ClassName(), primitives) + return sorted(primitivesNames) def _getUniqueDivId(self): - ''' + """ Every DIV containing a JavaScript snippet must be unique in the notebook. This method provides a unique identifier. With the introduction of JupyterLab, multiple Notebooks can exist simultaneously on the same HTML page. In order to ensure a unique identifier with the UID throughout all open Notebooks the UID is generated as a timestamp. - ''' - return 'root_plot_' + str(int(round(time.time() * 1000))) + """ + return "root_plot_" + str(int(round(time.time() * 1000))) def _canJsDisplay(self): # returns true if js-based drawing should be used if not TBufferJSONAvailable(): - return False + return False # RCanvas clways displayed with jsroot if self.isRCanvas: return True @@ -537,8 +589,12 @@ def _canJsDisplay(self): primitivesTypesNames = self._getListOfPrimitivesNamesAndTypes() for unsupportedPattern in _jsNotDrawableClassesPatterns: for primitiveTypeName in primitivesTypesNames: - if fnmatch.fnmatch(primitiveTypeName,unsupportedPattern): - print("The canvas contains an object of a type jsROOT cannot currently handle (%s). Falling back to a static png." %primitiveTypeName, file=sys.stderr) + if fnmatch.fnmatch(primitiveTypeName, unsupportedPattern): + print( + "The canvas contains an object of a type jsROOT cannot currently handle (%s). Falling back to a static png." + % primitiveTypeName, + file=sys.stderr, + ) return False return True @@ -556,20 +612,22 @@ def _getJsCode(self): options = "all" if self.isCanvas: - if self.drawableObject.GetWindowWidth() > 0: width = self.drawableObject.GetWindowWidth() - if self.drawableObject.GetWindowHeight() > 0: height = self.drawableObject.GetWindowHeight() + if self.drawableObject.GetWindowWidth() > 0: + width = self.drawableObject.GetWindowWidth() + if self.drawableObject.GetWindowHeight() > 0: + height = self.drawableObject.GetWindowHeight() options = "" if self.isRCanvas: - if self.drawableObject.GetWidth() > 0: width = self.drawableObject.GetWidth() - if self.drawableObject.GetHeight() > 0: height = self.drawableObject.GetHeight() + if self.drawableObject.GetWidth() > 0: + width = self.drawableObject.GetWidth() + if self.drawableObject.GetHeight() > 0: + height = self.drawableObject.GetHeight() options = "" - thisJsCode = _jsCode.format(jsCanvasWidth = width, - jsCanvasHeight = height, - jsonContent = json, - jsDrawOptions = options, - jsDivId = divId) + thisJsCode = _jsCode.format( + jsCanvasWidth=width, jsCanvasHeight=height, jsonContent=json, jsDrawOptions=options, jsDivId=divId + ) return thisJsCode def _getJsDiv(self): @@ -581,7 +639,7 @@ def _getDrawId(self): if self.isRCanvas: return self.drawableObject.GetUID() # all other objects do not support update and can be ignored - return '' + return "" def _getUpdated(self): if self.isCanvas: @@ -607,7 +665,7 @@ def _getPngImage(self): ofile = tempfile.NamedTemporaryFile(suffix=".png", delete=False) with _setIgnoreLevel(ROOT.kError): self.drawableObject.SaveAs(ofile.name) - img = display.Image(filename=ofile.name, format='png', embed=True) + img = display.Image(filename=ofile.name, format="png", embed=True) ofile.close() os.unlink(ofile.name) return img @@ -625,37 +683,40 @@ def _pngDisplay(self): display.display(img) def _display(self): - if _enableJSVisDebug: - self._pngDisplay() - self._jsDisplay() - else: - if self._canJsDisplay(): - self._jsDisplay() - else: + if _enableJSVisDebug: self._pngDisplay() + self._jsDisplay() + else: + if self._canJsDisplay(): + self._jsDisplay() + else: + self._pngDisplay() def GetDrawableObjects(self): if _enableJSVisDebug: - return [self._getJsDiv(),self._getPngImage()] + return [self._getJsDiv(), self._getPngImage()] if self._canJsDisplay(): - return [self._getJsDiv()] + return [self._getJsDiv()] else: - return [self._getPngImage()] + return [self._getPngImage()] def Draw(self): self._display() return 0 + def setStyle(): - style=ROOT.gStyle + style = ROOT.gStyle style.SetFuncWidth(2) + captures = [] + def loadMagicsAndCapturers(): global captures - extNames = ["JupyROOT.magics." + name for name in ["cppmagic","jsrootmagic"]] + extNames = ["JupyROOT.magics." + name for name in ["cppmagic", "jsrootmagic"]] ip = get_ipython() extMgr = ExtensionManager(ip) for extName in extNames: @@ -663,7 +724,9 @@ def loadMagicsAndCapturers(): captures.append(StreamCapture()) captures.append(CaptureDrawnPrimitives()) - for capture in captures: capture.register() + for capture in captures: + capture.register() + def declareProcessLineWrapper(): ROOT.gInterpreter.Declare(""" @@ -674,20 +737,23 @@ def declareProcessLineWrapper(): } """) + def enhanceROOTModule(): ROOT.enableJSVis = enableJSVis ROOT.disableJSVis = disableJSVis ROOT.enableJSVisDebug = enableJSVisDebug ROOT.disableJSVisDebug = disableJSVisDebug + def enableCppHighlighting(): ipDispJs = display.display_javascript # Define highlight mode for %%cpp magic - ipDispJs(_jsMagicHighlight.format(cppMIME = cppMIME), raw=True) + ipDispJs(_jsMagicHighlight.format(cppMIME=cppMIME), raw=True) + def iPythonize(): setStyle() loadMagicsAndCapturers() declareProcessLineWrapper() - #enableCppHighlighting() + # enableCppHighlighting() enhanceROOTModule() diff --git a/config/rootrc.in b/config/rootrc.in index f1bfd4c775200..c437b829f68f2 100644 --- a/config/rootrc.in +++ b/config/rootrc.in @@ -242,6 +242,9 @@ GeomPainter.Name: @root_geompainter_type@ # THttpServer specific settings # location of JavaScript ROOT sources #HttpServ.JSRootPath: @jsrootdir@ +# Default setting for enabling or disabling JSROOT. +# This can be toggled in the notebook using the `%jsroot on/off` command. +Jupyter.JSRoot: on # WebGui specific settings (defaults are shown) # fixed http port number for server, 0 - not fixed, -1 - disabled completely