diff --git a/bindings/pyroot/pythonizations/python/CMakeLists.txt b/bindings/pyroot/pythonizations/python/CMakeLists.txt index 6dcbf3e7584e1..0bddff3c3216b 100644 --- a/bindings/pyroot/pythonizations/python/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/python/CMakeLists.txt @@ -110,9 +110,11 @@ set(py_sources ROOT/_jupyroot/kernel/magics/__init__.py ROOT/_jupyroot/kernel/magics/cppmagic.py ROOT/_jupyroot/kernel/magics/jsrootmagic.py + ROOT/_jupyroot/kernel/magics/rootbrowsemagic.py ROOT/_jupyroot/magics/__init__.py ROOT/_jupyroot/magics/cppmagic.py ROOT/_jupyroot/magics/jsrootmagic.py + ROOT/_jupyroot/magics/rootbrowsemagic.py ROOT/_numbadeclare.py ROOT/_pythonization/__init__.py ROOT/_pythonization/_cppinstance.py diff --git a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/helpers/handlers.py b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/helpers/handlers.py index 1401695ca8c23..98f40c7386eeb 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/helpers/handlers.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/helpers/handlers.py @@ -169,8 +169,7 @@ def __init__(self, poller): def display_drawables(displayFunction): drawers = helpers.utils.GetDrawers() for drawer in drawers: - for dobj in drawer.GetDrawableObjects(): - displayFunction(dobj) + drawer.Draw(displayFunction) class JupyROOTDisplayer(Runner): diff --git a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/helpers/utils.py b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/helpers/utils.py index 72918f4edd1e7..66587a23fb1e5 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/helpers/utils.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/helpers/utils.py @@ -21,6 +21,7 @@ import sys import tempfile import time +import ctypes from contextlib import contextmanager from datetime import datetime from hashlib import sha1 @@ -37,9 +38,13 @@ cppMIME = "text/x-c++src" +_enableJSVis = True + # Keep display handle for canvases to be able update them _canvasHandles = {} +_visualObjects = [] + _jsMagicHighlight = """ Jupyter.CodeCell.options_default.highlight_modes['magic_{cppMIME}'] = {{'reg':[/^%%cpp/]}}; console.log("JupyROOT - %%cpp magic configured"); @@ -50,25 +55,54 @@ _jsCanvasWidth = 800 _jsCanvasHeight = 600 -_jsCode = """ +_jsFixedSizeDiv = """
+""" + +_jsFullWidthDiv = """ +
+
+
+
+""" + +_jsDrawJsonCode = """ +Core.unzipJSON({jsonLength},'{jsonZip}').then(json => {{ + const obj = Core.parse(json); + Core.draw('{jsDivId}', obj, '{jsDrawOptions}'); +}}); +""" + +_jsBrowseUrlCode = """ +Core.buildGUI('{jsDivId}','notebook').then(h => h.openRootFile('{fileUrl}')); +""" + + +_jsBrowseFileCode = """ +const binaryString = atob('{fileBase64}'); +const bytes = new Uint8Array(binaryString.length); +for (let i = 0; i < binaryString.length; i++) + bytes[i] = binaryString.charCodeAt(i); +Core.buildGUI('{jsDivId}','notebook').then(h => h.openRootFile(bytes.buffer)); +""" + +_jsCode = """ +{jsDivHtml} + """ + +_jsFileCode = """ +
+
+
+
+ +""" + + + 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)?" @@ -113,7 +188,7 @@ def RCanvasAvailable(): return True -def _initializeJSVis(): +def initializeJSVis(): global _enableJSVis jupyter_jsroot = ROOT.gEnv.GetValue("Jupyter.JSRoot", "on").lower() if jupyter_jsroot not in {"on", "off"}: @@ -122,42 +197,49 @@ def _initializeJSVis(): _enableJSVis = jupyter_jsroot == "on" -_initializeJSVis() -_enableJSVisDebug = False - - -def enableJSVis(): - if not TBufferJSONAvailable(): - return +def enableJSVis(flag=True): global _enableJSVis - _enableJSVis = True + _enableJSVis = flag and TBufferJSONAvailable() -def disableJSVis(): - global _enableJSVis - _enableJSVis = False - +def addVisualObject(object, kind="none", option=""): + global _visualObjects + if kind == "url": + _visualObjects.append(NotebookDrawerUrl(object)) + elif kind == "tfile": + _visualObjects.append(NotebookDrawerFile(object, option)) + else: + _visualObjects.append(NotebookDrawerJson(object)) -def enableJSVisDebug(): - if not TBufferJSONAvailable(): - return - global _enableJSVis - global _enableJSVisDebug - _enableJSVis = True - _enableJSVisDebug = True +def browseRootFile(fname, force=False): + if not fname: + return False -def disableJSVisDebug(): - global _enableJSVis - global _enableJSVisDebug - _enableJSVis = False - _enableJSVisDebug = False + if not force and (fname.startswith("https://") or fname.startswith("http://")): + addVisualObject(fname, "url") + return True + addVisualObject(fname, "tfile", force) + return False def _getPlatform(): return sys.platform +def _getUniqueDivId(): + """ + 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))) + + + def _getLibExtension(thePlatform): """Return appropriate file extension for a shared library >>> _getLibExtension('darwin') @@ -306,61 +388,6 @@ def invokeAclic(cell): processCppCode(".L %s+" % fileName) -def produceCanvasJson(canvas): - if canvas.IsUpdated() and not canvas.IsDrawn(): - canvas.Draw() - - if TWebCanvasAvailable(): - return ROOT.TWebCanvas.CreateCanvasJSON(canvas, 23, True) - - # Add extra primitives to canvas with custom colors, palette, gStyle - - prim = canvas.GetListOfPrimitives() - - style = ROOT.gStyle - colors = ROOT.gROOT.GetListOfColors() - palette = None - - # 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 - - # 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) - - ROOT.TColor.DefinedColors() - - 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) - - return canvas_json - - transformers = [] @@ -439,61 +466,43 @@ def __del__(self): def GetCanvasDrawers(): lOfC = ROOT.gROOT.GetListOfCanvases() - return [NotebookDrawer(can) for can in lOfC if can.IsDrawn() or can.IsUpdated()] + return [NotebookDrawerTCanvas(can) for can in lOfC if can.IsDrawn() or can.IsUpdated()] def GetRCanvasDrawers(): if not RCanvasAvailable(): return [] lOfC = ROOT.Experimental.RCanvas.GetCanvases() - return [NotebookDrawer(can.__smartptr__().get()) for can in lOfC if can.IsShown() or can.IsUpdated()] + return [NotebookDrawerRCanvas(can.__smartptr__().get()) for can in lOfC if can.IsShown() or can.IsUpdated()] +def GetVisualDrawers(): + global _visualObjects + res = [obj for obj in _visualObjects] + _visualObjects.clear() + return res -def GetGeometryDrawer(): +def GetGeometryDrawers(): if not hasattr(ROOT, "gGeoManager"): - return + return [] if not ROOT.gGeoManager: - return - if not ROOT.gGeoManager.GetUserPaintVolume(): - return - vol = ROOT.gGeoManager.GetTopVolume() - if vol: - return NotebookDrawer(vol) + return [] + vol = ROOT.gGeoManager.GetUserPaintVolume() + if not vol: + return [] + return [NotebookDrawerGeometry(vol)] def GetDrawers(): - drawers = GetCanvasDrawers() + GetRCanvasDrawers() - geometryDrawer = GetGeometryDrawer() - 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() + return GetCanvasDrawers() + GetRCanvasDrawers() + GetVisualDrawers() + GetGeometryDrawers() def NotebookDraw(): - DrawGeometry() - DrawCanvases() - DrawRCanvases() + drawers = GetDrawers() + for drawer in drawers: + drawer.Draw(display.display) -class CaptureDrawnPrimitives(object): +class CaptureDrawnPrimitives: """ Capture the canvas which is drawn to display it. """ @@ -508,68 +517,266 @@ def register(self): self.shell.events.register("post_execute", self._post_execute) -class NotebookDrawer(object): + +class NotebookDrawerUrl: + """ + Special drawer for file URL - JSROOT uses url to load file directly, without root + """ + + def __init__(self, theUrl): + self.drawUrl = theUrl + + def _getUrlJsCode(self): + + id = _getUniqueDivId() + + drawHtml = _jsFullWidthDiv.format( + jsDivId=id, + jsCanvasHeight=_jsCanvasHeight + ) + + browseUrlCode = _jsBrowseUrlCode.format( + jsDivId=id, + fileUrl=self.drawUrl + ) + + thisJsCode = _jsCode.format( + jsDivId=id, + jsDivHtml=drawHtml, + jsDrawCode=browseUrlCode + ) + + return thisJsCode + + def Draw(self, displayFunction): + code = self._getUrlJsCode() + displayFunction(display.HTML(code)) + + + +class NotebookDrawerFile: + """ + Special drawer for TFile - shows files hierarchy with possibility to draw objects + """ + + def __init__(self, theFileName, theForce=False): + self.drawFileName = theFileName + self.drawForce = theForce + + def _getFileJsCode(self): + base64 = "" + + with ROOT.TDirectory.TContext(), ROOT.TFile.Open(self.drawFileName) as f: + if not f: + return f"Fail to open file {self.drawFileName}" + + if f.GetVersion() < 30000: + return f"File version {f.GetVersion()} is too old and not supported by JSROOT" + + sz = f.GetSize() + if sz > 300000000: + return f"File size {sz} is too large for JSROOT display." + if sz > 10000000 and not self.drawForce: + return f"File size {sz} is large for JSROOT display. Use '-f' flag like '%rootbrowse {self.drawFileName} -f' to show file nevertheless" + + # create plain buffer and get pointer on it + u_buffer = (ctypes.c_ubyte * sz)(*range(sz)) + addrc = ctypes.cast(ctypes.pointer(u_buffer), ctypes.c_char_p) + + if f.ReadBuffer(addrc, 0, sz): + return f"Fail to read file {self.drawFileName} buffer of size {sz}" + + base64 = ROOT.TBase64.Encode(addrc, sz) + + id = _getUniqueDivId() + + drawHtml = _jsFullWidthDiv.format( + jsDivId=id, + jsCanvasHeight=_jsCanvasHeight + ) + + browseFileCode = _jsBrowseFileCode.format( + jsDivId=id, + fileBase64=base64 + ) + + thisJsCode = _jsCode.format( + jsDivId=id, + jsDivHtml=drawHtml, + jsDrawCode=browseFileCode + ) + + return thisJsCode + + def Draw(self, displayFunction): + code = self._getFileJsCode() + displayFunction(display.HTML(code)) + + + +class NotebookDrawerJson: """ - Capture the canvas which is drawn and decide if it should be displayed using - jsROOT. + Generic class to create JSROOT drawing for the arbitrary object based on json conversion. """ 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"): - self.isRCanvas = True + self.drawObject = theObject + + def _canJsDisplay(self): + return TBufferJSONAvailable() + + def _canPngDisplay(self): + return True + + def _getWidth(self): + return _jsCanvasWidth + + def _getHeight(self): + return _jsCanvasHeight + + def _getJson(self): + return ROOT.TBufferJSON.ConvertToJSON(self.drawObject, 23).Data() + + def _getJsOptions(self): + return "" + + def _getJsCode(self): + width = self._getWidth() + height = self._getHeight() + json = self._getJson() + options = self._getJsOptions() + + if not json: + return f"Class {self.drawObject.ClassName()} not supported yet" + + zip = ROOT.TBufferJSON.zipJSON(json) + + id = _getUniqueDivId() + + drawHtml = _jsFixedSizeDiv.format( + jsDivId=id, + jsCanvasWidth=width, + jsCanvasHeight=height + ) + + drawJsonCode = _jsDrawJsonCode.format( + jsDivId=id, + jsonLength=len(json), + jsonZip=zip, + jsDrawOptions=options + ) + + thisJsCode = _jsCode.format( + jsDivId=id, + jsDivHtml=drawHtml, + jsDrawCode=drawJsonCode + ) + return thisJsCode + + def _getCanvas(self): + gPadSave = ROOT.gPad + c1 = ROOT.TCanvas("__tmp_draw_image_canvas__", "", self._getWidth(), self._getHeight()) + c1.Add(self.drawObject) + ROOT.gPad = gPadSave + return c1 + + def _getPngImage(self): + ofile = tempfile.NamedTemporaryFile(suffix=".png", delete=False) + canv = self._getCanvas() + with _setIgnoreLevel(ROOT.kError): + canv.SaveAs(ofile.name) + img = display.Image(filename=ofile.name, format="png", embed=True) + ofile.close() + os.unlink(ofile.name) + return img + + + def Draw(self, displayFunction): + global _enableJSVis + if _enableJSVis and self._canJsDisplay(): + code = self._getJsCode() + displayFunction(display.HTML(code)) + elif self._canPngDisplay(): + displayFunction(self._getPngImage()) else: - self.isCanvas = self.drawableObject.ClassName() == "TCanvas" + displayFunction(display.HTML(f"Neither JSROOT nor plain drawing of {self.drawObject.ClassName()} class is not implemented")) + + + +class NotebookDrawerGeometry(NotebookDrawerJson): + """ + Drawer for geometry - png output is not supported. + """ def __del__(self): - if self.isRCanvas: - self.drawableObject.ClearShown() - self.drawableObject.ClearUpdated() - elif self.isCanvas: - self.drawableObject.ResetDrawn() - self.drawableObject.ResetUpdated() + self.drawObject.GetGeoManager().SetUserPaintVolume(ROOT.nullptr) + + def _getJsOptions(self): + return "all" + + def _canPngDisplay(self): + return False + + + +class NotebookDrawerCanvBase(NotebookDrawerJson): + """ + Base class for TCanvas/RCanvas drawing. + Implements a specific Draw function where canvas updating is handled + """ + + def _getCanvasId(self): + return "" + + def _getUpdated(self): + return False + + def _getCanvas(self): + return self.drawObject + + def Draw(self, displayFunction): + global _enableJSVis, _canvasHandles + code = "" + if _enableJSVis and self._canJsDisplay(): + code = display.HTML(self._getJsCode()) + elif self._canPngDisplay(): + code = self._getPngImage() + else: + code = display.HTML(f"Neither JSROOT nor plain drawing of {self.drawObject.ClassName()} is implemented") + + name = self._getCanvasId() + updated = self._getUpdated() + if updated and name and (name in _canvasHandles): + _canvasHandles[name].update(code) + elif name: + _canvasHandles[name] = displayFunction(code, display_id=True) else: - 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) - - 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))) + displayFunction(code) + + + +class NotebookDrawerTCanvas(NotebookDrawerCanvBase): + """ + Drawer of TCanvas. + """ + + def __del__(self): + self.drawObject.ResetDrawn() + self.drawObject.ResetUpdated() + + def _getCanvasId(self): + return self.drawObject.GetName() + str(ROOT.AddressOf(self.drawObject)[0]) + + def _getUpdated(self): + return self.drawObject.IsUpdated() def _canJsDisplay(self): - # returns true if js-based drawing should be used if not TBufferJSONAvailable(): return False - # RCanvas clways displayed with jsroot - if self.isRCanvas: - return True - # check if jsroot was disabled - if not _enableJSVis: - return False - # geometry can be drawn, TWebCanvas also can be used - if not self.isCanvas or TWebCanvasAvailable(): + if TWebCanvasAvailable(): return True - - # to be optimised - primitivesTypesNames = self._getListOfPrimitivesNamesAndTypes() + primitives = self.drawObject.GetListOfPrimitives() + primitivesTypesNames = sorted(map(lambda p: p.ClassName(), primitives)) for unsupportedPattern in _jsNotDrawableClassesPatterns: for primitiveTypeName in primitivesTypesNames: if fnmatch.fnmatch(primitiveTypeName, unsupportedPattern): @@ -581,118 +788,100 @@ def _canJsDisplay(self): return False return True - def _getJsCode(self): - # produce JSON for the canvas - if self.isRCanvas: - json = self.drawableObject.CreateJSON() - else: - json = produceCanvasJson(self.drawableObject).Data() + def _getWidth(self): + if self.drawObject.GetWindowWidth() > 0: + return self.drawObject.GetWindowWidth() + return _jsCanvasWidth - divId = self._getUniqueDivId() + def _getHeight(self): + if self.drawObject.GetWindowHeight() > 0: + return self.drawObject.GetWindowHeight() + return _jsCanvasHeight - width = _jsCanvasWidth - height = _jsCanvasHeight - jsonzip = ROOT.TBufferJSON.zipJSON(json) - options = "all" + def _getJson(self): + if self.drawObject.IsUpdated() and not self.drawObject.IsDrawn(): + self.drawObject.Draw() - if self.isCanvas: - if self.drawableObject.GetWindowWidth() > 0: - width = self.drawableObject.GetWindowWidth() - if self.drawableObject.GetWindowHeight() > 0: - height = self.drawableObject.GetWindowHeight() - options = "" + if TWebCanvasAvailable(): + return ROOT.TWebCanvas.CreateCanvasJSON(self.drawObject, 23, True).Data() - if self.isRCanvas: - if self.drawableObject.GetWidth() > 0: - width = self.drawableObject.GetWidth() - if self.drawableObject.GetHeight() > 0: - height = self.drawableObject.GetHeight() - options = "" + # Add extra primitives to canvas with custom colors, palette, gStyle - thisJsCode = _jsCode.format( - jsCanvasWidth=width, - jsCanvasHeight=height, - jsonLength=len(json), - jsonZip=jsonzip, - jsDrawOptions=options, - jsDivId=divId, - ) - return thisJsCode + prim = self.drawObject.GetListOfPrimitives() - def _getJsDiv(self): - return display.HTML(self._getJsCode()) + style = ROOT.gStyle + colors = ROOT.gROOT.GetListOfColors() + palette = None - def _getDrawId(self): - if self.isCanvas: - return self.drawableObject.GetName() + self.drawableId - if self.isRCanvas: - return self.drawableObject.GetUID() - # all other objects do not support update and can be ignored - return "" + # always provide gStyle object + if prim.FindObject(style): + style = None + else: + prim.Add(style) - def _getUpdated(self): - if self.isCanvas: - return self.drawableObject.IsUpdated() - if self.isRCanvas: - return self.drawableObject.IsUpdated() - return False + cnt = 0 + for n in range(colors.GetLast() + 1): + if colors.At(n): + cnt = cnt + 1 - def _jsDisplay(self): - global _canvasHandles - name = self._getDrawId() - updated = self._getUpdated() - jsdiv = self._getJsDiv() - if name and (name in _canvasHandles) and updated: - _canvasHandles[name].update(jsdiv) - elif name: - _canvasHandles[name] = display.display(jsdiv, display_id=True) + # add all colors if there are more than 598 colors defined + if cnt < 599 or prim.FindObject(colors): + colors = None else: - display.display(jsdiv) - return 0 + prim.Add(colors) - 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) - ofile.close() - os.unlink(ofile.name) - return img + 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) - def _pngDisplay(self): - global _canvasHandles - name = self._getDrawId() - updated = self._getUpdated() - img = self._getPngImage() - if updated and name and (name in _canvasHandles): - _canvasHandles[name].update(img) - elif name: - _canvasHandles[name] = display.display(img, display_id=True) - else: - display.display(img) + # reset global flag to avoid automatic colors storage by canvas + ROOT.TColor.DefinedColors() - def _display(self): - if _enableJSVisDebug: - self._pngDisplay() - self._jsDisplay() - else: - if self._canJsDisplay(): - self._jsDisplay() - else: - self._pngDisplay() + canvas_json = ROOT.TBufferJSON.ConvertToJSON(self.drawObject, 23) - def GetDrawableObjects(self): - if _enableJSVisDebug: - return [self._getJsDiv(), self._getPngImage()] + # 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) - if self._canJsDisplay(): - return [self._getJsDiv()] - else: - return [self._getPngImage()] + return canvas_json.Data() + + +class NotebookDrawerRCanvas(NotebookDrawerCanvBase): + """ + Drawer of RCanvas. + """ + + def __del__(self): + self.drawObject.ClearShown() + self.drawObject.ClearUpdated() + + def _getCanvasId(self): + return self.drawObject.GetUID() + + def _getUpdated(self): + return self.drawObject.IsUpdated() + + def _getWidth(self): + if self.drawObject.GetWidth() > 0: + return self.drawObject.GetWidth() + return _jsCanvasWidth + + def _getHeight(self): + if self.drawObject.GetHeight() > 0: + return self.drawObject.GetHeight() + return _jsCanvasHeight + + def _getJson(self): + return self.drawObject.CreateJSON() - def Draw(self): - self._display() - return 0 def setStyle(): @@ -705,7 +894,7 @@ def setStyle(): def loadMagicsAndCapturers(): global captures - extNames = ["ROOT._jupyroot.magics." + name for name in ["cppmagic", "jsrootmagic"]] + extNames = ["ROOT._jupyroot.magics." + name for name in ["cppmagic", "jsrootmagic", "rootbrowsemagic"]] ip = get_ipython() extMgr = ExtensionManager(ip) for extName in extNames: @@ -729,9 +918,6 @@ def declareProcessLineWrapper(): def enhanceROOTModule(): ROOT.enableJSVis = enableJSVis - ROOT.disableJSVis = disableJSVis - ROOT.enableJSVisDebug = enableJSVisDebug - ROOT.disableJSVisDebug = disableJSVisDebug def enableCppHighlighting(): @@ -742,6 +928,7 @@ def enableCppHighlighting(): def iPythonize(): setStyle() + initializeJSVis() loadMagicsAndCapturers() declareProcessLineWrapper() # enableCppHighlighting() diff --git a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/kernel/magics/jsrootmagic.py b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/kernel/magics/jsrootmagic.py index d4e172f979bfa..d2db11722b921 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/kernel/magics/jsrootmagic.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/kernel/magics/jsrootmagic.py @@ -17,9 +17,7 @@ from ROOT._jupyroot.helpers.utils import ( TBufferJSONAvailable, TBufferJSONErrorMessage, - disableJSVis, - enableJSVis, - enableJSVisDebug, + enableJSVis ) @@ -32,12 +30,9 @@ def line_jsroot(self, args): """Change the visualisation of plots from images to interactive JavaScript objects.""" if args == "on" or args == "": self.printErrorIfNeeded() - enableJSVis() + enableJSVis(True) elif args == "off": - disableJSVis() - elif args == "debug": - self.printErrorIfNeeded() - enableJSVisDebug() + enableJSVis(False) def printErrorIfNeeded(self): if not TBufferJSONAvailable(): diff --git a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/kernel/magics/rootbrowsemagic.py b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/kernel/magics/rootbrowsemagic.py new file mode 100644 index 0000000000000..37e040bab1b84 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/kernel/magics/rootbrowsemagic.py @@ -0,0 +1,40 @@ +# -*- coding:utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2016, ROOT Team. +# Authors: Danilo Piparo CERN +# ----------------------------------------------------------------------------- + +################################################################################ +# Copyright (C) 1995-2020, 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 metakernel import Magic, option + +from ROOT._jupyroot.helpers.utils import browseRootFile + + +class RootBrowseMagics(Magic): + def __init__(self, kernel): + super(RootBrowseMagics, self).__init__(kernel) + + @option("arg", default="", help="Show JSROOT browser with file content") + @option( + "-f", "--force", + action="store_true", + default=False, + help="Force opening of large files" + ) + def line_rootbrowse(self, arg, force): + """Open file and start browser.""" + if not browseRootFile(arg, force): + self.kernel.Error(f"Not able to open file {arg}") + else: + self.kernel.do_display() + + +def register_magics(kernel): + kernel.register_magics(RootBrowseMagics) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/kernel/rootkernel.py b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/kernel/rootkernel.py index 274fe27042def..6b3b9deee9280 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/kernel/rootkernel.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/kernel/rootkernel.py @@ -80,6 +80,9 @@ def print_output(self, handler): for streamDict in filter(lambda d: d is not None, streamDicts): self.send_response(self.iopub_socket, "stream", streamDict) + def do_display(self): + Display(self.Displayer, self.Display) + def do_execute_direct(self, code, silent=False): if not code.strip(): return @@ -88,7 +91,7 @@ def do_execute_direct(self, code, silent=False): try: RunAsyncAndPrint(self.Executor, code, self.ioHandler, self.print_output, silent, 0.1) - Display(self.Displayer, self.Display) + self.do_display() except KeyboardInterrupt: ROOT.gROOT.SetInterrupt() diff --git a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/magics/jsrootmagic.py b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/magics/jsrootmagic.py index 05851b03b90b5..9058c9c059d0d 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/magics/jsrootmagic.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/magics/jsrootmagic.py @@ -15,7 +15,7 @@ from IPython.core.magic import Magics, line_magic, magics_class from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring -from ROOT._jupyroot.helpers.utils import disableJSVis, enableJSVis, enableJSVisDebug +from ROOT._jupyroot.helpers.utils import enableJSVis @magics_class @@ -35,11 +35,9 @@ def jsroot(self, line): """Change the visualisation of plots from images to interactive JavaScript objects.""" args = parse_argstring(self.jsroot, line) if args.arg == "on": - enableJSVis() + enableJSVis(True) elif args.arg == "off": - disableJSVis() - elif args.arg == "debug": - enableJSVisDebug() + enableJSVis(False) def load_ipython_extension(ipython): diff --git a/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/magics/rootbrowsemagic.py b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/magics/rootbrowsemagic.py new file mode 100644 index 0000000000000..a11ae7c212ac9 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_jupyroot/magics/rootbrowsemagic.py @@ -0,0 +1,51 @@ +# -*- coding:utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, ROOT Team. +# Authors: Sergey Linev GSI +# ----------------------------------------------------------------------------- + +################################################################################ +# Copyright (C) 1995-2026, 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 IPython.core.magic import Magics, line_magic, magics_class +from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring + +from ROOT._jupyroot.helpers.utils import browseRootFile + + +@magics_class +class RootBrowseMagics(Magics): + def __init__(self, shell): + super(RootBrowseMagics, self).__init__(shell) + + @line_magic + @magic_arguments() + @argument( + "arg", + nargs="?", + default="", + help="Open and browse ROOT file", + ) + @argument( + "--force", + "-f", + action="store_true", + help="Force opening of large files" + ) + def rootbrowse(self, line): + """start root browser.""" + args = parse_argstring(self.rootbrowse, line) + if not args: + print("Provide ROOT file name") + else: + browseRootFile(args.arg, args.force) + + + +def load_ipython_extension(ipython): + ipython.register_magics(RootBrowseMagics)