diff --git a/etgtools/extractors.py b/etgtools/extractors.py index af1e9b3c..70b37be8 100644 --- a/etgtools/extractors.py +++ b/etgtools/extractors.py @@ -17,7 +17,7 @@ import os import pprint import xml.etree.ElementTree as et -from tweaker_tools import removeWxPrefix +from tweaker_tools import removeWxPrefix, magicMethods #--------------------------------------------------------------------------- # These classes simply hold various bits of information about the classes, @@ -339,28 +339,48 @@ class FunctionDef(BaseDef): if self.type and self.type != 'void': returns.append(_cleanName(self.type)) - for param in self.items: - assert isinstance(param, ParamDef) - if param.ignored: - continue - if param.arraySize: - continue - s = param.pyName or param.name - if param.out: - returns.append(s) - else: - if param.inOut: - returns.append(s) - if param.default: - default = param.default - defValueMap = { 'true': 'True', - 'false': 'False', - 'NULL': 'None', } + defValueMap = { 'true': 'True', + 'false': 'False', + 'NULL': 'None', } + if isinstance(self, CppMethodDef): + # rip appart the argsString instead of using the (empty) list of parameters + lastP = self.argsString.rfind(')') + args = self.argsString[:lastP].strip('()').split(',') + for arg in args: + if not arg: + continue + # is there a default value? + default = '' + if '=' in arg: + default = arg.split('=')[1] + arg = arg.split('=')[0] if default in defValueMap: default = defValueMap.get(default) - - s += '=' + '|'.join([_cleanName(x) for x in default.split('|')]) - params.append(s) + # now grab just the last word, it should be the variable name + arg = arg.split()[-1] + if default: + arg += '=' + default + params.append(arg) + else: + for param in self.items: + assert isinstance(param, ParamDef) + if param.ignored: + continue + if param.arraySize: + continue + s = param.pyName or param.name + if param.out: + returns.append(s) + else: + if param.inOut: + returns.append(s) + if param.default: + default = param.default + if default in defValueMap: + default = defValueMap.get(default) + + s += '=' + '|'.join([_cleanName(x) for x in default.split('|')]) + params.append(s) self.pyArgsString = '(' + ', '.join(params) + ')' if len(returns) == 1: @@ -383,6 +403,8 @@ class FunctionDef(BaseDef): f.makePyArgsString() sig = f.pyName or removeWxPrefix(f.name) + if sig in magicMethods: + sig = magicMethods[sig] sig += f.pyArgsString sigs.append(sig) return sigs @@ -572,7 +594,7 @@ class ClassDef(BaseDef): def countNonDefaultArgs(m): count = 0 for p in m.items: - if not p.default: + if not p.default and not p.ignored: count += 1 return count diff --git a/etgtools/generators.py b/etgtools/generators.py index 08bc630d..87862997 100644 --- a/etgtools/generators.py +++ b/etgtools/generators.py @@ -30,4 +30,47 @@ class StubbedDocsGenerator(DocsGeneratorBase): def generate(self, module): pass -#--------------------------------------------------------------------------- + +#--------------------------------------------------------------------------- +# helpers + +def nci(text, numSpaces=0, stripLeading=True): + """ + Normalize Code Indents + + First use the count of leading spaces on the first line and remove that + many spaces from the front of all lines, and then indent each line by + adding numSpaces spaces. This is used so we can convert the arbitrary + indents that might be used by the tweaker code into what is expected for + the context we are generating for. + """ + def _getLeadingSpaceCount(line): + count = 0 + for c in line: + assert c != '\t', "Use spaces for indent, not tabs" + if c != ' ': + break + count += 1 + return count + + def _allSpaces(text): + for c in text: + if c != ' ': + return False + return True + + + lines = text.rstrip().split('\n') + if stripLeading: + numStrip = _getLeadingSpaceCount(lines[0]) + else: + numStrip = 0 + + for idx, line in enumerate(lines): + assert _allSpaces(line[:numStrip]), "Indentation inconsistent with first line" + lines[idx] = ' '*numSpaces + line[numStrip:] + + newText = '\n'.join(lines) + '\n' + return newText + +#--------------------------------------------------------------------------- diff --git a/etgtools/pi_generator.py b/etgtools/pi_generator.py new file mode 100644 index 00000000..79fa0428 --- /dev/null +++ b/etgtools/pi_generator.py @@ -0,0 +1,400 @@ +#--------------------------------------------------------------------------- +# Name: etgtools/pi_generator.py +# Author: Robin Dunn +# +# Created: 18-Oct-2011 +# Copyright: (c) 2011 by Total Control Software +# License: wxWindows License +#--------------------------------------------------------------------------- + +""" +This generator will create "Python Interface" files, which define a skeleton +verison of the classes, functions, attributes, docstrings, etc. as Python +code. This is useful for enabling some introspcetion of things located in +extension modules where there is less information available for +introspection. The .pi files are used by WingIDE for assisting with code +completion, displaying docstrings in the source assistant panel, etc. +""" + +import sys, os, re +import extractors +import generators +from generators import nci +from tweaker_tools import removeWxPrefix, magicMethods +from cStringIO import StringIO + + +phoenixRoot = os.path.abspath(os.path.split(__file__)[0]+'/..') +header = """\ +#--------------------------------------------------------------------------- +# This file is generated by wxPython's PI generator. Do not edit by hand. +# +# (The *.pi files are used by WingIDE to provide more information than it is +# able to glean from introspection of extension types and methods.) +# +# Copyright: (c) 2011 by Total Control Software +# License: wxWindows License +#--------------------------------------------------------------------------- + +""" +#--------------------------------------------------------------------------- + +class PiWrapperGenerator(generators.WrapperGeneratorBase): + + def generate(self, module, destFile=None): + stream = StringIO() + + # generate SIP code from the module and its objects + self.generateModule(module, stream) + + # Write the contents of the stream to the destination file + if not destFile: + name = module.module + '.pi' + if name.startswith('_'): + name = name[1:] + destFile = os.path.join(phoenixRoot, 'wx', name) + + if not os.path.exists(destFile): + # create the file and write the header + f = file(destFile, 'wt') + f.write(header) + f.close() + + self.writeSection(destFile, module.name, stream.getvalue()) + + + def writeSection(self, destFile, sectionName, sectionText): + """ + Read all the lines from destFile, remove those currently between + begin/end markers for sectionName (if any), and write the lines back + to the file with the new text in sectionText. + """ + sectionBeginLine = -1 + sectionEndLine = -1 + sectionBeginMarker = '#-- begin-' + sectionName + sectionEndMarker = '#-- end-' + sectionName + + lines = file(destFile, 'rt').readlines() + for idx, line in enumerate(lines): + if line.startswith(sectionBeginMarker): + sectionBeginLine = idx + if line.startswith(sectionEndMarker): + sectionEndLine = idx + + if sectionBeginLine == -1: + # not there already, add to the end + lines.append(sectionBeginMarker + '\n') + lines.append(sectionText) + lines.append(sectionEndMarker + '\n') + else: + # replace the existing lines + lines[sectionBeginLine+1:sectionEndLine] = [sectionText] + + f = file(destFile, 'wt') + f.writelines(lines) + #f.write('\n') + f.close() + + + + #----------------------------------------------------------------------- + def generateModule(self, module, stream): + """ + Generate code for each of the top-level items in the module. + """ + assert isinstance(module, extractors.ModuleDef) + + methodMap = { + extractors.ClassDef : self.generateClass, + extractors.DefineDef : self.generateDefine, + extractors.FunctionDef : self.generateFunction, + extractors.EnumDef : self.generateEnum, + extractors.GlobalVarDef : self.generateGlobalVar, + extractors.TypedefDef : self.generateTypedef, + extractors.WigCode : self.generateWigCode, + extractors.PyCodeDef : self.generatePyCode, + extractors.CppMethodDef : self.generateCppMethod, + extractors.CppMethodDef_sip : self.generateCppMethod_sip, + } + + for item in module: + if item.ignored: + continue + function = methodMap[item.__class__] + function(item, stream) + + + #----------------------------------------------------------------------- + def generateEnum(self, enum, stream, indent=''): + assert isinstance(enum, extractors.EnumDef) + if enum.ignored: + return + for v in enum.items: + if v.ignored: + continue + name = v.pyName or v.name + stream.write('%s%s = 0\n' % (indent, name)) + + #----------------------------------------------------------------------- + def generateGlobalVar(self, globalVar, stream): + assert isinstance(globalVar, extractors.GlobalVarDef) + if globalVar.ignored: + return + name = globalVar.pyName or globalVar.name + if guessTypeInt(globalVar): + valTyp = '0' + elif guessTypeFloat(globalVar): + valTyp = '0.0' + elif guessTypeStr(globalVar): + valTyp = '""' + else: + valTyp = removeWxPrefix(globalVar.type) + '()' + + stream.write('%s = %s\n' % (name, valTyp)) + + #----------------------------------------------------------------------- + def generateDefine(self, define, stream): + assert isinstance(define, extractors.DefineDef) + if define.ignored: + return + # we're assuming that all #defines that are not ignored are integer values + stream.write('%s = 0\n' % (define.pyName or define.name)) + + #----------------------------------------------------------------------- + def generateTypedef(self, typedef, stream): + assert isinstance(typedef, extractors.TypedefDef) + if typedef.ignored: + return + # write nothing for this one + + #----------------------------------------------------------------------- + def generateWigCode(self, wig, stream, indent=''): + assert isinstance(wig, extractors.WigCode) + # write nothing for this one + + + #----------------------------------------------------------------------- + def generatePyCode(self, pc, stream, indent=''): + assert isinstance(pc, extractors.PyCodeDef) + stream.write('\n') + stream.write(nci(pc.code, len(indent))) + + #----------------------------------------------------------------------- + def generateFunction(self, function, stream): + assert isinstance(function, extractors.FunctionDef) + stream.write('\ndef %s' % function.pyName) + if function.overloads: + stream.write('(*args, **kw)') + else: + stream.write(function.pyArgsString) + stream.write(':\n') + stream.write(' """\n') + stream.write(nci(function.pyDocstring, 4)) + stream.write(' """\n') + + def generateParameters(self, parameters, stream, indent): + def _lastParameter(idx): + if idx == len(parameters)-1: + return True + for i in range(idx+1, len(parameters)): + if not parameters[i].ignored: + return False + return True + + for idx, param in enumerate(parameters): + if param.ignored: + continue + stream.write(param.name) + if param.default: + stream.write('=%s' % param.default) + if not _lastParameter(idx): + stream.write(', ') + + + #----------------------------------------------------------------------- + def generateClass(self, klass, stream, indent=''): + assert isinstance(klass, extractors.ClassDef) + if klass.ignored: + return + + # class declaration + klassName = klass.pyName or klass.name + stream.write('\n%sclass %s' % (indent, klassName)) + if klass.bases: + stream.write('(') + bases = [removeWxPrefix(b) for b in klass.bases] + stream.write(', '.join(bases)) + stream.write(')') + stream.write(':\n') + indent2 = indent + ' '*4 + + # docstring + stream.write('%s"""\n' % indent2) + stream.write(nci(klass.pyDocstring, len(indent2))) + stream.write('%s"""\n' % indent2) + + # generate nested classes + for item in klass.innerclasses: + self.generateClass(item, stream, indent2) + + # Split the items into public and protected groups + ctors = [i for i in klass if + isinstance(i, extractors.MethodDef) and + i.protection == 'public' and (i.isCtor or i.isDtor)] + public = [i for i in klass if i.protection == 'public' and i not in ctors] + protected = [i for i in klass if i.protection == 'protected'] + + dispatch = { + extractors.MemberVarDef : self.generateMemberVar, + extractors.PropertyDef : self.generateProperty, + extractors.PyPropertyDef : self.generatePyProperty, + extractors.MethodDef : self.generateMethod, + extractors.EnumDef : self.generateEnum, + extractors.CppMethodDef : self.generateCppMethod, + extractors.CppMethodDef_sip : self.generateCppMethod_sip, + extractors.PyMethodDef : self.generatePyMethod, + extractors.PyCodeDef : self.generatePyCode, + extractors.WigCode : self.generateWigCode, + } + + for item in ctors: + if item.isCtor: + self.generateMethod(item, stream, indent2, + name='__init__', docstring=klass.pyDocstring) + + for item in public: + f = dispatch[item.__class__] + f(item, stream, indent2) + + for item in protected: + f = dispatch[item.__class__] + f(item, stream, indent2) + + stream.write('%s# end of class %s\n\n' % (indent, klassName)) + + + def generateMemberVar(self, memberVar, stream, indent): + assert isinstance(memberVar, extractors.MemberVarDef) + if memberVar.ignored: + return + stream.write('%s%s = property(None, None)\n' % (indent, memberVar.name)) + + + def generateProperty(self, prop, stream, indent): + assert isinstance(prop, extractors.PropertyDef) + if prop.ignored: + return + stream.write('%s%s = property(None, None)\n' % (indent, prop.name)) + + + def generatePyProperty(self, prop, stream, indent): + assert isinstance(prop, extractors.PyPropertyDef) + if prop.ignored: + return + stream.write('%s%s = property(None, None)\n' % (indent, prop.name)) + + + def generateMethod(self, method, stream, indent, name=None, docstring=None): + assert isinstance(method, extractors.MethodDef) + if method.ignored: + return + + name = name or method.pyName or method.name + if name in magicMethods: + name = magicMethods[name] + + # write the method declaration + if method.isStatic: + stream.write('\n%s@staticmethod' % indent) + stream.write('\n%sdef %s' % (indent, name)) + if method.overloads: + if not method.isStatic: + stream.write('(self, *args, **kw)') + else: + stream.write('(*args, **kw)') + else: + argsString = method.pyArgsString + if not argsString: + argsString = '()' + if not method.isStatic: + if argsString[:2] == '()': + argsString = '(self)' + argsString[2:] + else: + argsString = '(self, ' + argsString[1:] + if '->' in argsString: + pos = argsString.rfind(')') + argsString = argsString[:pos+1] + stream.write(argsString) + stream.write(':\n') + indent2 = indent + ' '*4 + + # docstring + if not docstring: + if hasattr(method, 'pyDocstring'): + docstring = method.pyDocstring + else: + docstring = "" + stream.write('%s"""\n' % indent2) + stream.write(nci(docstring, len(indent2))) + stream.write('%s"""\n' % indent2) + + + + def generateCppMethod(self, method, stream, indent=''): + assert isinstance(method, extractors.CppMethodDef) + self.generateMethod(method, stream, indent) + + + def generateCppMethod_sip(self, method, stream, indent=''): + assert isinstance(method, extractors.CppMethodDef_sip) + self.generateMethod(method, stream, indent) + + + def generatePyMethod(self, pm, stream, indent): + assert isinstance(pm, extractors.PyMethodDef) + if pm.ignored: + return + stream.write('\n%sdef %s' % (indent, pm.name)) + stream.write(pm.argsString) + stream.write(':\n') + indent2 = indent + ' '*4 + + stream.write('%s"""\n' % indent2) + stream.write(nci(pm.pyDocstring, len(indent2))) + stream.write('%s"""\n' % indent2) + + + + +#--------------------------------------------------------------------------- +# helpers + +def guessTypeInt(v): + if isinstance(v, extractors.EnumValueDef): + return True + if isinstance(v, extractors.DefineDef) and '"' not in v.value: + return True + type = v.type.replace('const', '') + type = type.replace(' ', '') + if type in ['int', 'long', 'byte', 'size_t']: + return True + if 'unsigned' in type: + return True + return False + + +def guessTypeFloat(v): + type = v.type.replace('const', '') + type = type.replace(' ', '') + if type in ['float', 'double', 'wxDouble']: + return True + return False + +def guessTypeStr(v): + if hasattr(v, 'value') and '"' in v.value: + return True + if 'wxString' in v.type: + return True + return False + +#--------------------------------------------------------------------------- diff --git a/etgtools/sip_generator.py b/etgtools/sip_generator.py index 9ce42f86..b5592a00 100644 --- a/etgtools/sip_generator.py +++ b/etgtools/sip_generator.py @@ -15,12 +15,18 @@ objects produced by the ETG scripts. import sys, os, re import extractors import generators +from generators import nci from cStringIO import StringIO divider = '//' + '-'*75 + '\n' phoenixRoot = os.path.abspath(os.path.split(__file__)[0]+'/..') +class SipGeneratorError(RuntimeError): + pass + + + # This is a list of types that are used as return by value or by reference # function return types that we need to ensure are actually using pointer # types in their CppMethodDef or cppCode wrappers. @@ -331,7 +337,7 @@ from %s import * if klass.kind == 'class': stream.write('%s%s:\n' % (indent, item.protection)) item.klass = klass - self.generateClass(item, stream, indent + ' '*4) + self.generateClass(item, stream, indent2) if klass.kind == 'class': stream.write('%spublic:\n' % indent) @@ -359,19 +365,19 @@ from %s import * for item in ctors: item.klass = klass f = dispatch[item.__class__] - f(item, stream, indent + ' '*4) + f(item, stream, indent2) for item in public: item.klass = klass f = dispatch[item.__class__] - f(item, stream, indent + ' '*4) + f(item, stream, indent2) if protected and [i for i in protected if not i.ignored]: stream.write('\nprotected:\n') for item in protected: item.klass = klass f = dispatch[item.__class__] - f(item, stream, indent + ' '*4) + f(item, stream, indent2) if klass.convertFromPyObject: self.generateConvertCode('%ConvertToTypeCode', @@ -449,7 +455,7 @@ from %s import * # and save the docstring back into item in case it is needed by other # generators later on item.pyDocstring = nci(text) - + def generateMethod(self, method, stream, indent, _needDocstring=True): assert isinstance(method, extractors.MethodDef) @@ -661,9 +667,11 @@ from %s import * klassName = pm.klass.pyName or pm.klass.name stream.write("%%Extract(id=pycode%s)\n" % self.module_name) stream.write("def _%s_%s%s:\n" % (klassName, pm.name, pm.argsString)) + pm.pyDocstring = "" if pm.briefDoc: doc = nci(pm.briefDoc) - stream.write(nci('"""\n%s"""\n' % doc, 4)) + pm.pyDocstring = doc + stream.write(nci('"""\n%s"""\n' % doc, 4)) stream.write(nci(pm.body, 4)) if pm.deprecated: stream.write('%s.%s = wx.deprecated(_%s_%s)\n' % (klassName, pm.name, klassName, pm.name)) @@ -768,50 +776,3 @@ from %s import * return '' #--------------------------------------------------------------------------- -# helpers and utilities - -def nci(text, numSpaces=0, stripLeading=True): - """ - Normalize Code Indents - - First use the count of leading spaces on the first line and remove that - many spaces from the front of all lines, and then indent each line by - adding numSpaces spaces. This is used so we can convert the arbitrary - indents that might be used by the tweaker code into what is expected for - the context we are generating for. - """ - def _getLeadingSpaceCount(line): - count = 0 - for c in line: - assert c != '\t', "Use spaces for indent, not tabs" - if c != ' ': - break - count += 1 - return count - - def _allSpaces(text): - for c in text: - if c != ' ': - return False - return True - - - lines = text.rstrip().split('\n') - if stripLeading: - numStrip = _getLeadingSpaceCount(lines[0]) - else: - numStrip = 0 - - for idx, line in enumerate(lines): - assert _allSpaces(line[:numStrip]), "Indentation inconsistent with first line" - lines[idx] = ' '*numSpaces + line[numStrip:] - - newText = '\n'.join(lines) + '\n' - return newText - - -class SipGeneratorError(RuntimeError): - pass - - -#--------------------------------------------------------------------------- diff --git a/etgtools/swig_generator.py b/etgtools/swig_generator.py index 81530237..d019766d 100644 --- a/etgtools/swig_generator.py +++ b/etgtools/swig_generator.py @@ -8,5 +8,6 @@ #--------------------------------------------------------------------------- """ -Move along, there's nothing to see here... +Move along, there's nothing to see here... These are not the droids you are +looking for... """ diff --git a/etgtools/tweaker_tools.py b/etgtools/tweaker_tools.py index 428151f6..4dcbbbc0 100644 --- a/etgtools/tweaker_tools.py +++ b/etgtools/tweaker_tools.py @@ -15,6 +15,11 @@ stage of the ETG scripts. import extractors import sys, os +magicMethods = { + 'operator!=' : '__ne__', + 'operator==' : '__eq__', + # TODO +} def removeWxPrefixes(node): @@ -299,10 +304,9 @@ def getWrapperGenerator(): import sip_generator gClass = sip_generator.SipWrapperGenerator else: - # The default is sip, at least for now... + # The default is sip import sip_generator - gClass = sip_generator.SipWrapperGenerator - + gClass = sip_generator.SipWrapperGenerator return gClass() @@ -315,14 +319,23 @@ def getDocsGenerator(): def runGenerators(module): checkForUnitTestModule(module) + + generators = list() - # Create the code generator and make the wrapper code - wg = getWrapperGenerator() - wg.generate(module) + # Create the code generator selected from command line args + generators.append(getWrapperGenerator()) - # Create a documentation generator and let it do its thing - dg = getDocsGenerator() - dg.generate(module) + # Toss in the PI generator too + import pi_generator + generators.append(pi_generator.PiWrapperGenerator()) + + # And finally add the documentation generator + generators.append(getDocsGenerator()) + + # run them + for g in generators: + g.generate(module) + def checkForUnitTestModule(module):