22import json
33import os
44import shutil
5- import subprocess
65import sys
76import tempfile
87
9- nbExtension = ".ipynb"
10- convCmdTmpl = "%s nbconvert " \
11- "--to notebook " \
12- "--ExecutePreprocessor.kernel_name=%s " \
13- "--ExecutePreprocessor.enabled=True " \
14- "--ExecutePreprocessor.timeout=3600 " \
15- "--ExecutePreprocessor.startup_timeout=180 " \
16- "%s " \
17- "--output %s"
18- pythonInterpName = 'python3'
19-
20- rootKernelFileContent = '''{
21- "language": "c++",
22- "display_name": "ROOT C++",
23- "argv": [
24- "%s",
25- "-m",
26- "JupyROOT.kernel.rootkernel",
27- "-f",
28- "{connection_file}"
29- ]
30- }
31- ''' % pythonInterpName
32-
33-
348
359# Replace the criterion according to which a line shall be skipped
36- def customLineJunkFilter (line ):
10+ def should_keep_line (line ):
3711 # Skip the banner and empty lines
38- junkLines = ['Info in <TUnixSystem::ACLiC' ,
39- 'Info in <TMacOSXSystem::ACLiC' ,
40- 'FAILED TO establish the default connection to the WindowServer' ,
41- '"version": ' ,
42- '"pygments_lexer": "ipython' ,
43- ' "execution_count":' ,
44- 'libclang_rt.asan-' ]
45- for junkLine in junkLines :
46- if junkLine in line : return False
12+ skip_patterns = [
13+ "Info in <TUnixSystem::ACLiC" ,
14+ "Info in <TMacOSXSystem::ACLiC" ,
15+ "FAILED TO establish the default connection to the WindowServer" ,
16+ '"version": ' ,
17+ '"pygments_lexer": "ipython' ,
18+ ' "execution_count":' ,
19+ "libclang_rt.asan-" ,
20+ ]
21+ for pattern in skip_patterns :
22+ if pattern in line :
23+ return False
4724 return True
4825
26+
4927def removeCellMetadata (lines ):
5028 filteredLines = []
5129 discardLine = False
5230 for line in lines :
5331 if ' "metadata": {' in line :
54- if line .endswith ('},' + os .linesep ): # empty metadata
32+ if line .endswith ("}," + os .linesep ): # empty metadata
5533 continue
5634 discardLine = True
5735
5836 if not discardLine :
5937 filteredLines .append (line )
6038
61- if discardLine and ' },' in line : # end of metadata
39+ if discardLine and " }," in line : # end of metadata
6240 discardLine = False
6341
6442 return filteredLines
6543
44+
6645def getFilteredLines (fileName ):
67- filteredLines = list (filter (customLineJunkFilter , open (fileName ).readlines ()))
46+ with open (fileName ) as f :
47+ filteredLines = list (filter (should_keep_line , f .readlines ()))
6848
6949 # Sometimes the jupyter server adds a new line at the end of the notebook
7050 # and nbconvert does not.
7151 lastLine = filteredLines [- 1 ]
72- if lastLine [- 1 ] != "\n " : filteredLines [- 1 ] += "\n "
52+ if lastLine [- 1 ] != "\n " :
53+ filteredLines [- 1 ] += "\n "
7354
7455 # Remove the metadata field of cells (contains specific execution timestamps)
7556 filteredLines = removeCellMetadata (filteredLines )
7657
7758 return filteredLines
7859
60+
7961# Workaround to support nbconvert versions >= 7.14 . See #14303
8062def patchForNBConvert714 (outNBLines ):
8163 newOutNBLines = []
82- toReplace = ''' "1\\ n"\n '''
64+ toReplace = """ "1\\ n"\n """
8365 replacement = [
84- ''' "1"\n ''' ,
85- ''' ]\n ''' ,
86- ''' },\n ''' ,
87- ''' {\n ''' ,
88- ''' "name": "stdout",\n ''' ,
89- ''' "output_type": "stream",\n ''' ,
90- ''' "text": [\n ''' ,
91- ''' "\\ n"\n ''' ]
66+ """ "1"\n """ ,
67+ """ ]\n """ ,
68+ """ },\n """ ,
69+ """ {\n """ ,
70+ """ "name": "stdout",\n """ ,
71+ """ "output_type": "stream",\n """ ,
72+ """ "text": [\n """ ,
73+ """ "\\ n"\n """ ,
74+ ]
9275
9376 for line in outNBLines :
9477 if line == toReplace :
@@ -97,7 +80,8 @@ def patchForNBConvert714(outNBLines):
9780 newOutNBLines .append (line )
9881 return newOutNBLines
9982
100- def compareNotebooks (inNBName ,outNBName ):
83+
84+ def compareNotebooks (inNBName , outNBName ):
10185 inNBLines = getFilteredLines (inNBName )
10286 inNBLines = patchForNBConvert714 (inNBLines )
10387 outNBLines = getFilteredLines (outNBName )
@@ -106,9 +90,11 @@ def compareNotebooks(inNBName,outNBName):
10690 for line in difflib .unified_diff (inNBLines , outNBLines , fromfile = inNBName , tofile = outNBName ):
10791 areDifferent = True
10892 sys .stdout .write (line )
109- if areDifferent : print ("\n " )
93+ if areDifferent :
94+ print ("\n " )
11095 return areDifferent
11196
97+
11298def createKernelSpec ():
11399 """Create a root kernel spec with the right python interpreter name
114100 and puts it in a tmp directory. Return the name of such directory."""
@@ -117,12 +103,26 @@ def createKernelSpec():
117103 os .mkdir (kernelsPath )
118104 rootKernelPath = os .path .join (kernelsPath , "root" )
119105 os .mkdir (rootKernelPath )
120- kernel_file = open (os .path .join (rootKernelPath , "kernel.json" ), "w" )
121- kernel_file .write (rootKernelFileContent )
122- kernel_file .close ()
106+ with open (os .path .join (rootKernelPath , "kernel.json" ), "w" ) as kernel_file :
107+ kernel_file .write (
108+ """{
109+ "language": "c++",
110+ "display_name": "ROOT C++",
111+ "argv": [
112+ "%s",
113+ "-m",
114+ "JupyROOT.kernel.rootkernel",
115+ "-f",
116+ "{connection_file}"
117+ ]
118+ }
119+ """
120+ % sys .executable
121+ )
123122
124123 return tmpd
125124
125+
126126def addEtcToEnvironment (inNBDirName ):
127127 """Add the etc directory of root to the environment under the name of
128128 JUPYTER_PATH in order to pick up the kernel specs.
@@ -132,43 +132,58 @@ def addEtcToEnvironment(inNBDirName):
132132 os .environ ["IPYTHONDIR" ] = ipythondir
133133 return ipythondir
134134
135- def getInterpreterName ():
136- """Find if the 'jupyter' executable is available on the platform. If
137- yes, return its name else return 'ipython'
138- """
139- ret = subprocess .call ("type jupyter" ,
140- shell = True ,
141- stdout = subprocess .PIPE , stderr = subprocess .PIPE )
142- return "jupyter" if ret == 0 else "i%s" % pythonInterpName
143135
144136def getKernelName (inNBName ):
145- nbj = json .load (open (inNBName ))
146- if nbj ["metadata" ]["kernelspec" ]["language" ] == "python" :
147- return pythonInterpName
148- else : # we support only Python and C++
149- return 'root'
137+ with open (inNBName ) as f :
138+ nbj = json .load (f )
139+ return nbj ["metadata" ]["kernelspec" ]["name" ]
150140
151141
152- def canReproduceNotebook (inNBName , kernelName , needsCompare ):
142+ def canReproduceNotebook (inNBName , needsCompare ):
143+ import nbformat
144+ from nbconvert .preprocessors import ExecutePreprocessor
145+
153146 tmpDir = addEtcToEnvironment (os .path .dirname (inNBName ))
154- outNBName = inNBName .replace (nbExtension ,"_out" + nbExtension )
155- interpName = getInterpreterName ()
156- convCmd = convCmdTmpl % (interpName , kernelName , inNBName , outNBName )
157- exitStatus = os .system (convCmd ) # we use system to inherit the environment in os.environ
158- shutil .rmtree (tmpDir )
147+ outNBName = inNBName .replace (".ipynb" , "_out.ipynb" )
148+
149+ # Load input notebook
150+ with open (inNBName , "r" , encoding = "utf-8" ) as f :
151+ nb = nbformat .read (f , as_version = 4 )
152+
153+ # Configure execution
154+ ep = ExecutePreprocessor (
155+ kernel_name = getKernelName (inNBName ),
156+ timeout = 3600 ,
157+ startup_timeout = 180 ,
158+ allow_errors = False ,
159+ )
160+
161+ # Run the notebook
162+ ep .preprocess (nb , {"metadata" : {"path" : os .path .dirname (inNBName )}})
163+
164+ # Export executed notebook
165+ with open (outNBName , "w" , encoding = "utf-8" ) as f :
166+ nbformat .write (nb , f )
167+
168+ # Compare or return success
159169 if needsCompare :
160- return compareNotebooks (inNBName ,outNBName )
170+ return compareNotebooks (inNBName , outNBName )
161171 else :
162- return exitStatus
172+ return 0 # success
173+
174+ shutil .rmtree (tmpDir )
175+
163176
164177def isInputNotebookFileName (filename ):
165178 if not filename .endswith (".ipynb" ):
166- print ("Notebook files shall have the %s extension" % nbExtension )
179+ print ("Notebook files shall have the .ipynb extension" )
167180 return False
168181 return True
169182
183+
170184if __name__ == "__main__" :
171185 import sys
186+
172187 needsCompare = True
173188 if len (sys .argv ) < 2 :
174189 print ("Usage: nbdiff.py myNotebook.ipynb [compare_output]" )
@@ -180,13 +195,5 @@ def isInputNotebookFileName(filename):
180195 if not isInputNotebookFileName (nbFileName ):
181196 sys .exit (1 )
182197
183- try :
184- # If jupyter is there, ipython is too
185- import jupyter
186- except :
187- raise ImportError ("Cannot import jupyter" )
188-
189- kernelName = getKernelName (nbFileName )
190-
191- retCode = canReproduceNotebook (nbFileName , kernelName , needsCompare )
198+ retCode = canReproduceNotebook (nbFileName , needsCompare )
192199 sys .exit (retCode )
0 commit comments