Logo ROOT  
Reference Guide
Loading...
Searching...
No Matches
converttonotebook.py
Go to the documentation of this file.
1#!/usr/bin/env python
2# Author: Pau Miquel i Mir <pau.miquel.mir@cern.ch> <pmm1g15@soton.ac.uk>>
3# Date: July, 2016
4#
5# DISCLAIMER: This script is a prototype and a work in progress. Indeed, it is possible that
6# it may not work for certain tutorials, and that it, or the tutorial, might need to be
7# tweaked slightly to ensure full functionality. Please do not hesitate to email the author
8# with any questions or with examples that do not work.
9#
10# HELP IT DOESN'T WORK: Two possible solutions:
11# 1. If the tutorial takes a long time to execute (more than 90 seconds), add the name of the
12# tutorial to the list of long tutorials listLongTutorials, in the function findTimeout.
13# 2. Check that helper functions are recognised correctly in split(text).
14#
15# REQUIREMENTS: This script needs jupyter to be properly installed, as it uses the python
16# package nbformat and calls the shell commands `jupyter nbconvert` and `jupyter trust`. The
17# rest of the packages used should be included in a standard installation of python. The script
18# is intended to be run on a UNIX based system.
19#
20#
21# FUNCTIONING:
22# -----------
23# The converttonotebook script creates Jupyter notebooks from raw C++ or python files.
24# Particularly, it is indicated to convert the ROOT tutorials found in the ROOT
25# repository.
26#
27# The script should be called from bash with the following format:
28# python /path/to/script/converttonotebook.py /path/to/<macro>.C /path/to/outdir
29#
30# Indeed the script takes two arguments, the path to the macro and the path to the directory
31# where the notebooks will be created
32#
33# The script's general functioning is as follows. The macro to be converted is imported as a string.
34# A series of modifications are made to this string, for instance delimiting where markdown and
35# code cells begin and end. Then, this string is converted into ipynb format using a function
36# in the nbconvert package. Finally, the notebook is executed and output.
37#
38# For converting python tutorials it is fairly straightforward. It extracts the description and
39# author information from the header and then removes it. It also converts any comment at the
40# beginning of a line into a Markdown cell.
41#
42# For C++ files the process is slightly more complex. The script separates the functions from the
43# main code. The main function is identified as it has the same name as the macro file. The other
44# functions are considered functions. The main function is "extracted" and presented as main code.
45# The helper functions are placed in their own code cell with the %%cpp -d magic to enable function
46# defintion. Finally, as with Python macros, relevant information is extracted from the header, and
47# newline comments are converted into Markdown cells (unless they are in helper functions).
48#
49# The script creates an .ipynb version of the macro, with the full output included.
50# The files are named:
51# <macro>.<C or py>.nbconvert.ipynb
52#
53# It is called by filter.cxx, which in turn is called by doxygen when processing any file
54# in the ROOT repository. filter.cxx only calls convertonotebook.py when the string \notebook
55# is found in the header of the tutorial, but this script checks for its presence as well.
56
57
58import re
59import os
60import sys
61import json
62import time
63import doctest
64import textwrap
65import subprocess
66from nbformat import v3, v4
67from datetime import datetime, date
68import statistics
69
70# -------------------------------------
71# -------- Function definitions--------
72# -------------------------------------
73
74def unindenter(string, spaces = 3):
75 """
76 Returns string with each line unindented by 3 spaces. If line isn't indented, it stays the same.
77 >>> unindenter(" foobar")
78 'foobar\\n'
79 >>> unindenter("foobar")
80 'foobar\\n'
81 >>> unindenter('''foobar
82 ... foobar
83 ... foobar''')
84 'foobar\\nfoobar\\nfoobar\\n'
85 """
86 newstring = ''
87 lines = string.splitlines()
88 for line in lines:
89 if line.startswith(spaces*' '):
90 newstring += (line[spaces:] + "\n")
91 else:
92 newstring += (line + "\n")
93
94 return newstring
95
96def measureIndentation(text):
97 """Measure the indentation width"""
98 nSpaces = sorted({len(line) - len(line.lstrip()) for line in text.splitlines() if line.strip()})
99 return nSpaces[0] if nSpaces[0] != 0 or len(nSpaces) == 1 else nSpaces[1]
100
101
102def readHeaderPython(text):
103 """
104 Extract author and description from header, eliminate header from text. Also returns
105 notebook boolean, which is True if the string \notebook is present in the header
106 Also determine options (-js, -nodraw, -header) passed in \notebook command, and
107 return their booleans
108 >>> readHeaderPython('''## \\file
109 ... ## \\ingroup tutorials
110 ... ## \\\\notebook
111 ... ## This is the description of the tutorial
112 ... ##
113 ... ## \\macro_image
114 ... ## \\macro_code
115 ... ##
116 ... ## \\\\author John Brown
117 ... def tutorialfuncion()''')
118 ('def tutorialfuncion()\\n', 'This is the description of the tutorial\\n\\n\\n', 'John Brown', True, False, False, False)
119 >>> readHeaderPython('''## \\file
120 ... ## \\ingroup tutorials
121 ... ## \\\\notebook -js
122 ... ## This is the description of the tutorial
123 ... ##
124 ... ## \\macro_image
125 ... ## \\macro_code
126 ... ##
127 ... ## \\\\author John Brown
128 ... def tutorialfuncion()''')
129 ('def tutorialfuncion()\\n', 'This is the description of the tutorial\\n\\n\\n', 'John Brown', True, True, False, False)
130 >>> readHeaderPython('''## \\file
131 ... ## \\ingroup tutorials
132 ... ## \\\\notebook -nodraw
133 ... ## This is the description of the tutorial
134 ... ##
135 ... ## \\macro_image
136 ... ## \\macro_code
137 ... ##
138 ... ## \\\\author John Brown
139 ... def tutorialfuncion()''')
140 ('def tutorialfuncion()\\n', 'This is the description of the tutorial\\n\\n\\n', 'John Brown', True, False, True, False)
141 """
142 lines = text.splitlines()
143
144 description = ''
145 author = ''
146 isNotebook = False
147 isJsroot = False
148 nodraw = False
149 needsHeaderFile = False
150 for i, line in enumerate(lines):
151 if line.startswith("## \\aut"):
152 author = line[11:]
153 elif line.startswith("## \\note"):
154 isNotebook = True
155 if "-js" in line:
156 isJsroot = True
157 if "-nodraw" in line:
158 nodraw = True
159 if "-header" in line:
160 needsHeaderFile = True
161 elif line.startswith("##"):
162 if not line.startswith("## \\") and isNotebook:
163 description += (line[3:] + '\n')
164 else:
165 break
166 newtext = ''
167 for line in lines[i:]:
168 newtext += (line + "\n")
169
170 return newtext, description, author, isNotebook, isJsroot, nodraw, needsHeaderFile
171
172
173def pythonComments(text):
174 """
175 Converts comments delimited by # or ## and on a new line into a markdown cell.
176 For python files only
177 >>> pythonComments('''## This is a
178 ... ## multiline comment
179 ... def function()''')
180 '# <markdowncell>\\n## This is a\\n## multiline comment\\n# <codecell>\\ndef function()\\n'
181 >>> pythonComments('''def function():
182 ... variable = 5 # Comment not in cell
183 ... # Comment also not in cell''')
184 'def function():\\n variable = 5 # Comment not in cell\\n # Comment also not in cell\\n'
185 """
186 text = text.splitlines()
187 newtext = ''
188 inComment = False
189 for i, line in enumerate(text):
190 if line.startswith("#") and not inComment: # True if first line of comment
191 inComment = True
192 newtext += "# <markdowncell>\n"
193 newtext += (line + "\n")
194 elif inComment and not line.startswith("#"): # True if first line after comment
195 inComment = False
196 newtext += "# <codecell>\n"
197 newtext += (line+"\n")
198 else:
199 newtext += (line+"\n")
200 return newtext
201
202
203def pythonMainFunction(text):
204 lines = text.splitlines()
205 functionContentRe = re.compile('def %s\\(.*\\):' % tutName , flags = re.DOTALL | re.MULTILINE)
206 newtext = ''
207 inMainFunction = False
208 hasMainFunction = False
209 for line in lines:
210
211 if hasMainFunction:
212 if line.startswith("""if __name__ == "__main__":""") or line.startswith("""if __name__ == '__main__':"""):
213 break
214 match = functionContentRe.search(line)
215 if inMainFunction and not line.startswith(" ") and line != "":
216 inMainFunction = False
217 if match:
218 inMainFunction = True
219 hasMainFunction = True
220 else:
221 if inMainFunction:
222 newtext += (line[4:] + '\n')
223 else:
224 newtext += (line + '\n')
225 return newtext
226
227def convertDoxygenLatexToMarkdown(text):
228 """Replace formula tags used by doxygen to the ones used in notebooks."""
229 text = text.replace("\\f$", "$")
230 text = text.replace("\\f[", "$$")
231 text = text.replace("\\f]", "$$")
232 return text
233
234def readHeaderCpp(text):
235 """
236 Extract author and description from header, eliminate header from text. Also returns
237 notebook boolean, which is True if the string \notebook is present in the header
238 Also determine options (-js, -nodraw, -header) passed in \notebook command, and
239 return their booleans
240 >>> readHeaderCpp('''/// \\file
241 ... /// \\ingroup tutorials
242 ... /// \\\\notebook
243 ... /// This is the description of the tutorial
244 ... ///
245 ... /// \\macro_image
246 ... /// \\macro_code
247 ... ///
248 ... /// \\\\author John Brown
249 ... void tutorialfuncion(){}''')
250 ('void tutorialfuncion(){}\\n', '# This is the description of the tutorial\\n# \\n# \\n', 'John Brown', True, False, False, False)
251 >>> readHeaderCpp('''/// \\file
252 ... /// \\ingroup tutorials
253 ... /// \\\\notebook -js
254 ... /// This is the description of the tutorial
255 ... ///
256 ... /// \\macro_image
257 ... /// \\macro_code
258 ... ///
259 ... /// \\\\author John Brown
260 ... void tutorialfuncion(){}''')
261 ('void tutorialfuncion(){}\\n', '# This is the description of the tutorial\\n# \\n# \\n', 'John Brown', True, True, False, False)
262 >>> readHeaderCpp('''/// \\file
263 ... /// \\ingroup tutorials
264 ... /// \\\\notebook -nodraw
265 ... /// This is the description of the tutorial
266 ... ///
267 ... /// \\macro_image
268 ... /// \\macro_code
269 ... ///
270 ... /// \\\\author John Brown
271 ... void tutorialfuncion(){}''')
272 ('void tutorialfuncion(){}\\n', '# This is the description of the tutorial\\n# \\n# \\n', 'John Brown', True, False, True, False)
273 """
274 lines = text.splitlines()
275
276 description = ''
277 author = ''
278 isNotebook = False
279 isJsroot = False
280 nodraw = False
281 needsHeaderFile = False
282 for i, line in enumerate(lines):
283 if line.startswith("/// \\aut"):
284 author = line[12:]
285 if line.startswith("/// \\note"):
286 isNotebook = True
287 if "-js" in line:
288 isJsroot = True
289 if "-nodraw" in line:
290 nodraw = True
291 if "-header" in line:
292 needsHeaderFile = True
293 if line.startswith("///"):
294 if not line.startswith("/// \\") and isNotebook:
295 description += ('# ' + line[4:] + '\n')
296 else:
297 break
298 newtext = ''
299 for line in lines[i:]:
300 newtext += (line + "\n")
301 description = convertDoxygenLatexToMarkdown(description)
302 return newtext, description, author, isNotebook, isJsroot, nodraw, needsHeaderFile
303
304
305def cppFunction(text):
306 """
307 Extracts main function for the function enclosure by means of regular expression
308 >>> cppFunction('''void mainfunction(arguments = values){
309 ... content of function
310 ... which spans
311 ... several lines
312 ... }''')
313 '\\n content of function\\n which spans\\n several lines\\n'
314 >>> cppFunction('''void mainfunction(arguments = values)
315 ... {
316 ... content of function
317 ... which spans
318 ... several lines
319 ... }''')
320 '\\n content of function\\n which spans\\n several lines\\n'
321 >>> cppFunction('''void mainfunction(arguments = values
322 ... morearguments = morevalues)
323 ... {
324 ... content of function
325 ... which spans
326 ... several lines
327 ... }''')
328 '\\n content of function\\n which spans\\n several lines\\n'
329 """
330 functionContentRe = re.compile(r'(?<=\{).*(?=^\})', flags = re.DOTALL | re.MULTILINE)
331
332 match = functionContentRe.search(text)
333
334 if match:
335 return match.group()
336 else:
337 return text
338
339def cppComments(text):
340 """
341 Converts comments delimited by // and on a new line into a markdown cell. Skips comments inside
342 blocks and braces, though, since these would otherwise be ripped apart. For C++ files only.
343 >>> cppComments('''// This is a
344 ... // multiline comment
345 ... void function(){}''')
346 '# <markdowncell>\\n# This is a\\n# multiline comment\\n# <codecell>\\nvoid function(){}\\n'
347 >>> cppComments('''void function(){
348 ... int variable = 5 // Comment not in cell
349 ... // Comment also not in cell
350 ... }''')
351 'void function(){\\n int variable = 5 // Comment not in cell\\n // Comment also not in cell\\n}\\n'
352 """
353 newtext = ''
354 inComment = False
355 curlyDepth = 0
356 braceDepth = 0
357
358 for line in text.splitlines():
359
360 if line.strip().startswith("//") and curlyDepth == 0 and braceDepth == 0:
361 line = line.strip(" /")
362
363 # Allow for doxygen-style latex:
364 line = convertDoxygenLatexToMarkdown(line)
365
366 if not inComment: # True if first line of comment
367 inComment = True
368 newtext += "# <markdowncell>\n\n"
369 newtext += ("# " + line + "\n")
370 else:
371 for char in line:
372 if char == "(":
373 braceDepth += 1
374 elif char == ")":
375 braceDepth -= 1
376 elif char == "{":
377 curlyDepth += 1
378 elif char == "}":
379 curlyDepth -= 1
380
381 if inComment: # True if first line after comment
382 inComment = False
383 newtext += "# <codecell>\n"
384
385 newtext += (line+"\n")
386
387 return newtext
388
389
390def findBracedBlock(text, startpos, openingBraceChar):
391 """
392 Scan the string in <text>, starting at <startpos>. Return index of opening and
393 matching closing brace if present. The brace to search for is passed in <openingBraceChar>,
394 e.g. '(', '{', '['.
395 """
396 depth = 0
397 braceRegex = {"<": r"[<>]", "(": r"[()]", "{": r"[{}]", "[": r"[[]]"}
398 if openingBraceChar not in braceRegex:
399 raise ValueError("Brace character " + openingBraceChar + " doesn't seem to be an opening brace")
400
401 begin = None
402 bracketRe = re.compile(braceRegex[openingBraceChar], flags = re.DOTALL | re.MULTILINE)
403
404 while True:
405 lastMatch = bracketRe.search(text, startpos)
406 if lastMatch:
407 if begin is None:
408 if lastMatch.group() != openingBraceChar:
409 raise ValueError("Found " + lastMatch.group() + " while looking for opening brace in\n" + text[pos:])
410 begin = lastMatch.start()
411
412 depth += 1 if lastMatch.group(0) == openingBraceChar else -1
413 startpos = lastMatch.end()
414
415 if depth == 0:
416 ret = (begin,lastMatch.end()-1)
417 begin = None
418 yield ret
419 elif depth == 0:
420 return
421 else:
422 raise ValueError("Unmatched " + braceRegex[openingBraceChar] + " at " + text[startpos-15:startpos+15])
423
424
425def findStuffBeforeFunc(text, searchStart, searchEnd):
426 beforeFunctionRe = re.compile("(;|}|//[^\n]*)\s*$", flags = re.MULTILINE)
427 try:
428 # Find the last '}' or comment line etc before function definition:
429 lastMatchBeforeFunc = [thisMatch for thisMatch in beforeFunctionRe.finditer(text, searchStart, searchEnd)][-1]
430 return lastMatchBeforeFunc.end()+1
431 except IndexError as e:
432 return searchStart
433
434
435def split(text):
436 """
437 Splits the text string into main, helpers, and rest. main is the main function,
438 i.e. the function with the same name as the macro file. Helpers is a list of
439 strings, each a helper function, i.e. any other function that is not the main function.
440 Finally, rest is a string containing any top-level code/declarations outside of any function.
441 Comments above a function will be converted to a markdown cell in notebooks.
442 Intended for C++ files only.
443 >>> split('''void tutorial(){
444 ... content of tutorial
445 ... }''')
446 ('void tutorial(){\\n content of tutorial\\n}', [], '')
447 >>> split('''void tutorial(){
448 ... content of tutorial
449 ... }
450 ... void helper(arguments = values){
451 ... helper function
452 ... content spans lines
453 ... }''')
454 ('void tutorial(){\\n content of tutorial\\n}', ['\\n# <markdowncell>\\n A helper function is created: \\n# <codecell>\\n%%cpp -d\\nvoid helper(arguments = values){\\n helper function\\n content spans lines\\n}'], '')
455 >>> split('''#include <header.h>
456 ... using namespace NAMESPACE
457 ... void tutorial(){
458 ... content of tutorial
459 ... }
460 ... void helper(arguments = values){
461 ... helper function
462 ... content spans lines
463 ... }''')
464 ('void tutorial(){\\n content of tutorial\\n}', ['\\n# <markdowncell>\\n A helper function is created: \\n# <codecell>\\n%%cpp -d\\nvoid helper(arguments = values){\\n helper function\\n content spans lines\\n}'], '#include <header.h>\\nusing namespace NAMESPACE')
465 >>> split('''void tutorial(){
466 ... content of tutorial
467 ... }
468 ... // This is a multiline
469 ... // description of the
470 ... // helper function
471 ... void helper(arguments = values){
472 ... helper function
473 ... content spans lines
474 ... }''')
475 ('void tutorial(){\\n content of tutorial\\n}', ['\\n# <markdowncell>\\n This is a multiline\\n description of the\\n helper function\\n \\n# <codecell>\\n%%cpp -d\\nvoid helper(arguments = values){\\n helper function\\n content spans lines\\n}'], '')
476 """
477 functionRe = re.compile(r"[^(/;]*?\s+[*&]*([\w:]+)\s*\‍(", flags = re.DOTALL | re.MULTILINE)
478 tailRe = re.compile(r"}\s*;?", flags = re.DOTALL | re.MULTILINE)
479 helpers = []
480 definitions = []
481 main = ""
482 searchStart = 0
483 for curlyBegin,curlyEnd in findBracedBlock(text, 0, "{"):
484 functionMatches = [match for match in functionRe.finditer(text, searchStart, curlyBegin)]
485
486 tailMatch = tailRe.match(text, curlyEnd)
487 if tailMatch:
488 curlyEnd = tailMatch.end()
489 else:
490 sys.stderr.write("Failed match on " + text[curlyEnd:curlyEnd+10])
491
492 if not functionMatches:
493 definitions.append("%%cpp -d\n" + text[searchStart:curlyEnd])
494 searchStart = curlyEnd+1
495 continue
496
497 functionMatch = functionMatches[-1]
498 startpos = functionMatch.start()
499 # Search for function arguments
500 roundBegin,roundEnd = next( findBracedBlock(text, functionMatch.end()-2, "(") )
501 if roundEnd > curlyBegin:
502 raise RuntimeError("Didn't find '()' before '{}'")
503
504
505 beforeFuncPos = findStuffBeforeFunc(text, searchStart, functionMatch.start(1))
506 stuffBeforeFunc = text[searchStart:beforeFuncPos]
507 funcString = text[beforeFuncPos:curlyEnd]
508 searchStart = curlyEnd
509
510 commentLines = []
511 for line in stuffBeforeFunc.splitlines():
512 if line.strip().startswith("//"):
513 commentLines.append(line)
514 else:
515 definitions.append(line)
516
517 if tutName == functionMatch.group(1).strip(): # if the name of the function is that of the macro
518 main = funcString
519 else:
520 helpers.append((funcString, functionMatch.group(1).strip(), commentLines))
521
522 newHelpers = []
523 for helper,functionName,commentLines in helpers: # For each helper function
524 helper = helper.rstrip(" \n")
525 helperDescription = ""
526 for line in commentLines:
527 helper = helper.replace(line, "")
528 if line.strip() in ["//", "///"]:
529 # Convert empty comment lines to markdown breaks
530 helperDescription += "\n\n"
531 else:
532 helperDescription += line.strip(" /*") + "\n"
533 if len(helperDescription) == 0: # If no comments are found create generic description
534 helperDescription = "Definition of a helper function:"
535
536 if functionName != "main": # remove void main function
537 newHelpers.append("\n# <markdowncell>\n " + helperDescription + " \n# <codecell>\n%%cpp -d\n" + helper)
538
539 return main, newHelpers, "\n".join(definitions)
540
541
542
543def processmain(text):
544 """
545 Evaluates whether the main function returns a TCanvas or requires input. If it
546 does then the keepfunction flag is True, meaning the function wont be extracted
547 by cppFunction. If the initial condition is true then an extra cell is added
548 before at the end that calls the main function is returned, and added later.
549 >>> processmain('''void function(){
550 ... content of function
551 ... spanning several
552 ... lines
553 ... }''')
554 ('void function(){\\n content of function\\n spanning several\\n lines\\n}', '')
555 >>> processmain('''void function(arguments = values){
556 ... content of function
557 ... spanning several
558 ... lines
559 ... }''')
560 ('void function(arguments = values){\\n content of function\\n spanning several\\n lines\\n}', '# <markdowncell> \\n Arguments are defined. \\n# <codecell>\\narguments = values;\\n# <codecell>\\n')
561 >>> processmain('''void function(argument1 = value1, //comment 1
562 ... argument2 = value2 /*comment 2*/ ,
563 ... argument3 = value3,
564 ... argument4 = value4)
565 ... {
566 ... content of function
567 ... spanning several
568 ... lines
569 ... }''')
570 ('void function(argument1 = value1, //comment 1\\n argument2 = value2 /*comment 2*/ ,\\n argument3 = value3, \\n argument4 = value4)\\n{\\n content of function\\n spanning several\\n lines\\n}', '# <markdowncell> \\n Arguments are defined. \\n# <codecell>\\nargument1 = value1;\\nargument2 = value2;\\nargument3 = value3;\\nargument4 = value4;\\n# <codecell>\\n')
571 >>> processmain('''TCanvas function(){
572 ... content of function
573 ... spanning several
574 ... lines
575 ... return c1
576 ... }''')
577 ('TCanvas function(){\\n content of function\\n spanning several \\n lines\\n return c1\\n}', '')
578 """
579
580 argumentsCell = ''
581
582 if text:
583 argumentsre = re.compile(r'(?<=\‍().*?(?=\‍))', flags = re.DOTALL | re.MULTILINE)
584 arguments = argumentsre.search(text)
585
586 if len(arguments.group()) > 3:
587 argumentsCell = "# <markdowncell> \n Arguments are defined. \n# <codecell>\n"
588 individualArgumentre = re.compile(r'[^/\n,]*?=[^/\n,]*') #, flags = re.DOTALL) #| re.MULTILINE)
589 argumentList=individualArgumentre.findall(arguments.group())
590 for argument in argumentList:
591 argumentsCell += argument.strip("\n ") + ";\n"
592 argumentsCell += "# <codecell>\n"
593
594 return text, argumentsCell
595
596# now define text transformers
597def removePaletteEditor(code):
598 code = code.replace("img->StartPaletteEditor();", "")
599 code = code.replace("Open the color editor", "")
600 return code
601
602
603def runEventExe(code):
604 if "copytree" in tutName:
605 return "# <codecell> \n.! $ROOTSYS/test/eventexe 1000 1 1 1 \n" + code
606 return code
607
608
609def getLibMathMore(code):
610 if "quasirandom" == tutName:
611 return "# <codecell> \ngSystem->Load(\"libMathMore\"); \n# <codecell> \n" + code
612 return code
613
614
615def roofitRemoveSpacesComments(code):
616
617 def changeString(matchObject):
618 matchString = matchObject.group()
619 matchString = matchString[0] + " " + matchString[1:]
620 matchString = matchString.replace(" " , "THISISASPACE")
621 matchString = matchString.replace(" " , "")
622 matchString = matchString.replace("THISISASPACE" , " ")
623 return matchString
624
625 newcode = re.sub("#\s\s?\w\s[\w-]\s\w.*", changeString , code)
626 return newcode
627
628def declareNamespace(code):
629 if "using namespace RooFit;\nusing namespace RooStats;" in code:
630 code = code.replace("using namespace RooFit;\nusing namespace RooStats;", "# <codecell>\n%%cpp -d\n// This is a workaround to make sure the namespace is used inside functions\nusing namespace RooFit;\nusing namespace RooStats;\n# <codecell>\n")
631
632 else:
633 code = code.replace("using namespace RooFit;", "# <codecell>\n%%cpp -d\n// This is a workaround to make sure the namespace is used inside functions\nusing namespace RooFit;\n# <codecell>\n")
634 code = code.replace("using namespace RooStats;", "# <codecell>\n%%cpp -d\n// This is a workaround to make sure the namespace is used inside functions\nusing namespace RooStats;\n# <codecell>\n")
635 code = code.replace("using namespace ROOT::Math;", "# <codecell>\n%%cpp -d\n// This is a workaround to make sure the namespace is used inside functions\nusing namespace ROOT::Math;\n# <codecell>\n")
636
637 return code
638
639
640def rs401dGetFiles(code):
641 if tutName == "rs401d_FeldmanCousins":
642 code = code.replace(
643 """#include "../tutorials/roofit/roostats/NuMuToNuE_Oscillation.h"\n#include "../tutorials/roofit/roostats/NuMuToNuE_Oscillation.cxx" // so that it can be executed directly\n#else\n#include "../tutorials/roofit/roostats/NuMuToNuE_Oscillation.cxx+" // so that it can be executed directly\n""" , """TString tutDir = gROOT->GetTutorialDir();\nTString headerDir = TString::Format("#include \\\"%s/roostats/NuMuToNuE_Oscillation.h\\\"", tutDir.Data());\nTString impDir = TString::Format("#include \\\"%s/roostats/NuMuToNuE_Oscillation.cxx\\\"", tutDir.Data());\ngROOT->ProcessLine(headerDir);\ngROOT->ProcessLine(impDir);""")
644 return code
645
646
647def declareIncludes(code):
648 if tutName != "fitcont":
649 code = re.sub(r"# <codecell>\s*#include", "# <codecell>\n%%cpp -d\n#include" , code)
650 return code
651
652
653def tree4GetFiles(code):
654 if tutName == "tree4":
655 code = code.replace(
656 """#include \"../test/Event.h\"""" , """# <codecell>\nTString dir = "$ROOTSYS/test/Event.h";\ngSystem->ExpandPathName(dir);\nTString includeCommand = TString::Format("#include \\\"%s\\\"" , dir.Data());\ngROOT->ProcessLine(includeCommand);""")
657 return code
658
659
660def disableDrawProgressBar(code):
661 code = code.replace(":DrawProgressBar",":!DrawProgressBar")
662 return code
663def fixes(code):
664 codeTransformers=[removePaletteEditor, runEventExe, getLibMathMore,
665 roofitRemoveSpacesComments,
666 #declareNamespace,
667 rs401dGetFiles,
668 declareIncludes, tree4GetFiles, disableDrawProgressBar]
669
670 for transformer in codeTransformers:
671 code = transformer(code)
672
673 return code
674
675
676def changeMarkdown(code):
677 code = code.replace("~~~" , "```")
678 code = code.replace("{.cpp}", "cpp")
679 code = code.replace("{.bash}", "bash")
680 return code
681
682
683def isCpp():
684 """
685 Return True if extension is a C++ file
686 """
687 return extension in ("C", "c", "cpp", "C++", "cxx")
688
689
690def findTimeout():
691 if tutName in ["OneSidedFrequentistUpperLimitWithBands",
692 "StandardBayesianNumericalDemo",
693 "TwoSidedFrequentistUpperLimitWithBands",
694 "HybridStandardForm",
695 "rs401d_FeldmanCousins",
696 "TMVAMultipleBackgroundExample",
697 "TMVARegression",
698 "TMVAClassification",
699 "StandardHypoTestDemo"]:
700 return 900
701 elif tutName in ["df103_NanoAODHiggsAnalysis"]:
702 return 1200
703 else:
704 return 120
705
706
707# -------------------------------------
708# ------------ Main Program------------
709# -------------------------------------
710def mainfunction(text):
711 """
712 Main function. Calls all other functions, depending on whether the macro input is in python or c++.
713 It adds the header information. Also, it adds a cell that draws all canvases. The working text is
714 then converted to a version 3 jupyter notebook, subsequently updated to a version 4. Then, metadata
715 associated with the language the macro is written in is attatched to he notebook. Finally the
716 notebook is executed and output as a Jupyter notebook.
717 """
718 # Modify text from macros to suit a notebook
719 if isCpp():
720 try:
721 main, helpers, rest = split(text)
722 main, argumentsCell = processmain(main)
723 funcText = cppFunction(main)
724 main = cppComments(unindenter(funcText, measureIndentation(funcText))) # Remove function, Unindent, and convert comments to Markdown cells
725
726 if argumentsCell:
727 main = argumentsCell + main
728
729 rest = cppComments(rest) # Convert top level code comments to Markdown cells
730
731 # Construct text by starting with top level code, then the helper functions, and finally the main function.
732 # Also add cells for headerfile, or keepfunction
733 if needsHeaderFile:
734 text = "# <markdowncell>\n# The header file must be copied to the current directory\n# <codecell>\n.!cp %s%s.h .\n# <codecell>\n" % (tutRelativePath, tutName)
735 text += rest
736 else:
737 text = "# <codecell>\n" + rest
738
739 for helper in helpers:
740 text += helper
741
742 text += ("\n# <codecell>\n" + main)
743 except Exception as e:
744 sys.stderr.write("Failed to convert C++ to notebook\n" + str(e))
745 raise e
746
747 if extension == "py":
748 text = pythonMainFunction(text)
749 text = pythonComments(text) # Convert comments into Markdown cells
750
751
752 # Perform last minute fixes to the notebook, used for specific fixes needed by some tutorials
753 text = fixes(text)
754
755 # Change to standard Markdown
756 newDescription = changeMarkdown(description)
757
758 # Add the title and header of the notebook
759 text = "# <markdowncell> \n# # %s\n%s# \n# \n# **Author:** %s \n# <i><small>This notebook tutorial was automatically generated " \
760 "with <a href= \"https://github.com/root-project/root/blob/master/documentation/doxygen/converttonotebook.py\">ROOTBOOK-izer</a> " \
761 "from the macro found in the ROOT repository on %s.</small></i>\n# <codecell>\n%s" % (tutTitle, newDescription, author, date, text)
762
763 # Add cell at the end of the notebook that draws all the canvasses. Add a Markdown cell before explaining it.
764 if isJsroot and not nodraw:
765 if isCpp():
766 text += "\n# <markdowncell> \n# Draw all canvases \n# <codecell>\n%jsroot on\ngROOT->GetListOfCanvases()->Draw()"
767 if extension == "py":
768 text += "\n# <markdowncell> \n# Draw all canvases \n# <codecell>\n%jsroot on\nfrom ROOT import gROOT \ngROOT.GetListOfCanvases().Draw()"
769
770 elif not nodraw:
771 if isCpp():
772 text += "\n# <markdowncell> \n# Draw all canvases \n# <codecell>\ngROOT->GetListOfCanvases()->Draw()"
773 if extension == "py":
774 text += "\n# <markdowncell> \n# Draw all canvases \n# <codecell>\nfrom ROOT import gROOT \ngROOT.GetListOfCanvases().Draw()"
775
776 # Create a notebook from the working text
777 nbook = v3.reads_py(text)
778 nbook = v4.upgrade(nbook) # Upgrade v3 to v4
779
780 # Load notebook string into json format, essentially creating a dictionary
781 json_data = json.loads(v4.writes(nbook))
782
783 # add the corresponding metadata
784 if extension == "py":
785 json_data['metadata'] = {
786 "kernelspec": {
787 "display_name": "Python " + str(sys.version_info[0]),
788 "language": "python",
789 "name": "python" + str(sys.version_info[0])
790 },
791 "language_info": {
792 "codemirror_mode": {
793 "name": "ipython",
794 "version": 2
795 },
796 "file_extension": ".py",
797 "mimetype": "text/x-python",
798 "name": "python",
799 "nbconvert_exporter": "python",
800 "pygments_lexer": "ipython2",
801 "version": "2.7.10"
802 }
803 }
804 elif isCpp():
805 json_data['metadata'] = {
806 "kernelspec": {
807 "display_name": "ROOT C++",
808 "language": "c++",
809 "name": "root"
810 },
811 "language_info": {
812 "codemirror_mode": "text/x-c++src",
813 "file_extension": ".C",
814 "mimetype": " text/x-c++src",
815 "name": "c++"
816 }
817 }
818
819 # write the json file with the metadata
820 with open(outPathName, 'w') as fout:
821 json.dump(json_data, fout, indent=1, sort_keys=True)
822
823 timeout = findTimeout()
824
825 # Call commmand that executes the notebook and creates a new notebook with the output
826 r = subprocess.call(["jupyter", "nbconvert", "--ExecutePreprocessor.timeout={}".format(timeout), "--to=notebook", "--execute", outPathName])
827 retry = 0
828 while r != 0 and retry < 2:
829 # Work around glitches in NB conversion
830 retry += 1
831 r = subprocess.call(["jupyter", "nbconvert", "--ExecutePreprocessor.timeout={}".format(timeout*2), "--to=notebook", "--execute", outPathName])
832
833 if r != 0:
834 sys.stderr.write("NOTEBOOK_CONVERSION_WARNING: Nbconvert failed for notebook %s with return code %s\n" %(outPathName,r))
835 # If notebook conversion did not work, try again without the option --execute
836 if subprocess.call(["jupyter", "nbconvert", "--ExecutePreprocessor.timeout={}".format(timeout), "--to=notebook", outPathName]) != 0:
837 raise RuntimeError("NOTEBOOK_CONVERSION_WARNING: Nbconvert failed for notebook %s with return code %s\n" %(outname,r))
838 else:
839 if isJsroot:
840 subprocess.call(["jupyter", "trust", os.path.join(outdir, outnameconverted)])
841 # Only remove notebook without output if nbconvert succeeds
842 os.remove(outPathName)
843
844 return r
845
846if __name__ == "__main__":
847
848 if str(sys.argv[1]) == "-test":
849 tutName = "tutorial"
850 doctest.testmod(verbose=True)
851
852 else:
853 # -------------------------------------
854 # ----- Preliminary definitions--------
855 # -------------------------------------
856
857 # Extract and define the name of the file as well as its derived names
858 tutPathName = str(sys.argv[1])
859 tutPath = os.path.dirname(tutPathName)
860 if tutPath.split("/")[-2] == "tutorials":
861 tutRelativePath = "$ROOTSYS/tutorials/%s/" % tutPath.split("/")[-1]
862 tutFileName = os.path.basename(tutPathName)
863 tutName, extension = tutFileName.split(".")
864 #tutTitle = re.sub( r"([A-Z\d])", r" \1", tutName).title()
865 tutTitle = tutName
866 outname = tutFileName + ".ipynb"
867 outnameconverted = tutFileName + ".nbconvert.ipynb"
868
869 # Extract output directory
870 try:
871 outdir = str(sys.argv[2])
872 except:
873 outdir = tutPath
874
875 outPathName = os.path.join(outdir, outname)
876 # Find and define the time and date this script is run
877 date = datetime.now().strftime("%A, %B %d, %Y at %I:%M %p")
878
879 # -------------------------------------
880 # -------------------------------------
881 # -------------------------------------
882
883 # Set DYLD_LIBRARY_PATH. When run without root access or as a different user, especially from Mac systems,
884 # it is possible for security reasons that the environment does not include this definition, so it is manually defined.
885 os.environ["DYLD_LIBRARY_PATH"] = os.environ["ROOTSYS"] + "/lib"
886
887 # Open the file to be converted
888 with open(tutPathName) as fin:
889 text = fin.read()
890
891 # Extract information from header and remove header from text
892 if extension == "py":
893 text, description, author, isNotebook, isJsroot, nodraw, needsHeaderFile = readHeaderPython(text)
894 elif isCpp():
895 text, description, author, isNotebook, isJsroot, nodraw, needsHeaderFile = readHeaderCpp(text)
896
897 if isNotebook:
898 starttime = time.time()
899 try:
900 ret = mainfunction(text)
901 except Exception as e:
902 print("Failed to convert to notebook", outPathName)
903 raise e
904 print("Notebook {0} run time".format(tutFileName), time.time() - starttime)
905
906 sys.exit(ret)
unindenter(string, spaces=3)
Returns string with each line unindented by 3 spaces.