mirror of
https://github.com/wxWidgets/Phoenix.git
synced 2025-12-15 17:20:07 +01:00
1818 lines
62 KiB
Python
1818 lines
62 KiB
Python
#---------------------------------------------------------------------------
|
|
# Name: etgtools/extractors.py
|
|
# Author: Robin Dunn
|
|
#
|
|
# Created: 3-Nov-2010
|
|
# Copyright: (c) 2010-2020 by Total Control Software
|
|
# License: wxWindows License
|
|
#---------------------------------------------------------------------------
|
|
|
|
"""
|
|
Functions and classes that can parse the Doxygen XML files and extract the
|
|
wxWidgets API info which we need from them.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import pprint
|
|
from typing import Optional
|
|
import xml.etree.ElementTree as ET
|
|
import copy
|
|
|
|
from .tweaker_tools import AutoConversionInfo, FixWxPrefix, MethodType, magicMethods, \
|
|
guessTypeInt, guessTypeFloat, guessTypeStr, \
|
|
textfile_open, Signature, removeWxPrefix
|
|
from sphinxtools.utilities import findDescendants
|
|
|
|
if sys.version_info >= (3, 11):
|
|
from typing import Self
|
|
else:
|
|
from typing_extensions import Self
|
|
|
|
#---------------------------------------------------------------------------
|
|
# These classes simply hold various bits of information about the classes,
|
|
# methods, functions and other items in the C/C++ API being wrapped.
|
|
#---------------------------------------------------------------------------
|
|
|
|
class BaseDef:
|
|
"""
|
|
The base class for all element types and provides the common attributes
|
|
and functions that they all share.
|
|
"""
|
|
nameTag = 'name'
|
|
def __init__(self, element=None):
|
|
self.name = '' # name of the item
|
|
self.pyName = '' # rename to this name
|
|
self.ignored = False # skip this item
|
|
self.docsIgnored = False # skip this item when generating docs
|
|
self.briefDoc = '' # either a string or a single para Element
|
|
self.detailedDoc = [] # collection of para Elements
|
|
self.deprecated = False # is this item deprecated
|
|
|
|
# The items list is used by some subclasses to collect items that are
|
|
# part of that item, like methods of a ClassDef, parameters in a
|
|
# MethodDef, etc.
|
|
self.items = []
|
|
|
|
if element is not None:
|
|
self.extract(element)
|
|
|
|
def __iter__(self):
|
|
return iter(self.items)
|
|
|
|
def __repr__(self):
|
|
return "{}: '{}', '{}'".format(self.__class__.__name__, self.name, self.pyName)
|
|
|
|
def extract(self, element):
|
|
# Pull info from the ElementTree element that is pertinent to this
|
|
# class. Should be overridden in derived classes to get what each one
|
|
# needs in addition to the base.
|
|
self.name = element.find(self.nameTag).text
|
|
if self.name is None:
|
|
self.name = ''
|
|
if '::' in self.name:
|
|
loc = self.name.rfind('::')
|
|
self.name = self.name[loc+2:]
|
|
bd = element.find('briefdescription')
|
|
if len(bd):
|
|
self.briefDoc = bd[0] # Should be just one <para> element
|
|
self.detailedDoc = list(element.find('detaileddescription'))
|
|
|
|
|
|
def checkDeprecated(self):
|
|
# Don't iterate all items, just the para items found in detailedDoc,
|
|
# so that classes with a deprecated method don't likewise become deprecated.
|
|
for para in self.detailedDoc:
|
|
for item in para.iter():
|
|
itemid = item.get('id')
|
|
if itemid and itemid.startswith('deprecated'):
|
|
self.deprecated = True
|
|
return
|
|
|
|
|
|
def clearDeprecated(self):
|
|
"""
|
|
Remove the deprecation notice from the detailedDoc, if any, and reset
|
|
self.deprecated to False.
|
|
"""
|
|
self.deprecated = False
|
|
for para in self.detailedDoc:
|
|
for item in para.iter():
|
|
itemid = item.get('id')
|
|
if itemid and itemid.startswith('deprecated'):
|
|
self.detailedDoc.remove(para)
|
|
return
|
|
|
|
|
|
def ignore(self, val=True) -> Self:
|
|
self.ignored = val
|
|
return self
|
|
|
|
|
|
def find(self, name):
|
|
"""
|
|
Locate and return an item within this item that has a matching name.
|
|
The name string can use a dotted notation to continue the search
|
|
recursively. Raises ExtractorError if not found.
|
|
"""
|
|
try:
|
|
head, tail = name.split('.', 1)
|
|
except ValueError:
|
|
head, tail = name, None
|
|
for item in self._findItems():
|
|
if item.name == head or item.pyName == head: # TODO: exclude ignored items?
|
|
if not tail:
|
|
return item
|
|
else:
|
|
return item.find(tail)
|
|
else: # got though all items with no match
|
|
raise ExtractorError("Unable to find item named '%s' within %s named '%s'" %
|
|
(head, self.__class__.__name__, self.name))
|
|
|
|
def findItem(self, name):
|
|
"""
|
|
Just like find() but does not raise an exception if the item is not found.
|
|
"""
|
|
try:
|
|
item = self.find(name)
|
|
return item
|
|
except ExtractorError:
|
|
return None
|
|
|
|
|
|
def addItem(self, item):
|
|
self.items.append(item)
|
|
return item
|
|
|
|
def insertItem(self, index, item):
|
|
self.items.insert(index, item)
|
|
return item
|
|
|
|
def insertItemAfter(self, after, item):
|
|
try:
|
|
idx = self.items.index(after)
|
|
self.items.insert(idx+1, item)
|
|
except ValueError:
|
|
self.items.append(item)
|
|
return item
|
|
|
|
def insertItemBefore(self, before, item):
|
|
try:
|
|
idx = self.items.index(before)
|
|
self.items.insert(idx, item)
|
|
except ValueError:
|
|
self.items.insert(0, item)
|
|
return item
|
|
|
|
|
|
def allItems(self):
|
|
"""
|
|
Recursively create a sequence for traversing all items in the
|
|
collection. A generator would be nice but just prebuilding a list will
|
|
be good enough.
|
|
"""
|
|
items = [self]
|
|
for item in self.items:
|
|
items.extend(item.allItems())
|
|
if hasattr(item, 'overloads'):
|
|
for o in item.overloads:
|
|
items.extend(o.allItems())
|
|
if hasattr(item, 'innerclasses'):
|
|
for o in item.innerclasses:
|
|
items.extend(o.allItems())
|
|
|
|
return items
|
|
|
|
|
|
def findAll(self, name):
|
|
"""
|
|
Search recursively for items that have the given name.
|
|
"""
|
|
matches = list()
|
|
for item in self.allItems():
|
|
if item.name == name or item.pyName == name:
|
|
matches.append(item)
|
|
return matches
|
|
|
|
|
|
def _findItems(self):
|
|
# If there are more items to be searched than what is in self.items, a
|
|
# subclass can override this to give a different list.
|
|
return self.items
|
|
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class VariableDef(BaseDef):
|
|
"""
|
|
Represents a basic variable declaration.
|
|
"""
|
|
def __init__(self, element=None, **kw):
|
|
super(VariableDef, self).__init__()
|
|
self.type = None
|
|
self.definition = ''
|
|
self.argsString = ''
|
|
self.pyInt = False
|
|
self.noSetter = False
|
|
self.__dict__.update(**kw)
|
|
if element is not None:
|
|
self.extract(element)
|
|
|
|
def extract(self, element):
|
|
super(VariableDef, self).extract(element)
|
|
self.type = flattenNode(element.find('type'))
|
|
self.definition = element.find('definition').text
|
|
self.argsString = element.find('argsstring').text
|
|
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
# These need the same attributes as VariableDef, but we use separate classes
|
|
# so we can identify what kind of element it came from originally.
|
|
|
|
class GlobalVarDef(VariableDef):
|
|
pass
|
|
|
|
|
|
class TypedefDef(VariableDef):
|
|
def __init__(self, element=None, **kw):
|
|
super(TypedefDef, self).__init__()
|
|
self.noTypeName = False
|
|
self.docAsClass = False
|
|
self.bases = []
|
|
self.protection = 'public'
|
|
self.__dict__.update(**kw)
|
|
if element is not None:
|
|
self.extract(element)
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class MemberVarDef(VariableDef):
|
|
"""
|
|
Represents a variable declaration in a class.
|
|
"""
|
|
def __init__(self, element=None, **kw):
|
|
super(MemberVarDef, self).__init__()
|
|
self.isStatic = False
|
|
self.protection = 'public'
|
|
self.getCode = ''
|
|
self.setCode = ''
|
|
self.__dict__.update(kw)
|
|
if element is not None:
|
|
self.extract(element)
|
|
|
|
def extract(self, element):
|
|
super(MemberVarDef, self).extract(element)
|
|
self.isStatic = element.get('static') == 'yes'
|
|
self.protection = element.get('prot')
|
|
assert self.protection in ['public', 'protected']
|
|
# TODO: Should protected items be ignored by default or should we
|
|
# leave that up to the tweaker code or the generators?
|
|
if self.protection == 'protected':
|
|
self.ignore()
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
_globalIsCore = None
|
|
|
|
class FunctionDef(BaseDef, FixWxPrefix):
|
|
"""
|
|
Information about a standalone function.
|
|
"""
|
|
_default_method_type = MethodType.FUNCTION
|
|
|
|
def __init__(self, element=None, **kw):
|
|
super(FunctionDef, self).__init__()
|
|
self.type = None
|
|
self.definition = ''
|
|
self.argsString = ''
|
|
self.signature: Optional[Signature] = None
|
|
self.pyArgsString = ''
|
|
self.isOverloaded = False
|
|
self.overloads = []
|
|
self.factory = False # a factory function that creates a new instance of the return value
|
|
self.pyReleaseGIL = False # release the Python GIL for this function call
|
|
self.pyHoldGIL = False # hold the Python GIL for this function call
|
|
self.noCopy = False # don't make a copy of the return value, just wrap the original
|
|
self.pyInt = False # treat char types as integers
|
|
self.transfer = False # transfer ownership of return value to C++?
|
|
self.transferBack = False # transfer ownership of return value from C++ to Python?
|
|
self.transferThis = False # ownership of 'this' pointer transferred to C++
|
|
self.cppCode = None # Use this code instead of the default wrapper
|
|
self.noArgParser = False # set the NoargParser annotation
|
|
self.preMethodCode = None
|
|
|
|
self.__dict__.update(kw)
|
|
if element is not None:
|
|
self.extract(element)
|
|
|
|
|
|
|
|
def extract(self, element):
|
|
super(FunctionDef, self).extract(element)
|
|
self.type = flattenNode(element.find('type'))
|
|
self.definition = element.find('definition').text
|
|
self.argsString = element.find('argsstring').text
|
|
self.checkDeprecated()
|
|
for node in element.findall('param'):
|
|
p = ParamDef(node)
|
|
self.items.append(p)
|
|
# TODO: Look at self.detailedDoc and pull out any matching
|
|
# parameter description items and assign that value as the
|
|
# briefDoc for this ParamDef object.
|
|
|
|
|
|
def releaseGIL(self, release=True):
|
|
self.pyReleaseGIL = release
|
|
|
|
def holdGIL(self, hold=True):
|
|
self.pyHoldGIL = hold
|
|
|
|
|
|
def setCppCode_sip(self, code):
|
|
"""
|
|
Use the given C++ code instead of that automatically generated by the
|
|
back-end. This is similar to adding a new C++ method, except it uses
|
|
info we've already received from the source XML such as the argument
|
|
types and names, docstring, etc.
|
|
|
|
The code generated for this version will expect the given code to use
|
|
SIP specific variable names, etc. For example::
|
|
|
|
sipRes = sipCpp->Foo();
|
|
"""
|
|
self.cppCode = (code, 'sip')
|
|
|
|
|
|
def setCppCode(self, code):
|
|
"""
|
|
Use the given C++ code instead of that automatically generated by the
|
|
back-end. This is similar to adding a new C++ method, except it uses
|
|
info we've already received from the source XML such as the argument
|
|
types and names, docstring, etc.
|
|
|
|
The code generated for this version will put the given code in a
|
|
wrapper function that will enable it to be more independent, not SIP
|
|
specific, and also more natural. For example::
|
|
|
|
return self->Foo();
|
|
"""
|
|
self.cppCode = (code, 'function')
|
|
|
|
|
|
def checkForOverload(self, methods):
|
|
for m in methods:
|
|
if isinstance(m, FunctionDef) and m.name == self.name:
|
|
m.overloads.append(self)
|
|
m.isOverloaded = self.isOverloaded = True
|
|
return True
|
|
return False
|
|
|
|
|
|
def all(self):
|
|
return [self] + self.overloads
|
|
|
|
|
|
def findOverload(self, matchText, isConst=None, printSig=False):
|
|
"""
|
|
Search for an overloaded method that has matchText in its C++ argsString.
|
|
"""
|
|
for o in self.all():
|
|
if printSig:
|
|
print("%s%s" % (o.name, o.argsString))
|
|
if matchText in o.argsString and not o.ignored:
|
|
if isConst is None:
|
|
return o
|
|
else:
|
|
if o.isConst == isConst:
|
|
return o
|
|
return None
|
|
|
|
|
|
def hasOverloads(self):
|
|
"""
|
|
Returns True if there are any overloads that are not ignored.
|
|
"""
|
|
return bool([x for x in self.overloads if not x.ignored])
|
|
|
|
|
|
def renameOverload(self, matchText, newName, **kw):
|
|
"""
|
|
Rename the overload with matching matchText in the argsString to
|
|
newName. The overload is moved out of this function's overload list
|
|
and directly into the parent module or class so it can appear to be a
|
|
separate function.
|
|
"""
|
|
if hasattr(self, 'module'):
|
|
parent = self.module
|
|
else:
|
|
parent = self.klass
|
|
item = self.findOverload(matchText)
|
|
assert item is not None
|
|
item.pyName = newName
|
|
if item.signature:
|
|
item.signature.method_name = newName
|
|
item.__dict__.update(kw)
|
|
|
|
if item is self and not self.hasOverloads():
|
|
# We're done, there actually is only one instance of this method
|
|
pass
|
|
elif item is self:
|
|
# Make the first overload take the place of this node in the
|
|
# parent, and then insert this item into the parent's list again
|
|
overloads = self.overloads
|
|
overloads.sort(key=lambda o: o.ignored)
|
|
self.overloads = []
|
|
first = overloads[0]
|
|
first.overloads = overloads[1:]
|
|
idx = parent.items.index(self)
|
|
parent.items[idx] = first
|
|
parent.insertItemAfter(first, self)
|
|
|
|
else:
|
|
# Just remove from the overloads list and insert it into the parent.
|
|
self.overloads.remove(item)
|
|
parent.insertItemAfter(self, item)
|
|
return item
|
|
|
|
|
|
def ignore(self, val=True) -> Self:
|
|
# In addition to ignoring this item, reorder any overloads to ensure
|
|
# the primary overload is not ignored, if possible.
|
|
super(FunctionDef, self).ignore(val)
|
|
if val and self.overloads:
|
|
self.reorderOverloads()
|
|
return self
|
|
|
|
|
|
def reorderOverloads(self):
|
|
# Reorder a set of overloaded functions such that the primary
|
|
# FunctionDef is one that is not ignored.
|
|
if self.overloads and self.ignored:
|
|
all = [self] + self.overloads
|
|
all.sort(key=lambda item: item.ignored)
|
|
first = all[0]
|
|
if not first.ignored:
|
|
if hasattr(self, 'module'):
|
|
parent = self.module
|
|
else:
|
|
parent = self.klass
|
|
self.overloads = []
|
|
first.overloads = all[1:]
|
|
idx = parent.items.index(self)
|
|
parent.items[idx] = first
|
|
|
|
|
|
def _findItems(self):
|
|
items = list(self.items)
|
|
for o in self.overloads:
|
|
items.extend(o.items)
|
|
return items
|
|
|
|
|
|
def makePyArgsString(self):
|
|
"""
|
|
Create a pythonized version of the argsString in function and method
|
|
items that can be used as part of the docstring.
|
|
"""
|
|
params: list[Signature.Parameter] = []
|
|
returns: list[str] = []
|
|
if self.type and self.type != 'void':
|
|
returns.append(self.cleanType(self.type))
|
|
|
|
defValueMap = { 'true': 'True',
|
|
'false': 'False',
|
|
'NULL': 'None',
|
|
'wxString()': '""',
|
|
'wxArrayString()' : '[]',
|
|
'wxArrayInt()' : '[]',
|
|
'wxEmptyString': "''", # Makes signatures much shorter
|
|
}
|
|
P = Signature.Parameter
|
|
if isinstance(self, CppMethodDef):
|
|
# rip apart 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].strip()
|
|
arg = arg.split('=')[0].strip()
|
|
default = defValueMap.get(default, default)
|
|
default = self.fixWxPrefix(default, True)
|
|
# now grab just the last word, it should be the variable name
|
|
# The rest will be the type information
|
|
arg_type, arg = arg.rsplit(None, 1)
|
|
arg, arg_type = self.parseNameAndType(arg, arg_type, True)
|
|
params.append(P(arg, arg_type, default))
|
|
if default == 'None':
|
|
params[-1].make_optional()
|
|
else:
|
|
for param in self.items:
|
|
assert isinstance(param, ParamDef)
|
|
if param.ignored:
|
|
continue
|
|
if param.arraySize:
|
|
continue
|
|
s, param_type = self.parseNameAndType(param.pyName or param.name, param.type, not param.out)
|
|
if param.out:
|
|
if param_type:
|
|
returns.append(param_type)
|
|
else:
|
|
default = ''
|
|
if param.inOut:
|
|
if param_type:
|
|
returns.append(param_type)
|
|
if param.default:
|
|
default = param.default
|
|
default = defValueMap.get(default, default)
|
|
default = '|'.join([self.cleanName(x, True) for x in default.split('|')])
|
|
params.append(P(s, param_type, default))
|
|
if default == 'None':
|
|
params[-1].make_optional()
|
|
if getattr(self, 'isCtor', False):
|
|
name = '__init__'
|
|
else:
|
|
name = self.pyName or self.name
|
|
name = self.fixWxPrefix(name)
|
|
# __bool__ and __nonzero__ need to be defined as returning int for SIP, but for Python
|
|
# __bool__ is required to return a bool:
|
|
if name in ('__bool__', '__nonzero__'):
|
|
return_type = 'bool'
|
|
elif not returns:
|
|
return_type = 'None'
|
|
elif len(returns) == 1:
|
|
return_type = returns[0]
|
|
else:
|
|
return_type = f"Tuple[{', '.join(returns)}]"
|
|
kind = MethodType.STATIC_METHOD if getattr(self, 'isStatic', False) else type(self)._default_method_type
|
|
self.signature = Signature(name, *params, return_type=return_type, method_type=kind)
|
|
self.pyArgsString = self.signature.args_string(False)
|
|
|
|
|
|
def collectPySignatures(self):
|
|
"""
|
|
Collect the pyArgsStrings for self and any overloads, and create a
|
|
list of function signatures for the docstrings.
|
|
"""
|
|
sigs = list()
|
|
for f in [self] + self.overloads:
|
|
assert isinstance(f, FunctionDef)
|
|
if f.ignored:
|
|
continue
|
|
if not f.pyArgsString:
|
|
f.makePyArgsString()
|
|
|
|
sig = f.pyName or self.fixWxPrefix(f.name)
|
|
if sig in magicMethods:
|
|
sig = magicMethods[sig]
|
|
sig += f.pyArgsString
|
|
sigs.append(sig)
|
|
return sigs
|
|
|
|
|
|
def mustHaveApp(self, value=True):
|
|
if value:
|
|
self.preMethodCode = "if (!wxPyCheckForApp()) return NULL;\n"
|
|
else:
|
|
self.preMethodCode = None
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class MethodDef(FunctionDef):
|
|
"""
|
|
Represents a class method, ctor or dtor declaration.
|
|
"""
|
|
_default_method_type = MethodType.METHOD
|
|
|
|
def __init__(self, element=None, className=None, **kw):
|
|
super(MethodDef, self).__init__()
|
|
self.className = className
|
|
self.isVirtual = False
|
|
self.isPureVirtual = False
|
|
self.isStatic = False
|
|
self.isConst = False
|
|
self.isCtor = False
|
|
self.isDtor = False
|
|
self.protection = 'public'
|
|
self.defaultCtor = False # use this ctor as the default one
|
|
self.noDerivedCtor = False # don't generate a ctor in the derived class for this ctor
|
|
self.cppSignature = None
|
|
self.virtualCatcherCode = None
|
|
self.__dict__.update(kw)
|
|
if element is not None:
|
|
self.extract(element)
|
|
elif not hasattr(self, 'isCore'):
|
|
self.isCore = _globalIsCore
|
|
|
|
|
|
def extract(self, element):
|
|
super(MethodDef, self).extract(element)
|
|
self.isStatic = element.get('static') == 'yes'
|
|
self.isVirtual = element.get('virt') in ['virtual', 'pure-virtual']
|
|
self.isPureVirtual = element.get('virt') == 'pure-virtual'
|
|
self.isConst = element.get('const') == 'yes'
|
|
self.isCtor = self.name == self.className
|
|
self.isDtor = self.name == '~' + self.className
|
|
self.protection = element.get('prot')
|
|
assert self.protection in ['public', 'protected']
|
|
# TODO: Should protected items be ignored by default or should we
|
|
# leave that up to the tweaker code or the generators?
|
|
if self.protection == 'protected':
|
|
self.ignore()
|
|
|
|
|
|
def setVirtualCatcherCode(self, code):
|
|
"""
|
|
"""
|
|
self.virtualCatcherCode = code
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class ParamDef(BaseDef):
|
|
"""
|
|
A parameter of a function or method.
|
|
"""
|
|
def __init__(self, element=None, **kw):
|
|
super(ParamDef, self).__init__()
|
|
self.type = '' # data type
|
|
self.default = '' # default value
|
|
self.out = False # is it an output arg?
|
|
self.inOut = False # is it both input and output?
|
|
self.pyInt = False # treat char types as integers
|
|
self.array = False # the param is to be treated as an array
|
|
self.arraySize = False # the param is the size of the array
|
|
self.transfer = False # transfer ownership of arg to C++?
|
|
self.transferBack = False # transfer ownership of arg from C++ to Python?
|
|
self.transferThis = False # ownership of 'this' pointer transferred to this arg
|
|
self.keepReference = False # an extra reference to the arg is held
|
|
self.constrained = False # limit auto-conversion of similar types (like float -> int)
|
|
self.__dict__.update(kw)
|
|
if element is not None:
|
|
self.extract(element)
|
|
|
|
def extract(self, element):
|
|
try:
|
|
self.type = flattenNode(element.find('type'))
|
|
# we've got varags
|
|
if self.type == '...':
|
|
self.name = ''
|
|
else:
|
|
if element.find('declname') is not None:
|
|
self.name = element.find('declname').text
|
|
elif element.find('defname') is not None:
|
|
self.name = element.find('defname').text
|
|
if element.find('defval') is not None:
|
|
self.default = flattenNode(element.find('defval'))
|
|
except:
|
|
print("error when parsing element:")
|
|
ET.dump(element)
|
|
raise
|
|
#---------------------------------------------------------------------------
|
|
|
|
class ClassDef(BaseDef):
|
|
"""
|
|
The information about a class that is needed to generate wrappers for it.
|
|
"""
|
|
nameTag = 'compoundname'
|
|
def __init__(self, element=None, kind='class', **kw):
|
|
super(ClassDef, self).__init__()
|
|
self.kind = kind
|
|
self.protection = 'public'
|
|
self.templateParams = [] # class is a template
|
|
self.bases = [] # base class names
|
|
self.subClasses = [] # sub classes
|
|
self.nodeBases = [] # for the inheritance diagram
|
|
self.enum_file = '' # To link sphinx output classes to enums
|
|
self.includes = [] # .h file for this class
|
|
self.abstract = False # is it an abstract base class?
|
|
self.external = False # class is in another module
|
|
self.noDefCtor = False # do not generate a default constructor
|
|
self.singleton = False # class is a singleton so don't call the dtor until the interpreter exits
|
|
self.allowAutoProperties = True
|
|
self.headerCode = []
|
|
self.cppCode = []
|
|
self.convertToPyObject = None
|
|
self._convertFromPyObject = None
|
|
self.allowNone = False # Allow the convertFrom code to handle None too.
|
|
self.instanceCode = None # Code to be used to create new instances of this class
|
|
self.innerclasses = []
|
|
self.isInner = False # Is this a nested class?
|
|
self.klass = None # if so, then this is the outer class
|
|
self.preMethodCode = None
|
|
self.postProcessReST = None
|
|
|
|
# Stuff that needs to be generated after the class instead of within
|
|
# it. Some back-end generators need to put stuff inside the class, and
|
|
# others need to do it outside the class definition. The generators
|
|
# can move things here for later processing when they encounter those
|
|
# items.
|
|
self.generateAfterClass = []
|
|
|
|
self.__dict__.update(kw)
|
|
if element is not None:
|
|
self.extract(element)
|
|
|
|
@property
|
|
def convertFromPyObject(self) -> Optional[str]:
|
|
return self._convertFromPyObject
|
|
|
|
@convertFromPyObject.setter
|
|
def convertFromPyObject(self, value: AutoConversionInfo) -> None:
|
|
self._convertFromPyObject = value.code
|
|
name = self.pyName or self.name
|
|
name = removeWxPrefix(name)
|
|
FixWxPrefix.register_autoconversion(name, value.convertables)
|
|
|
|
def is_top_level(self) -> bool:
|
|
"""Check if this class is a subclass of wx.TopLevelWindow"""
|
|
if not self.nodeBases:
|
|
return False
|
|
all_classes, specials = self.nodeBases
|
|
if 'wxTopLevelWindow' in specials:
|
|
return True
|
|
return 'wxTopLevelWindow' in all_classes
|
|
|
|
|
|
def renameClass(self, newName):
|
|
self.pyName = newName
|
|
for item in self.items:
|
|
if hasattr(item, 'className'):
|
|
item.className = newName
|
|
for overload in item.overloads:
|
|
overload.className = newName
|
|
|
|
|
|
def findHierarchy(self, element, all_classes, specials, read):
|
|
from etgtools import XMLSRC
|
|
|
|
if not read:
|
|
fullname = self.name
|
|
specials = [fullname]
|
|
else:
|
|
fullname = element.text
|
|
|
|
baselist = []
|
|
|
|
if read:
|
|
refid = element.get('refid')
|
|
if refid is None:
|
|
return all_classes, specials
|
|
|
|
fname = os.path.join(XMLSRC, refid+'.xml')
|
|
root = ET.parse(fname).getroot()
|
|
compounds = findDescendants(root, 'basecompoundref')
|
|
else:
|
|
compounds = element.findall('basecompoundref')
|
|
|
|
for c in compounds:
|
|
baselist.append(c.text)
|
|
|
|
all_classes[fullname] = (fullname, baselist)
|
|
|
|
for c in compounds:
|
|
all_classes, specials = self.findHierarchy(c, all_classes, specials, True)
|
|
|
|
return all_classes, specials
|
|
|
|
|
|
def extract(self, element):
|
|
super(ClassDef, self).extract(element)
|
|
|
|
self.checkDeprecated()
|
|
self.nodeBases = self.findHierarchy(element, {}, [], False)
|
|
|
|
for node in element.findall('basecompoundref'):
|
|
self.bases.append(node.text)
|
|
for node in element.findall('derivedcompoundref'):
|
|
self.subClasses.append(node.text)
|
|
for node in element.findall('includes'):
|
|
self.includes.append(node.text)
|
|
for node in element.findall('templateparamlist/param'):
|
|
if node.find('declname') is not None:
|
|
txt = node.find('declname').text
|
|
else:
|
|
txt = node.find('type').text
|
|
txt = txt.replace('class ', '')
|
|
txt = txt.replace('typename ', '')
|
|
self.templateParams.append(txt)
|
|
|
|
for node in element.findall('innerclass'):
|
|
if node.get('prot') == 'private':
|
|
continue
|
|
from etgtools import XMLSRC
|
|
ref = node.get('refid')
|
|
fname = os.path.join(XMLSRC, ref+'.xml')
|
|
root = ET.parse(fname).getroot()
|
|
innerclass = root[0]
|
|
kind = innerclass.get('kind')
|
|
assert kind in ['class', 'struct']
|
|
item = ClassDef(innerclass, kind)
|
|
item.protection = node.get('prot')
|
|
item.isInner = True
|
|
item.klass = self # This makes a reference cycle but it's okay
|
|
self.innerclasses.append(item)
|
|
|
|
|
|
# TODO: Is it possible for there to be memberdef's w/o a sectiondef?
|
|
for node in element.findall('sectiondef/memberdef'):
|
|
# skip any private items
|
|
if node.get('prot') == 'private':
|
|
continue
|
|
kind = node.get('kind')
|
|
if kind == 'function':
|
|
m = MethodDef(node, self.name, klass=self)
|
|
if not m.checkForOverload(self.items):
|
|
self.items.append(m)
|
|
elif kind == 'variable':
|
|
v = MemberVarDef(node)
|
|
self.items.append(v)
|
|
elif kind == 'enum':
|
|
e = EnumDef(node, [self])
|
|
self.items.append(e)
|
|
elif kind == 'typedef':
|
|
t = TypedefDef(node)
|
|
self.items.append(t)
|
|
elif kind == 'friend':
|
|
continue
|
|
else:
|
|
raise ExtractorError('Unknown memberdef kind: %s' % kind)
|
|
|
|
|
|
def _findItems(self):
|
|
return self.items + self.innerclasses
|
|
|
|
|
|
def addHeaderCode(self, code):
|
|
if isinstance(code, list):
|
|
self.headerCode.extend(code)
|
|
else:
|
|
self.headerCode.append(code)
|
|
|
|
def addCppCode(self, code):
|
|
if isinstance(code, list):
|
|
self.cppCode.extend(code)
|
|
else:
|
|
self.cppCode.append(code)
|
|
|
|
|
|
def includeCppCode(self, filename):
|
|
with textfile_open(filename) as fid:
|
|
self.addCppCode(fid.read())
|
|
|
|
|
|
def addAutoProperties(self):
|
|
"""
|
|
Look at MethodDef and PyMethodDef items and generate properties if
|
|
there are items that have Get/Set prefixes and have appropriate arg
|
|
counts.
|
|
"""
|
|
def countNonDefaultArgs(m):
|
|
count = 0
|
|
for p in m.items:
|
|
if not p.default and not p.ignored:
|
|
count += 1
|
|
return count
|
|
|
|
def countPyArgs(item):
|
|
count = 0
|
|
args = item.argsString.replace('(', '').replace(')', '')
|
|
for arg in args.split(','):
|
|
if arg != 'self':
|
|
count += 1
|
|
return count
|
|
|
|
def countPyNonDefaultArgs(item):
|
|
count = 0
|
|
args = item.argsString.replace('(', '').replace(')', '')
|
|
for arg in args.split(','):
|
|
if arg != 'self' and '=' not in arg:
|
|
count += 1
|
|
return count
|
|
|
|
props = dict()
|
|
for item in self.items:
|
|
if isinstance(item, (MethodDef, PyMethodDef)) \
|
|
and item.name not in ['Get', 'Set'] \
|
|
and (item.name.startswith('Get') or item.name.startswith('Set')):
|
|
prefix = item.name[:3]
|
|
name = item.name[3:]
|
|
prop = props.get(name, PropertyDef(name))
|
|
if isinstance(item, PyMethodDef):
|
|
ok = False
|
|
argCount = countPyArgs(item)
|
|
nonDefaultArgCount = countPyNonDefaultArgs(item)
|
|
if prefix == 'Get' and argCount == 0:
|
|
ok = True
|
|
prop.getter = item.name
|
|
prop.usesPyMethod = True
|
|
elif prefix == 'Set'and \
|
|
(nonDefaultArgCount == 1 or (nonDefaultArgCount == 0 and argCount > 0)):
|
|
ok = True
|
|
prop.setter = item.name
|
|
prop.usesPyMethod = True
|
|
|
|
else:
|
|
# look at all overloads
|
|
ok = False
|
|
for m in item.all():
|
|
# don't use ignored or static methods for propertiess
|
|
if m.ignored or m.isStatic:
|
|
continue
|
|
if prefix == 'Get':
|
|
prop.getter = m.name
|
|
# Getters must be able to be called with no args, ensure
|
|
# that item has exactly zero args without a default value
|
|
if countNonDefaultArgs(m) != 0:
|
|
continue
|
|
ok = True
|
|
break
|
|
elif prefix == 'Set':
|
|
prop.setter = m.name
|
|
# Setters must be able to be called with 1 arg, ensure
|
|
# that item has at least 1 arg and not more than 1 without
|
|
# a default value.
|
|
if len(m.items) == 0 or countNonDefaultArgs(m) > 1:
|
|
continue
|
|
ok = True
|
|
break
|
|
if ok:
|
|
if hasattr(prop, 'usesPyMethod'):
|
|
prop = PyPropertyDef(prop.name, prop.getter, prop.setter)
|
|
props[name] = prop
|
|
|
|
if props:
|
|
self.addPublic()
|
|
for name, prop in sorted(props.items()):
|
|
# properties must have at least a getter
|
|
if not prop.getter:
|
|
continue
|
|
starts_with_number = False
|
|
try:
|
|
int(name[0])
|
|
starts_with_number = True
|
|
except:
|
|
pass
|
|
|
|
# only create the prop if a method with that name does not exist, and it is a valid name
|
|
if starts_with_number:
|
|
print('WARNING: Invalid property name %s for class %s' % (name, self.name))
|
|
elif self.findItem(name):
|
|
print("WARNING: Method %s::%s already exists in C++ class API, can not create a property." % (self.name, name))
|
|
else:
|
|
self.items.append(prop)
|
|
|
|
|
|
|
|
|
|
def addProperty(self, *args, **kw):
|
|
"""
|
|
Add a property to a class, with a name, getter function and optionally
|
|
a setter method.
|
|
"""
|
|
# As a convenience allow the name, getter and (optionally) the setter
|
|
# to be passed as a single string. Otherwise the args will be passed
|
|
# as-is to PropertyDef
|
|
if len(args) == 1:
|
|
name = getter = setter = ''
|
|
split = args[0].split()
|
|
assert len(split) in [2 ,3]
|
|
if len(split) == 2:
|
|
name, getter = split
|
|
else:
|
|
name, getter, setter = split
|
|
p = PropertyDef(name, getter, setter, **kw)
|
|
else:
|
|
p = PropertyDef(*args, **kw)
|
|
self.items.append(p)
|
|
return p
|
|
|
|
|
|
|
|
def addPyProperty(self, *args, **kw):
|
|
"""
|
|
Add a property to a class that can use PyMethods that have been
|
|
monkey-patched into the class. (This property will also be
|
|
jammed in to the class in like manner.)
|
|
"""
|
|
# Read the nice comment in the method above. Ditto.
|
|
if len(args) == 1:
|
|
name = getter = setter = ''
|
|
split = args[0].split()
|
|
assert len(split) in [2 ,3]
|
|
if len(split) == 2:
|
|
name, getter = split
|
|
else:
|
|
name, getter, setter = split
|
|
p = PyPropertyDef(name, getter, setter, **kw)
|
|
else:
|
|
p = PyPropertyDef(*args, **kw)
|
|
self.items.append(p)
|
|
return p
|
|
|
|
#------------------------------------------------------------------
|
|
|
|
def _addMethod(self, md, overloadOkay=True):
|
|
md.klass = self
|
|
if overloadOkay and self.findItem(md.name):
|
|
item = self.findItem(md.name)
|
|
item.overloads.append(md)
|
|
item.reorderOverloads()
|
|
else:
|
|
self.items.append(md)
|
|
|
|
|
|
def addCppMethod(self, type, name, argsString, body, doc=None, isConst=False,
|
|
cppSignature=None, overloadOkay=True, **kw):
|
|
"""
|
|
Add a new C++ method to a class. This method doesn't have to actually
|
|
exist in the real C++ class. Instead it will be grafted on by the
|
|
back-end wrapper generator such that it is visible in the class in the
|
|
target language.
|
|
"""
|
|
md = CppMethodDef(type, name, argsString, body, doc, isConst, klass=self,
|
|
cppSignature=cppSignature, **kw)
|
|
self._addMethod(md, overloadOkay)
|
|
return md
|
|
|
|
|
|
def addCppCtor(self, argsString, body, doc=None, noDerivedCtor=True,
|
|
useDerivedName=False, cppSignature=None, **kw):
|
|
"""
|
|
Add a C++ method that is a constructor.
|
|
"""
|
|
md = CppMethodDef('', self.name, argsString, body, doc=doc,
|
|
isCtor=True, klass=self, noDerivedCtor=noDerivedCtor,
|
|
useDerivedName=useDerivedName, cppSignature=cppSignature, **kw)
|
|
self._addMethod(md)
|
|
return md
|
|
|
|
|
|
def addCppDtor(self, body, useDerivedName=False, **kw):
|
|
"""
|
|
Add a C++ method that is a destructor.
|
|
"""
|
|
md = CppMethodDef('', '~'+self.name, '()', body, isDtor=True, klass=self,
|
|
useDerivedName=useDerivedName, **kw)
|
|
self._addMethod(md)
|
|
return md
|
|
|
|
|
|
def addCppMethod_sip(self, type, name, argsString, body, doc=None, **kw):
|
|
"""
|
|
Just like the above but can do more things that are SIP specific in
|
|
the code body, instead of using the general purpose implementation.
|
|
"""
|
|
md = CppMethodDef_sip(type, name, argsString, body, doc, klass=self, **kw)
|
|
self._addMethod(md)
|
|
return md
|
|
|
|
def addCppCtor_sip(self, argsString, body, doc=None, noDerivedCtor=True,
|
|
cppSignature=None, **kw):
|
|
"""
|
|
Add a C++ method that is a constructor.
|
|
"""
|
|
md = CppMethodDef_sip('', self.name, argsString, body, doc=doc,
|
|
isCtor=True, klass=self, noDerivedCtor=noDerivedCtor,
|
|
cppSignature=cppSignature, **kw)
|
|
self._addMethod(md)
|
|
return md
|
|
|
|
#------------------------------------------------------------------
|
|
|
|
|
|
def addPyMethod(self, name, argsString, body, doc=None, **kw):
|
|
"""
|
|
Add a (monkey-patched) Python method to this class.
|
|
"""
|
|
pm = PyMethodDef(self, name, argsString, body, doc, **kw)
|
|
self.items.append(pm)
|
|
return pm
|
|
|
|
|
|
def addPyCode(self, code):
|
|
"""
|
|
Add a snippet of Python code which is to be associated with this class.
|
|
"""
|
|
pc = PyCodeDef(code, klass=self, protection = 'public')
|
|
self.items.append(pc)
|
|
return pc
|
|
|
|
|
|
def addPublic(self, code=''):
|
|
"""
|
|
Adds a 'public:' protection keyword to the class, optionally followed
|
|
by some additional code.
|
|
"""
|
|
text = 'public:'
|
|
if code:
|
|
text = text + '\n' + code
|
|
self.addItem(WigCode(text))
|
|
|
|
def addProtected(self, code=''):
|
|
"""
|
|
Adds a 'protected:' protection keyword to the class, optionally followed
|
|
by some additional code.
|
|
"""
|
|
text = 'protected:'
|
|
if code:
|
|
text = text + '\n' + code
|
|
self.addItem(WigCode(text))
|
|
|
|
|
|
def addPrivate(self, code=''):
|
|
"""
|
|
Adds a 'private:' protection keyword to the class, optionally followed
|
|
by some additional code.
|
|
"""
|
|
text = 'private:'
|
|
if code:
|
|
text = text + '\n' + code
|
|
self.addItem(WigCode(text))
|
|
|
|
|
|
def addDefaultCtor(self, prot='protected'):
|
|
# add declaration of a default constructor to this class
|
|
wig = WigCode("""\
|
|
{PROT}:
|
|
{CLASS}();""".format(CLASS=self.name, PROT=prot))
|
|
self.addItem(wig)
|
|
|
|
def addCopyCtor(self, prot='protected'):
|
|
# add declaration of a copy constructor to this class
|
|
wig = WigCode("""\
|
|
{PROT}:
|
|
{CLASS}(const {CLASS}&);""".format(CLASS=self.name, PROT=prot))
|
|
self.addItem(wig)
|
|
|
|
def addPrivateCopyCtor(self):
|
|
self.addCopyCtor('private')
|
|
|
|
def addPrivateDefaultCtor(self):
|
|
self.addDefaultCtor('private')
|
|
|
|
def addPrivateAssignOp(self):
|
|
# add declaration of an assignment opperator to this class
|
|
wig = WigCode("""\
|
|
private:
|
|
{CLASS}& operator=(const {CLASS}&);""".format(CLASS=self.name))
|
|
self.addItem(wig)
|
|
|
|
def addDtor(self, prot='protected', isVirtual=False):
|
|
# add declaration of a destructor to this class
|
|
virtual = 'virtual ' if isVirtual else ''
|
|
wig = WigCode("""\
|
|
{PROT}:
|
|
{VIRTUAL}~{CLASS}();""".format(VIRTUAL=virtual, CLASS=self.name, PROT=prot))
|
|
self.addItem(wig)
|
|
|
|
def addDefaultCtor(self, prot='protected'):
|
|
# add declaration of a default constructor to this class
|
|
wig = WigCode("""\
|
|
{PROT}:
|
|
{CLASS}();""".format(CLASS=self.name, PROT=prot))
|
|
self.addItem(wig)
|
|
|
|
def mustHaveApp(self, value=True):
|
|
if value:
|
|
self.preMethodCode = "if (!wxPyCheckForApp()) return NULL;\n"
|
|
else:
|
|
self.preMethodCode = None
|
|
|
|
|
|
def copyFromClass(self, klass, name):
|
|
"""
|
|
Copy an item from another class into this class. If it is a pure
|
|
virtual method in the other class then assume that it has a concrete
|
|
implementation in this class and change the flag.
|
|
|
|
Returns the new item.
|
|
"""
|
|
item = copy.deepcopy(klass.find(name))
|
|
if isinstance(item, MethodDef) and item.isPureVirtual:
|
|
item.isPureVirtual = False
|
|
self.addItem(item)
|
|
return item
|
|
|
|
def setReSTPostProcessor(self, func):
|
|
"""
|
|
Set a function to be called after the class's docs have been generated.
|
|
"""
|
|
self.postProcessReST = func
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class EnumDef(BaseDef):
|
|
"""
|
|
A named or anonymous enumeration.
|
|
"""
|
|
def __init__(self, element=None, inClass=[], **kw):
|
|
super(EnumDef, self).__init__()
|
|
self.inClass = inClass
|
|
if element is not None:
|
|
prot = element.get('prot')
|
|
if prot is not None:
|
|
self.protection = prot
|
|
assert self.protection in ['public', 'protected']
|
|
# TODO: Should protected items be ignored by default or should we
|
|
# leave that up to the tweaker code or the generators?
|
|
if self.protection == 'protected':
|
|
self.ignore()
|
|
self.extract(element)
|
|
self.__dict__.update(kw)
|
|
|
|
def extract(self, element):
|
|
super(EnumDef, self).extract(element)
|
|
for node in element.findall('enumvalue'):
|
|
value = EnumValueDef(node)
|
|
self.items.append(value)
|
|
|
|
|
|
|
|
|
|
class EnumValueDef(BaseDef):
|
|
"""
|
|
An item in an enumeration.
|
|
"""
|
|
def __init__(self, element=None, **kw):
|
|
super(EnumValueDef, self).__init__()
|
|
if element is not None:
|
|
self.extract(element)
|
|
self.__dict__.update(kw)
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class DefineDef(BaseDef):
|
|
"""
|
|
Represents a #define with a name and a value.
|
|
"""
|
|
def __init__(self, element=None, **kw):
|
|
super(DefineDef, self).__init__()
|
|
if element is not None:
|
|
self.name = element.find('name').text
|
|
self.value = flattenNode(element.find('initializer'))
|
|
self.__dict__.update(kw)
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class PropertyDef(BaseDef):
|
|
"""
|
|
Use the C++ methods of a class to make a Python property.
|
|
|
|
NOTE: This one is not automatically extracted, but can be added to
|
|
classes in the tweaker stage
|
|
"""
|
|
def __init__(self, name, getter=None, setter=None, doc=None, **kw):
|
|
super(PropertyDef, self).__init__()
|
|
self.name = name
|
|
self.getter = getter
|
|
self.setter = setter
|
|
self.briefDoc = doc
|
|
self.protection = 'public'
|
|
self.__dict__.update(kw)
|
|
|
|
|
|
class PyPropertyDef(PropertyDef):
|
|
pass
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class CppMethodDef(MethodDef):
|
|
"""
|
|
This class provides information that can be used to add the code for a new
|
|
method to a wrapper class that does not actually exist in the real C++
|
|
class, or it can be used to provide an alternate implementation for a
|
|
method that does exist. The backend generator support for this feature
|
|
would be things like %extend in SWIG or %MethodCode in SIP.
|
|
|
|
NOTE: This one is not automatically extracted, but can be added to
|
|
classes in the tweaker stage
|
|
"""
|
|
def __init__(self, type, name, argsString: str, body, doc=None, isConst=False,
|
|
cppSignature=None, virtualCatcherCode=None, **kw):
|
|
super(CppMethodDef, self).__init__()
|
|
self.type = type
|
|
self.name = name
|
|
self.useDerivedName = True
|
|
self.argsString = argsString
|
|
self.body = body
|
|
self.briefDoc = doc
|
|
self.protection = 'public'
|
|
self.klass = None
|
|
self.noDerivedCtor = False
|
|
self.isConst = isConst
|
|
self.isPureVirtual = False
|
|
self.cppSignature = cppSignature
|
|
self.virtualCatcherCode = virtualCatcherCode
|
|
self.isCore = _globalIsCore
|
|
self.isSlot = False
|
|
self.__dict__.update(kw)
|
|
|
|
@staticmethod
|
|
def FromMethod(method):
|
|
"""
|
|
Create a new CppMethodDef that is essentially a copy of a MethodDef,
|
|
so it can be used to write the code for a new wrapper function.
|
|
|
|
TODO: It might be better to just refactor the code in the generator
|
|
so it can be shared more easily instead of using a hack like this...
|
|
"""
|
|
m = CppMethodDef('', '', '', '')
|
|
m.__dict__.update(method.__dict__)
|
|
return m
|
|
|
|
|
|
class CppMethodDef_sip(CppMethodDef):
|
|
"""
|
|
Just like the above, but instead of generating a new function from the
|
|
provided code, the code is used inline inside SIP's %MethodCode directive.
|
|
This makes it possible to use additional SIP magic for things that are
|
|
beyond the general scope of the other C++ Method implementation.
|
|
"""
|
|
pass
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class WigCode(BaseDef):
|
|
"""
|
|
This class allows code defined by the extractors to be injected into the
|
|
generated Wrapper Interface Generator file. In other words, this is extra
|
|
code meant to be consumed by the back-end code generator, and it will be
|
|
injected at the point in the file generation that this object is seen.
|
|
"""
|
|
def __init__(self, code, **kw):
|
|
super(WigCode, self).__init__()
|
|
self.code = code
|
|
self.protection = 'public'
|
|
self.__dict__.update(kw)
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class PyCodeDef(BaseDef):
|
|
"""
|
|
This code held by this class will be written to a Python module
|
|
that wraps the import of the extension module.
|
|
"""
|
|
def __init__(self, code, order=None, **kw):
|
|
super(PyCodeDef, self).__init__()
|
|
self.code = code
|
|
self.order = order
|
|
self.__dict__.update(kw)
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class PyFunctionDef(BaseDef):
|
|
"""
|
|
A PyFunctionDef can be used to define Python functions that will be
|
|
written pretty much as-is to a module's .py file. Can also be used for
|
|
methods in a PyClassDef. Using an explicit extractor object rather than
|
|
just "including" the raw code lets us provide the needed metadata for
|
|
document generators, etc.
|
|
"""
|
|
def __init__(self, name, argsString, body, doc=None, order=None, **kw):
|
|
super(PyFunctionDef, self).__init__()
|
|
self.name = name
|
|
self.argsString = self.pyArgsString = argsString
|
|
self.body = body
|
|
self.briefDoc = doc
|
|
self.order = order
|
|
self.deprecated = False
|
|
self.isStatic = False
|
|
self.overloads = []
|
|
self.__dict__.update(kw)
|
|
|
|
|
|
def hasOverloads(self):
|
|
"""
|
|
Returns True if there are any overloads that are not ignored.
|
|
"""
|
|
return bool([x for x in self.overloads if not x.ignored])
|
|
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class PyClassDef(BaseDef):
|
|
"""
|
|
A PyClassDef is used to define a pure-python class that will be injected
|
|
into the module's .py file, but does so in a way that the various bits of
|
|
information about the class are available in the extractor objects for
|
|
the generators to use.
|
|
"""
|
|
def __init__(self, name, bases=[], doc=None, items=[], order=None, **kw):
|
|
super(PyClassDef, self).__init__()
|
|
self.name = name
|
|
self.bases = bases
|
|
self.briefDoc = self.pyDocstring = doc
|
|
self.enum_file = ''
|
|
self.nodeBases = []
|
|
self.subClasses = []
|
|
self.items.extend(items)
|
|
self.deprecated = False
|
|
self.order = order
|
|
self.__dict__.update(kw)
|
|
|
|
self.nodeBases = self.findHierarchy()
|
|
|
|
|
|
def findHierarchy(self):
|
|
|
|
all_classes = {}
|
|
fullname = self.name
|
|
specials = [fullname]
|
|
|
|
baselist = [base for base in self.bases if base != 'object']
|
|
all_classes[fullname] = (fullname, baselist)
|
|
|
|
for base in baselist:
|
|
all_classes[base] = (base, [])
|
|
|
|
return all_classes, specials
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class PyMethodDef(PyFunctionDef):
|
|
"""
|
|
A PyMethodDef can be used to define Python class methods that will then be
|
|
monkey-patched in to the extension module Types as if they belonged there.
|
|
"""
|
|
def __init__(self, klass, name, argsString, body, doc=None, **kw):
|
|
super(PyMethodDef, self).__init__(name, argsString, body, doc)
|
|
self.klass = klass
|
|
self.protection = 'public'
|
|
self.__dict__.update(kw)
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
class ModuleDef(BaseDef):
|
|
"""
|
|
This class holds all the items that will be in the generated module
|
|
"""
|
|
def __init__(self, package, module, name, docstring='', check4unittest=True):
|
|
super(ModuleDef, self).__init__()
|
|
self.package = package
|
|
self.module = module
|
|
self.name = name
|
|
self.docstring = docstring
|
|
self.check4unittest = check4unittest
|
|
self.headerCode = []
|
|
self.cppCode = []
|
|
self.initializerCode = []
|
|
self.preInitializerCode = []
|
|
self.postInitializerCode = []
|
|
self.includes = []
|
|
self.imports = []
|
|
self.isARealModule = (module == name)
|
|
|
|
|
|
def parseCompleted(self):
|
|
"""
|
|
Called after the loading of items from the XML has completed, just
|
|
before the tweaking stage is done.
|
|
"""
|
|
|
|
# Reorder the items in the module to be a little more sane, such as
|
|
# enums and other constants first, then the classes and functions (since
|
|
# they may use those constants) and then the global variables, but perhaps
|
|
# only those that have classes in this module as their type.
|
|
one = list()
|
|
two = list()
|
|
three = list()
|
|
for item in self.items:
|
|
if isinstance(item, (ClassDef, FunctionDef)):
|
|
two.append(item)
|
|
elif isinstance(item, GlobalVarDef) and (
|
|
guessTypeInt(item) or guessTypeFloat(item) or guessTypeStr(item)):
|
|
one.append(item)
|
|
elif isinstance(item, GlobalVarDef):
|
|
three.append(item)
|
|
# template instantiations go at the end
|
|
elif isinstance(item, TypedefDef) and '<' in item.type:
|
|
three.append(item)
|
|
|
|
else:
|
|
one.append(item)
|
|
self.items = one + two + three
|
|
|
|
# give everything an isCore flag
|
|
global _globalIsCore
|
|
_globalIsCore = self.module == '_core'
|
|
for item in self.allItems():
|
|
item.isCore = _globalIsCore
|
|
|
|
|
|
|
|
def addHeaderCode(self, code):
|
|
if isinstance(code, list):
|
|
self.headerCode.extend(code)
|
|
else:
|
|
self.headerCode.append(code)
|
|
|
|
def addCppCode(self, code):
|
|
if isinstance(code, list):
|
|
self.cppCode.extend(code)
|
|
else:
|
|
self.cppCode.append(code)
|
|
|
|
def includeCppCode(self, filename):
|
|
with textfile_open(filename) as fid:
|
|
self.addCppCode(fid.read())
|
|
|
|
def addInitializerCode(self, code):
|
|
if isinstance(code, list):
|
|
self.initializerCode.extend(code)
|
|
else:
|
|
self.initializerCode.append(code)
|
|
|
|
def addPreInitializerCode(self, code):
|
|
if isinstance(code, list):
|
|
self.preInitializerCode.extend(code)
|
|
else:
|
|
self.preInitializerCode.append(code)
|
|
|
|
def addPostInitializerCode(self, code):
|
|
if isinstance(code, list):
|
|
self.postInitializerCode.extend(code)
|
|
else:
|
|
self.postInitializerCode.append(code)
|
|
|
|
def addInclude(self, name):
|
|
if isinstance(name, list):
|
|
self.includes.extend(name)
|
|
else:
|
|
self.includes.append(name)
|
|
|
|
def addImport(self, name):
|
|
if isinstance(name, list):
|
|
self.imports.extend(name)
|
|
else:
|
|
self.imports.append(name)
|
|
|
|
|
|
def addElement(self, element):
|
|
item = None
|
|
kind = element.get('kind')
|
|
if kind == 'class':
|
|
extractingMsg(kind, element, ClassDef.nameTag)
|
|
item = ClassDef(element, module=self)
|
|
self.items.append(item)
|
|
|
|
elif kind == 'struct':
|
|
extractingMsg(kind, element, ClassDef.nameTag)
|
|
item = ClassDef(element, kind='struct')
|
|
self.items.append(item)
|
|
|
|
elif kind == 'function':
|
|
extractingMsg(kind, element)
|
|
item = FunctionDef(element, module=self)
|
|
if not item.checkForOverload(self.items):
|
|
self.items.append(item)
|
|
|
|
elif kind == 'enum':
|
|
inClass = []
|
|
for el in self.items:
|
|
if isinstance(el, ClassDef):
|
|
inClass.append(el)
|
|
|
|
extractingMsg(kind, element)
|
|
item = EnumDef(element, inClass)
|
|
self.items.append(item)
|
|
|
|
elif kind == 'variable':
|
|
extractingMsg(kind, element)
|
|
item = GlobalVarDef(element)
|
|
self.items.append(item)
|
|
|
|
elif kind == 'typedef':
|
|
extractingMsg(kind, element)
|
|
item = TypedefDef(element)
|
|
self.items.append(item)
|
|
|
|
elif kind == 'define':
|
|
# if it doesn't have a value, it must be a macro.
|
|
value = flattenNode(element.find("initializer"))
|
|
if not value:
|
|
skippingMsg(kind, element)
|
|
else:
|
|
# NOTE: This assumes that the #defines are numeric values.
|
|
# There will have to be some tweaking done for items that are
|
|
# not numeric...
|
|
extractingMsg(kind, element)
|
|
item = DefineDef(element)
|
|
self.items.append(item)
|
|
|
|
elif kind == 'file' or kind == 'namespace':
|
|
extractingMsg(kind, element)
|
|
for node in element.findall('sectiondef/memberdef'):
|
|
self.addElement(node)
|
|
for node in element.findall('sectiondef/member'):
|
|
node = self.resolveRefid(node)
|
|
self.addElement(node)
|
|
|
|
else:
|
|
raise ExtractorError('Unknown module item kind: %s' % kind)
|
|
|
|
return item
|
|
|
|
def resolveRefid(self, node):
|
|
from etgtools import XMLSRC
|
|
refid = node.get('refid')
|
|
fname = os.path.join(XMLSRC, refid.rsplit('_', 1)[0]) + '.xml'
|
|
root = ET.parse(fname).getroot()
|
|
return root.find(".//memberdef[@id='{}']".format(refid))
|
|
|
|
|
|
def addCppFunction(self, type, name, argsString, body, doc=None, **kw):
|
|
"""
|
|
Add a new C++ function into the module that is written by hand, not
|
|
wrapped.
|
|
"""
|
|
md = CppMethodDef(type, name, argsString, body, doc, **kw)
|
|
self.items.append(md)
|
|
return md
|
|
|
|
|
|
def addCppFunction_sip(self, type, name, argsString, body, doc=None, **kw):
|
|
"""
|
|
Add a new C++ function into the module that is written by hand, not
|
|
wrapped.
|
|
"""
|
|
md = CppMethodDef_sip(type, name, argsString, body, doc, **kw)
|
|
self.items.append(md)
|
|
return md
|
|
|
|
|
|
def addPyCode(self, code, order=None, **kw):
|
|
"""
|
|
Add a snippet of Python code to the wrapper module.
|
|
"""
|
|
pc = PyCodeDef(code, order, **kw)
|
|
self.items.append(pc)
|
|
return pc
|
|
|
|
|
|
def addGlobalStr(self, name, before=None, wide=False):
|
|
if self.findItem(name):
|
|
self.findItem(name).ignore()
|
|
if wide:
|
|
gv = GlobalVarDef(type='const wchar_t*', name=name)
|
|
else:
|
|
gv = GlobalVarDef(type='const char*', name=name)
|
|
if before is None:
|
|
self.addItem(gv)
|
|
elif isinstance(before, int):
|
|
self.insertItem(before, gv)
|
|
else:
|
|
self.insertItemBefore(before, gv)
|
|
return gv
|
|
|
|
|
|
def includePyCode(self, filename, order=None):
|
|
"""
|
|
Add a snippet of Python code from a file to the wrapper module.
|
|
"""
|
|
with textfile_open(filename) as fid:
|
|
text = fid.read()
|
|
return self.addPyCode(
|
|
"#" + '-=' * 38 + '\n' +
|
|
("# This code block was included from %s\n%s\n" % (filename, text)) +
|
|
"# End of included code block\n"
|
|
"#" + '-=' * 38 + '\n' ,
|
|
order
|
|
)
|
|
|
|
|
|
def addPyFunction(self, name, argsString, body, doc=None, order=None, **kw):
|
|
"""
|
|
Add a Python function to this module.
|
|
"""
|
|
pf = PyFunctionDef(name, argsString, body, doc, order, **kw)
|
|
self.items.append(pf)
|
|
return pf
|
|
|
|
|
|
def addPyClass(self, name, bases=[], doc=None, items=[], order=None, **kw):
|
|
"""
|
|
Add a pure Python class to this module.
|
|
"""
|
|
pc = PyClassDef(name, bases, doc, items, order, **kw)
|
|
self.items.append(pc)
|
|
return pc
|
|
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Some helper functions and such
|
|
#---------------------------------------------------------------------------
|
|
|
|
def flattenNode(node, rstrip=True):
|
|
"""
|
|
Extract just the text from a node and its children, tossing out any child
|
|
node tags and attributes.
|
|
"""
|
|
# TODO: can we just use ElementTree.tostring for this function?
|
|
if node is None:
|
|
return ""
|
|
if isinstance(node, str):
|
|
return node
|
|
text = node.text or ""
|
|
for n in node:
|
|
text += flattenNode(n, rstrip)
|
|
if node.tail:
|
|
text += node.tail
|
|
if rstrip:
|
|
text = text.rstrip()
|
|
if rstrip:
|
|
text = text.rstrip()
|
|
return text
|
|
|
|
|
|
def prettifyNode(elem):
|
|
"""
|
|
Return a pretty-printed XML string for the Element. Useful for debugging
|
|
or better understanding of xml trees.
|
|
"""
|
|
from xml.etree import ElementTree
|
|
from xml.dom import minidom
|
|
rough_string = ElementTree.tostring(elem, 'utf-8')
|
|
reparsed = minidom.parseString(rough_string)
|
|
return reparsed.toprettyxml(indent=" ")
|
|
|
|
|
|
def appendText(node, text):
|
|
"""
|
|
Append some text to a docstring element, such as an item's briefDoc or
|
|
detailedDoc attributes.
|
|
"""
|
|
ele = makeTextElement(text)
|
|
node.append(ele) # will work for either briefDoc (an Element) or detailedDoc (a list)
|
|
|
|
def prependText(node, text):
|
|
"""
|
|
Prepend some text to a docstring element, such as an item's briefDoc or
|
|
detailedDoc attributes.
|
|
"""
|
|
# If the node has text then just insert the new bti as a string
|
|
if hasattr(node, 'text') and node.text:
|
|
node.text = text + node.text
|
|
|
|
# otherwise insert it as an element
|
|
else:
|
|
ele = makeTextElement(text)
|
|
node.insert(0, ele)
|
|
|
|
|
|
|
|
def makeTextElement(text):
|
|
element = ET.Element('para')
|
|
element.text = text
|
|
return element
|
|
|
|
|
|
class ExtractorError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def _print(value, indent, stream):
|
|
if stream is None:
|
|
stream = sys.stdout
|
|
indent = ' ' * indent
|
|
for line in str(value).splitlines():
|
|
stream.write("%s%s\n" % (indent, line))
|
|
|
|
def _pf(item, indent):
|
|
if indent == 0:
|
|
indent = 4
|
|
txt = pprint.pformat(item, indent)
|
|
if '\n' in txt:
|
|
txt = '\n' + txt
|
|
return txt
|
|
|
|
|
|
def verbose():
|
|
return '--verbose' in sys.argv
|
|
|
|
def extractingMsg(kind, element, nameTag='name'):
|
|
if verbose():
|
|
print('Extracting %s: %s' % (kind, element.find(nameTag).text))
|
|
|
|
def skippingMsg(kind, element):
|
|
if verbose():
|
|
print('Skipping %s: %s' % (kind, element.find('name').text))
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|