102def readHeaderPython(text):
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
111 ... ## This is the description of the tutorial
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
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
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)
142 lines = text.splitlines()
149 needsHeaderFile = False
150 for i, line in enumerate(lines):
151 if line.startswith("## \\aut"):
153 elif line.startswith("## \\note"):
157 if "-nodraw" in line:
159 if "-header" in line:
160 needsHeaderFile = True
161 elif line.startswith("##"):
162 if not line.startswith("## \\") and isNotebook:
163 description += (line[3:] + '\n')
167 for line in lines[i:]:
168 newtext += (line + "\n")
170 return newtext, description, author, isNotebook, isJsroot, nodraw, needsHeaderFile
234def readHeaderCpp(text):
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
243 ... /// This is the description of the tutorial
245 ... /// \\macro_image
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
256 ... /// \\macro_image
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
267 ... /// \\macro_image
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)
274 lines = text.splitlines()
281 needsHeaderFile = False
282 for i, line in enumerate(lines):
283 if line.startswith("/// \\aut"):
285 if line.startswith("/// \\note"):
289 if "-nodraw" in line:
291 if "-header" in line:
292 needsHeaderFile = True
293 if line.startswith("///"):
294 if not line.startswith("/// \\") and isNotebook:
295 description += ('# ' + line[4:] + '\n')
299 for line in lines[i:]:
300 newtext += (line + "\n")
301 description = convertDoxygenLatexToMarkdown(description)
302 return newtext, description, author, isNotebook, isJsroot, nodraw, needsHeaderFile
339def cppComments(text):
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
351 'void function(){\\n int variable = 5 // Comment not in cell\\n // Comment also not in cell\\n}\\n'
358 for line in text.splitlines():
360 if line.strip().startswith("//") and curlyDepth == 0 and braceDepth == 0:
361 line = line.strip(" /")
363 # Allow for doxygen-style latex:
364 line = convertDoxygenLatexToMarkdown(line)
366 if not inComment: # True if first line of comment
368 newtext += "# <markdowncell>\n\n"
369 newtext += ("# " + line + "\n")
381 if inComment: # True if first line after comment
383 newtext += "# <codecell>\n"
385 newtext += (line+"\n")
390def findBracedBlock(text, startpos, openingBraceChar):
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>,
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")
402 bracketRe = re.compile(braceRegex[openingBraceChar], flags = re.DOTALL | re.MULTILINE)
405 lastMatch = bracketRe.search(text, startpos)
408 if lastMatch.group() != openingBraceChar:
409 raise ValueError("Found " + lastMatch.group() + " while looking for opening brace in\n" + text[pos:])
410 begin = lastMatch.start()
412 depth += 1 if lastMatch.group(0) == openingBraceChar else -1
413 startpos = lastMatch.end()
416 ret = (begin,lastMatch.end()-1)
422 raise ValueError("Unmatched " + braceRegex[openingBraceChar] + " at " + text[startpos-15:startpos+15])
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
446 ('void tutorial(){\\n content of tutorial\\n}', [], '')
447 >>> split('''void tutorial(){
448 ... content of tutorial
450 ... void helper(arguments = values){
452 ... content spans lines
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
458 ... content of tutorial
460 ... void helper(arguments = values){
462 ... content spans lines
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
468 ... // This is a multiline
469 ... // description of the
470 ... // helper function
471 ... void helper(arguments = values){
473 ... content spans lines
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}'], '')
477 functionRe = re.compile(r"[^(/;]*?\s+[*&]*([\w:]+)\s*\(", flags = re.DOTALL | re.MULTILINE)
478 tailRe = re.compile(r"}\s*;?", flags = re.DOTALL | re.MULTILINE)
483 for curlyBegin,curlyEnd in findBracedBlock(text, 0, "{"):
484 functionMatches = [match for match in functionRe.finditer(text, searchStart, curlyBegin)]
486 tailMatch = tailRe.match(text, curlyEnd)
488 curlyEnd = tailMatch.end()
490 sys.stderr.write("Failed match on " + text[curlyEnd:curlyEnd+10])
492 if not functionMatches:
493 definitions.append("%%cpp -d\n" + text[searchStart:curlyEnd])
494 searchStart = curlyEnd+1
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 '{}'")
505 beforeFuncPos = findStuffBeforeFunc(text, searchStart, functionMatch.start(1))
506 stuffBeforeFunc = text[searchStart:beforeFuncPos]
507 funcString = text[beforeFuncPos:curlyEnd]
508 searchStart = curlyEnd
511 for line in stuffBeforeFunc.splitlines():
512 if line.strip().startswith("//"):
513 commentLines.append(line)
515 definitions.append(line)
517 if tutName == functionMatch.group(1).strip(): # if the name of the function is that of the macro
520 helpers.append((funcString, functionMatch.group(1).strip(), commentLines))
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"
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:"
536 if functionName != "main": # remove void main function
537 newHelpers.append("\n# <markdowncell>\n " + helperDescription + " \n# <codecell>\n%%cpp -d\n" + helper)
539 return main, newHelpers, "\n".join(definitions)
543def processmain(text):
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
554 ('void function(){\\n content of function\\n spanning several\\n lines\\n}', '')
555 >>> processmain('''void function(arguments = values){
556 ... content of function
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)
566 ... content of function
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
577 ('TCanvas function(){\\n content of function\\n spanning several \\n lines\\n return c1\\n}', '')
583 argumentsre = re.compile(r'(?<=\().*?(?=\))', flags = re.DOTALL | re.MULTILINE)
584 arguments = argumentsre.search(text)
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"
594 return text, argumentsCell
596# now define text transformers
710def mainfunction(text):
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.
718 # Modify text from macros to suit a notebook
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
727 main = argumentsCell + main
729 rest = cppComments(rest) # Convert top level code comments to Markdown cells
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
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)
737 text = "# <codecell>\n" + rest
739 for helper in helpers:
742 text += ("\n# <codecell>\n" + main)
743 except Exception as e:
744 sys.stderr.write("Failed to convert C++ to notebook\n" + str(e))
747 if extension == "py":
748 text = pythonMainFunction(text)
749 text = pythonComments(text) # Convert comments into Markdown cells
752 # Perform last minute fixes to the notebook, used for specific fixes needed by some tutorials
755 # Change to standard Markdown
756 newDescription = changeMarkdown(description)
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)
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:
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()"
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()"
776 # Create a notebook from the working text
777 nbook = v3.reads_py(text)
778 nbook = v4.upgrade(nbook) # Upgrade v3 to v4
780 # Load notebook string into json format, essentially creating a dictionary
781 json_data = json.loads(v4.writes(nbook))
783 # add the corresponding metadata
784 if extension == "py":
785 json_data['metadata'] = {
787 "display_name": "Python " + str(sys.version_info[0]),
788 "language": "python",
789 "name": "python" + str(sys.version_info[0])
796 "file_extension": ".py",
797 "mimetype": "text/x-python",
799 "nbconvert_exporter": "python",
800 "pygments_lexer": "ipython2",
805 json_data['metadata'] = {
807 "display_name": "ROOT C++",
812 "codemirror_mode": "text/x-c++src",
813 "file_extension": ".C",
814 "mimetype": " text/x-c++src",
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)
823 timeout = findTimeout()
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])
828 while r != 0 and retry < 2:
829 # Work around glitches in NB conversion
831 r = subprocess.call(["jupyter", "nbconvert", "--ExecutePreprocessor.timeout={}".format(timeout*2), "--to=notebook", "--execute", outPathName])
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))
840 subprocess.call(["jupyter", "trust", os.path.join(outdir, outnameconverted)])
841 # Only remove notebook without output if nbconvert succeeds
842 os.remove(outPathName)