#!/usr/bin/env python """Build swig imput file from xml encoded header files (see gccxml).""" __author__ = "Randall J. Radmer" __version__ = "1.0" import sys, os import time import getopt import re import xml.etree.ElementTree as etree from distutils.version import LooseVersion import copy try: from html.parser import HTMLParser except ImportError: # python 2 from HTMLParser import HTMLParser INDENT = " " docTags = {'emphasis':'i', 'bold':'b', 'itemizedlist':'ul', 'listitem':'li', 'preformatted':'pre', 'computeroutput':'tt', 'subscript':'sub'} def striphtmltags(s): """Strip a couple html tags used inside docstrings in the C++ source to produce something more easily read as plain text. """ class ConvertLists(HTMLParser): def reset(self): HTMLParser.reset(self) self.out = [] def handle_starttag(self, tag, attrs): if tag == 'li': self.out.append('\n - ') def handle_data(self, data): self.out.append(data.strip()) convertlists = ConvertLists() def replace_ul_tags(m): a, b = m.span() sub = s[a:b] convertlists.reset() convertlists.feed(sub) return '\n%s\n\n' % ''.join(convertlists.out) s = s.replace('', '_').replace('', '_') s = s.replace('', '*').replace('', '*') s = re.sub('\s*(\s*)', replace_ul_tags, s, flags=re.MULTILINE | re.DOTALL) return s def trimToSingleSpace(text): if text is None or len(text) == 0: return "" t = text.strip() if len(t) == 0: return t if text[0].isspace(): t = " %s" % t if text[-1].isspace(): t = "%s " % t return t def getNodeText(node): if node.text is not None: s = node.text else: s = "" for n in node: if n.tag == "para": s = "%s%s\n\n" % (s, getNodeText(n)) elif n.tag == "ref": s = "%s%s" % (s, getNodeText(n)) else: if n.tag in docTags: tag = docTags[n.tag] s = "%s<%s>%s" % (s, tag, getNodeText(n), tag) if n.tail is not None: s = "%s%s" % (s, n.tail) return s def getText(subNodePath, node): s = "" for n in node.findall(subNodePath): s = "%s%s" % (s, trimToSingleSpace(getNodeText(n))) if n.tag == "para": s = "%s\n\n" % s return s.strip() OPENMM_RE_PATTERN=re.compile("(.*)OpenMM:[a-zA-Z:]*:(.*)") def stripOpenmmPrefix(name, rePattern=OPENMM_RE_PATTERN): try: m=rePattern.search(name) rValue = "%s%s" % m.group(1,2) rValue.strip() return rValue except: return name def findNodes(parent, path, **args): nodes = [] for node in parent.findall(path): match = True for arg in args: if arg not in node.attrib or node.attrib[arg] != args[arg]: match = False if match: nodes.append(node) return nodes def getClassMethodList(classNode, skipMethods): className = getText("compoundname", classNode) shortClassName=stripOpenmmPrefix(className) methodList=[] for section in findNodes(classNode, "sectiondef", kind="public-static-func")+findNodes(classNode, "sectiondef", kind="public-func"): for memberNode in findNodes(section, "memberdef", kind="function", prot="public"): methDefinition = getText("definition", memberNode) shortMethDefinition=stripOpenmmPrefix(methDefinition) methName=shortMethDefinition.split()[-1] if (shortClassName, methName) in skipMethods: continue numParams=len(findNodes(memberNode, 'param')) if (shortClassName, methName, numParams) in skipMethods: continue for catchString in ['Factory', 'Impl', 'Info', 'Kernel']: if shortClassName.endswith(catchString): sys.stderr.write("Warning: Including class %s\n" % shortClassName) continue if (shortClassName, methName) in skipMethods: continue # set template info templateType = getText("templateparamlist/param/type", memberNode) templateName = getText("templateparamlist/param/declname", memberNode) methodList.append( (shortClassName, memberNode, shortMethDefinition, methName, shortClassName==methName, '~'+shortClassName==methName, templateType, templateName ) ) return methodList class SwigInputBuilder: def __init__(self, inputDirname, configFilename, outputFilename=None, docstringFilename=None, pythonprependFilename=None, pythonappendFilename=None, skipAdditionalMethods=[], SWIG_VERSION='3.0.2'): self.nodeByID={} self.SWIG_COMPACT_ARGUMENTS = LooseVersion(SWIG_VERSION) < LooseVersion('3.0.5') self.configModule = __import__(os.path.splitext(configFilename)[0]) self.skipMethods=self.configModule.SKIP_METHODS[:] for skipMethod in skipAdditionalMethods: items=skipMethod.split('::') if len(items)==3: items[2]=int(items[2]) self.skipMethods.append(tuple(items)) # Read all the XML files and merge them into a single document. self.doc = etree.ElementTree(etree.Element('root')) for file in os.listdir(inputDirname): if file.lower().endswith('xml'): root = etree.parse(os.path.join(inputDirname, file)).getroot() for node in root: self.doc.getroot().append(node) if outputFilename: self.fOut = open(outputFilename, 'w') else: self.fOut = sys.stdout if docstringFilename: self.fOutDocstring = open(docstringFilename, 'w') else: self.fOutDocstring = None if pythonprependFilename: self.fOutPythonprepend = open(pythonprependFilename, 'w') else: self.fOutPythonprepend = None if pythonappendFilename: self.fOutPythonappend = open(pythonappendFilename, 'w') else: self.fOutPythonappend = None self._enumByClassname={} self._orderedClassNodes=self._buildOrderedClassNodes() def _getNodeByID(self, id): if id not in self.nodeByID: for node in findNodes(self.doc.getroot(), "compounddef", id=id): self.nodeByID[id] = node return self.nodeByID[id] def _buildOrderedClassNodes(self): orderedClassNodes=[] for node in findNodes(self.doc.getroot(), "compounddef", kind="class", prot="public"): self._findBaseNodes(node, orderedClassNodes) return orderedClassNodes def _findBaseNodes(self, node, excludedClassNodes=[]): if node in excludedClassNodes: return nodeName = getText("compoundname", node) if (nodeName.split("::")[-1],) in self.skipMethods: return for baseNodePnt in findNodes(node, "basecompoundref", prot="public"): if "refid" in baseNodePnt.attrib: baseNodeID = baseNodePnt.attrib["refid"] baseNode = self._getNodeByID(baseNodeID) self._findBaseNodes(baseNode, excludedClassNodes) excludedClassNodes.append(node) def writeFactories(self): self.fOut.write("\n/* Declare factories */\n\n") forceSubclassList = [] integratorSubclassList = [] for classNode in findNodes(self.doc.getroot(), "compounddef", kind="class", prot="public"): className = getText("compoundname", classNode) shortClassName=stripOpenmmPrefix(className) if (className.split("::")[-1],) in self.skipMethods: continue for baseNodePnt in findNodes(classNode, "basecompoundref", prot="public"): if "refid" in baseNodePnt.attrib: baseNodeID=baseNodePnt.attrib["refid"] baseNode=self._getNodeByID(baseNodeID) baseName = getText("compoundname", baseNode) if baseName == 'OpenMM::Force': forceSubclassList.append(shortClassName) elif baseName == 'OpenMM::Integrator': integratorSubclassList.append(shortClassName) self.fOut.write("%factory(OpenMM::Force& OpenMM::System::getForce") for name in sorted(forceSubclassList): self.fOut.write(",\n OpenMM::%s" % name) self.fOut.write(");\n\n") self.fOut.write("%factory(OpenMM::Force* OpenMM::Force::__copy__") for name in sorted(forceSubclassList): self.fOut.write(",\n OpenMM::%s" % name) self.fOut.write(");\n\n") self.fOut.write("%factory(OpenMM::Force* OpenMM_XmlSerializer__deserializeForce") for name in sorted(forceSubclassList): self.fOut.write(",\n OpenMM::%s" % name) self.fOut.write(");\n\n") self.fOut.write("%factory(OpenMM::Integrator* OpenMM::Integrator::__copy__") for name in sorted(integratorSubclassList): self.fOut.write(",\n OpenMM::%s" % name) self.fOut.write(");\n\n") self.fOut.write("%factory(OpenMM::Integrator* OpenMM_XmlSerializer__deserializeIntegrator") for name in sorted(integratorSubclassList): self.fOut.write(",\n OpenMM::%s" % name) self.fOut.write(");\n\n") self.fOut.write("%factory(OpenMM::Integrator& OpenMM::Context::getIntegrator") for name in sorted(integratorSubclassList): self.fOut.write(",\n OpenMM::%s" % name) self.fOut.write(");\n\n") self.fOut.write("%factory(OpenMM::Integrator& OpenMM::CompoundIntegrator::getIntegrator") for name in sorted(integratorSubclassList): self.fOut.write(",\n OpenMM::%s" % name) self.fOut.write(");\n\n") self.fOut.write("%factory(OpenMM::VirtualSite& OpenMM::System::getVirtualSite, OpenMM::TwoParticleAverageSite, OpenMM::ThreeParticleAverageSite, OpenMM::OutOfPlaneSite);\n\n") self.fOut.write("\n") def writeGlobalConstants(self): self.fOut.write("/* Global Constants */\n\n") node = next((x for x in findNodes(self.doc.getroot(), "compounddef", kind="namespace") if x.findtext("compoundname") == "OpenMM")) for section in findNodes(node, "sectiondef", kind="var"): for memberNode in findNodes(section, "memberdef", kind="variable", mutable="no", prot="public", static="yes"): vDef = stripOpenmmPrefix(getText("definition", memberNode)) iDef = getText("initializer", memberNode) if iDef.startswith("="): iDef = iDef[1:] self.fOut.write("static %s = %s;\n" % (vDef, iDef)) self.fOut.write("\n") def writeForwardDeclarations(self): self.fOut.write("\n/* Forward Declarations */\n\n") for classNode in self._orderedClassNodes: hasConstructor=False methodList=getClassMethodList(classNode, self.skipMethods) for items in methodList: (shortClassName, memberNode, shortMethDefinition, methName, isConstructors, isDestructor, templateType, templateName) = items if isConstructors: hasConstructor=True className = stripOpenmmPrefix(getText("compoundname", classNode)) # If has a constructor then tell swig tell to make a copy method if hasConstructor: self.fOut.write("%%copyctor %s ;\n" % className) self.fOut.write("class %s ;\n" % className) self.fOut.write("\n") def writeClassDeclarations(self): self.fOut.write("\n/* Class Declarations */\n\n") for classNode in self._orderedClassNodes: className = stripOpenmmPrefix(getText("compoundname", classNode)) if self.fOutDocstring: dNode = classNode.find('detaileddescription') if dNode is not None: docstring = getNodeText(dNode).strip().replace('"', '\\"') docstring = striphtmltags(docstring) self.fOutDocstring.write('%%feature("docstring") %s "%s";\n' % (className, docstring)) self.fOut.write("class %s" % className) if className in self.configModule.MISSING_BASE_CLASSES: self.fOut.write(" : public %s" % self.configModule.MISSING_BASE_CLASSES[className]) for baseNodePnt in findNodes(classNode, "basecompoundref", prot="public"): if "refid" in baseNodePnt.attrib: baseName = stripOpenmmPrefix(getText(".", baseNodePnt)) self.fOut.write(" : public %s" % baseName) self.fOut.write(" {\n") self.fOut.write("public:\n") self.writeEnumerations(classNode) self.writeMethods(classNode) self.fOut.write("};\n\n") self.fOut.write("\n") def writeEnumerations(self, classNode): enumNodes = [] for section in findNodes(classNode, "sectiondef", kind="public-type"): for node in findNodes(section, "memberdef", kind="enum", prot="public"): enumNodes.append(node) for enumNode in enumNodes: className = getText("compoundname", classNode) shortClassName=stripOpenmmPrefix(className) enumName = getText("name", enumNode) try: self._enumByClassname[shortClassName].append(enumName) except KeyError: self._enumByClassname[shortClassName]=[enumName] self.fOut.write("%senum %s {" % (INDENT, enumName)) argSep="\n" for valueNode in findNodes(enumNode, "enumvalue", prot="public"): vName = getText("name", valueNode) vInit = getText("initializer", valueNode) if vInit.startswith("="): vInit = vInit[1:] self.fOut.write("%s%s%s = %s" % (argSep, 2*INDENT, vName, vInit)) argSep=",\n" self.fOut.write("\n%s};\n" % INDENT) if len(enumNodes)>0: self.fOut.write("\n") def writeMethods(self, classNode): methodList=getClassMethodList(classNode, self.skipMethods) #write only Constructors for items in methodList: (shortClassName, memberNode, shortMethDefinition, methName, isConstructors, isDestructor, templateType, templateName) = items if isConstructors: mArgsstring = getText("argsstring", memberNode) try: pExceptions = " %s" % getText('exceptions', memberNode) except IndexError: pExceptions = "" self.fOut.write("%s%s%s%s;\n" % (INDENT, shortMethDefinition, mArgsstring, pExceptions)) #write only Destructors for items in methodList: (shortClassName, memberNode, shortMethDefinition, methName, isConstructors, isDestructor, templateType, templateName) = items if isDestructor: mArgsstring = getText("argsstring", memberNode) try: pExceptions = " %s" % getText('exceptions', memberNode) except IndexError: pExceptions = "" self.fOut.write("%s%s%s%s;\n" % (INDENT, shortMethDefinition, mArgsstring, pExceptions)) #write only non Constructor and Destructor methods and python mods self.fOut.write("\n") for items in methodList: clearOutput="" (shortClassName, memberNode, shortMethDefinition, methName, isConstructors, isDestructor, templateType, templateName) = items if isConstructors or isDestructor: continue key = (shortClassName, methName) if key in self.configModule.DOC_STRINGS: self.fOut.write('%%feature("autodoc", "%s") %s;\n' % (self.configModule.DOC_STRINGS[key], methName)) paramList=findNodes(memberNode, 'param') for pNode in paramList: try: pType = getText('type', pNode) except IndexError: pType = getText('type/ref', pNode) pName = getText('declname', pNode) key = (shortClassName, methName, pName) if pType.find('&')>=0 and \ 'const' not in pType.split(): if key not in self.configModule.NO_OUTPUT_ARGS: eType = pType.split()[0] if shortClassName in self._enumByClassname and \ eType in self._enumByClassname[shortClassName]: simpleType = re.sub(eType, 'int', pType) else: simpleType = pType self.fOut.write("%s%%apply %s OUTPUT { %s %s };\n" % (INDENT, simpleType, pType, pName)) clearOutput = "%s%s%%clear %s %s;\n" \ % (clearOutput, INDENT, pType, pName) mArgsstring = getText("argsstring", memberNode) try: pExceptions = " %s" % getText('exceptions', memberNode) except IndexError: pExceptions = "" if memberNode.attrib["virt"].strip()!='non-virtual': if 'virtual' not in shortMethDefinition.split(): shortMethDefinition="virtual %s" % shortMethDefinition if( len(templateType) > 0 ): self.fOut.write("%stemplate<%s %s> %s%s%s;\n" % (INDENT, templateType, templateName, shortMethDefinition, mArgsstring, pExceptions)) else: self.fOut.write("%s%s%s%s;\n" % (INDENT, shortMethDefinition, mArgsstring, pExceptions)) if clearOutput: self.fOut.write(clearOutput) #write python mod blocks for items in methodList: (shortClassName, memberNode, shortMethDefinition, methName, isConstructors, isDestructor, templateType, templateName) = items paramList=findNodes(memberNode, 'param') #write pythonprepend blocks mArgsstring = getText("argsstring", memberNode) if self.fOutPythonprepend and \ len(paramList) and \ mArgsstring.find('=0')<0: key=(shortClassName, methName) if key in self.configModule.STEAL_OWNERSHIP: for argNum in self.configModule.STEAL_OWNERSHIP[key]: if self.SWIG_COMPACT_ARGUMENTS: argName = 'args[%s]' % argNum else: argName = getText('declname', paramList[argNum]) text = ''' %pythonprepend OpenMM::{shortClassName}::{methName}{mArgsstring} %{{ if not {argName}.thisown: s = ("the %s object does not own its corresponding OpenMM object" % self.__class__.__name__) raise Exception(s) %}}'''.format(argName=argName, shortClassName=shortClassName, methName=methName, mArgsstring=mArgsstring) self.fOutPythonprepend.write(text) #write pythonappend blocks if self.fOutPythonappend \ and mArgsstring.find('=0')<0: key=(shortClassName, methName) #print "key %s %s \n" % (shortClassName, methName) addText='' returnType = getText("type", memberNode) if key in self.configModule.UNITS: valueUnits=self.configModule.UNITS[key] elif ("*", methName) in self.configModule.UNITS: valueUnits=self.configModule.UNITS[("*", methName)] elif methName.startswith('get') and returnType not in ('void', 'int', 'bool', 'std::string', 'const std::string &'): s = 'do not know how to add units to %s %s::%s' \ % (returnType, shortClassName, methName) raise Exception(s) else: valueUnits=[None, ()] index=0 if valueUnits[0] is not None: sys.stdout.write("%s.%s() returns %s\n" % (shortClassName, methName, valueUnits[0])) if len(valueUnits[1])>0: addText = "%s%sval[%d]=unit.Quantity(val[%d], %s)\n" \ % (addText, INDENT, index, index, valueUnits[0]) index+=1 else: addText = "%s%sval=unit.Quantity(val, %s)\n" \ % (addText, INDENT, valueUnits[0]) for vUnit in valueUnits[1]: if vUnit is not None: addText = "%s%sval[%s]=unit.Quantity(val[%s], %s)\n" \ % (addText, INDENT, index, index, vUnit) index+=1 if key in self.configModule.STEAL_OWNERSHIP: for argNum in self.configModule.STEAL_OWNERSHIP[key]: if self.SWIG_COMPACT_ARGUMENTS: argName = 'args[%s]' % argNum else: argName = getText('declname', paramList[argNum]) addText = "%s%s%s.thisown=0\n" \ % (addText, INDENT, argName) if addText: self.fOutPythonappend.write("%pythonappend") self.fOutPythonappend.write(" OpenMM::%s::%s(" % key) sepChar='' outputIndex=0 for pNode in paramList: try: pType = getText('type', pNode) except IndexError: pType = getText('type/ref', pNode) pName = getText('declname', pNode) self.fOutPythonappend.write("%s%s %s" % (sepChar, pType, pName)) sepChar=', ' if pType.find('&')>=0 and \ 'const' not in pType.split() and \ key not in self.configModule.NO_OUTPUT_ARGS and \ len(valueUnits[1])>0: try: unitType=valueUnits[1][outputIndex] except IndexError: s = "missing unit type for %s.%s() arg named %s" \ % (shortClassName, methName, pName) raise Exception(s) sys.stdout.write("%s.%s() returns %s as %s\n" % (shortClassName, methName, pName, unitType)) outputIndex+=1 if memberNode.attrib["const"]=="yes": constString=' const' else: constString='' self.fOutPythonappend.write(")%s %%{\n" % constString) self.fOutPythonappend.write(addText) self.fOutPythonappend.write("%}\n\n") #print "Done python mod blocks\n" #write Docstring info for items in methodList: (shortClassName, memberNode, shortMethDefinition, methName, isConstructors, isDestructor, templateType, templateName ) = items if self.fOutDocstring: for dNode in findNodes(memberNode, 'detaileddescription'): dString="" try: description=getText('para', dNode) description.strip() if description: dString=description except IndexError: pass params = findNodes(dNode, 'para/parameterlist/parameteritem') if len(params) > 0: dString="%s\n Parameters:" % dString for pNode in params: argName = getText('parameternamelist/parametername', pNode) argDoc = getText('parameterdescription/para', pNode) dString="%s\n - %s %s" % (dString, argName, argDoc) dString.strip() if dString: dString=re.sub(r'([^\\])"', r'\g<1>\"', dString) s = '%%feature("docstring") OpenMM::%s::%s "%s";' \ % (shortClassName, methName, dString) self.fOutDocstring.write("%s\n" % s) self.fOutDocstring.write("\n\n") #print "Done write Docstring info\n" def writeSwigFile(self): self.fOut.write("/* Swig input file,\n") self.fOut.write("%sgenerated by %s on %s\n*/\n\n\n" % (INDENT, sys.argv[0], time.asctime())) self.fOut.write("\nnamespace OpenMM {\n\n") self.writeFactories() self.writeGlobalConstants() self.writeForwardDeclarations() self.writeClassDeclarations() self.fOut.write("\n} // namespace OpenMM\n\n") def parseCommandLine(): opts, args_proper = getopt.getopt(sys.argv[1:], 'hi:c:o:d:a:z:s:v:') inputDirname = None configFilename = None outputFilename = "" docstringFilename = "" pythonprependFilename = "" pythonappendFilename = "" skipAdditionalMethods = [] swigVersion = '3.0.2' for option, parameter in opts: if option=='-h': usageError() if option=='-i': inputDirname = parameter if option=='-c': configFilename=parameter if option=='-o': outputFilename = parameter if option=='-d': docstringFilename = parameter if option=='-a': pythonprependFilename=parameter if option=='-z': pythonappendFilename=parameter if option=='-s': skipAdditionalMethods.append(parameter) if option=='-v': swigVersion = parameter if not inputDirname: usageError() if not configFilename: usageError() return (args_proper, inputDirname, configFilename, outputFilename, docstringFilename, pythonprependFilename, pythonappendFilename, skipAdditionalMethods, swigVersion) def main(): (args_proper, inputDirname, configFilename, outputFilename, docstringFilename, pythonprependFilename, pythonappendFilename, skipAdditionalMethods, swigVersion) = parseCommandLine() sBuilder = SwigInputBuilder(inputDirname, configFilename, outputFilename, docstringFilename, pythonprependFilename, pythonappendFilename, skipAdditionalMethods, swigVersion) #print "Calling writeSwigFile\n" sBuilder.writeSwigFile() #print "Done writeSwigFile\n" return def usageError(): sys.stdout.write('usage: %s -i xmlHeadersFilename \\\n' \ % os.path.basename(sys.argv[0])) sys.stdout.write(' %s -c inputConfigFilename\n' \ % (' '*len(os.path.basename(sys.argv[0])))) sys.stdout.write(' %s[-o swigInputDirname]\n' \ % (' '*len(os.path.basename(sys.argv[0])))) sys.stdout.write(' %s[-d docstringFilename]\n' \ % (' '*len(os.path.basename(sys.argv[0])))) sys.stdout.write(' %s[-a pythonprependFilename]\n' \ % (' '*len(os.path.basename(sys.argv[0])))) sys.stdout.write(' %s[-z pythonappendFilename]\n' \ % (' '*len(os.path.basename(sys.argv[0])))) sys.stdout.write(' %s[-s skippedClasses]\n' \ % (' '*len(os.path.basename(sys.argv[0])))) sys.stdout.write(' %s[-v swigVersion]\n' \ % (' '*len(os.path.basename(sys.argv[0])))) sys.exit(1) if __name__=='__main__': main()