[pygccxml-commit] SF.net SVN: pygccxml: [313] pyplusplus_dev/contrib
Brought to you by:
mbaas,
roman_yakovenko
From: <mb...@us...> - 2006-07-18 08:32:55
|
Revision: 313 Author: mbaas Date: 2006-07-18 01:32:45 -0700 (Tue, 18 Jul 2006) ViewCVS: http://svn.sourceforge.net/pygccxml/?rev=313&view=rev Log Message: ----------- Added the alternative pypp_api contribution that was formerly known as the 'experimental' API. Added Paths: ----------- pyplusplus_dev/contrib/pypp_api/ pyplusplus_dev/contrib/pypp_api/generate_docs.py pyplusplus_dev/contrib/pypp_api/pypp_api/ pyplusplus_dev/contrib/pypp_api/pypp_api/__init__.py pyplusplus_dev/contrib/pypp_api/pypp_api/argpolicy.py pyplusplus_dev/contrib/pypp_api/pypp_api/decltypes.py pyplusplus_dev/contrib/pypp_api/pypp_api/declwrapper.py pyplusplus_dev/contrib/pypp_api/pypp_api/filters.py pyplusplus_dev/contrib/pypp_api/pypp_api/modulebuilder.py pyplusplus_dev/contrib/pypp_api/pypp_api/selection.py pyplusplus_dev/contrib/pypp_api/pypp_api/treerange.py Added: pyplusplus_dev/contrib/pypp_api/generate_docs.py =================================================================== --- pyplusplus_dev/contrib/pypp_api/generate_docs.py (rev 0) +++ pyplusplus_dev/contrib/pypp_api/generate_docs.py 2006-07-18 08:32:45 UTC (rev 313) @@ -0,0 +1,12 @@ +# Generate documentation using epydoc +# +import os, os.path + +default_format = "epytext" + + +cmd_line = "epydoc -o html --docformat %s pypp_api"%(default_format) + +print "Running: ", cmd_line +os.system(cmd_line) + Added: pyplusplus_dev/contrib/pypp_api/pypp_api/__init__.py =================================================================== --- pyplusplus_dev/contrib/pypp_api/pypp_api/__init__.py (rev 0) +++ pyplusplus_dev/contrib/pypp_api/pypp_api/__init__.py 2006-07-18 08:32:45 UTC (rev 313) @@ -0,0 +1,72 @@ +# Copyright 2004 Roman Yakovenko. +# Distributed under the Boost Software License, Version 1.0. (See +# accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) + +"""This is an experimental module to high-level API testing. + +This module provides a simplified high-level API for using +pygccxml and pyplusplus. It is currently under heavy construction. + +Quickstart Usage +================ + + 1. Create an instance of the L{ModuleBuilder} class that serves as main + entry point for controlling pygccxml and pyplusplus. Parameters for + configuring pygccxml/pyplusplus can be passed in the constructor:: + + from pypp_api import * + + mb = ModuleBuilder(...) + + 2. Parse the header files using L{parse()<ModuleBuilder.parse>} and + customize your bindings:: + + toplevel_namespace = mb.parse() + + MyClass = toplevel_namespace.Class("MyClass") + MyClass.expose() + + MyClass.Method("foo").setPolicy(return_internal_reference()) + ... + + 3. Write the C++ source files using + L{writeModule()<ModuleBuilder.writeModule>}:: + + mb.writeModule() + + +Detailed Usage +============== + +Add detailed usage here. + +@todo: Add quickstart and detailed documentation for API. +@author: Allen Bierbaum +@author: Matthias Baas +@license: Boost Software License, Version 1.0 (see http://www.boost.org/LICENSE_1_0.txt) +@copyright: 2006 Allen Bierbaum, Matthias Baas +""" + + +# Bring in call policies to use +from pyplusplus.decl_wrappers import return_self +from pyplusplus.decl_wrappers import return_internal_reference +from pyplusplus.decl_wrappers import with_custodian_and_ward +from pyplusplus.decl_wrappers import copy_const_reference +from pyplusplus.decl_wrappers import copy_non_const_reference +from pyplusplus.decl_wrappers import manage_new_object +from pyplusplus.decl_wrappers import reference_existing_object +from pyplusplus.decl_wrappers import return_by_value +from pyplusplus.decl_wrappers import return_opaque_pointer +from pyplusplus.decl_wrappers import return_value_policy + +from pygccxml.declarations import ACCESS_TYPES +PUBLIC = ACCESS_TYPES.PUBLIC +PROTECTED = ACCESS_TYPES.PROTECTED +PRIVATE = ACCESS_TYPES.PRIVATE + +from decltypes import arg, cpp +from argpolicy import * + +from modulebuilder import ModuleBuilder Added: pyplusplus_dev/contrib/pypp_api/pypp_api/argpolicy.py =================================================================== --- pyplusplus_dev/contrib/pypp_api/pypp_api/argpolicy.py (rev 0) +++ pyplusplus_dev/contrib/pypp_api/pypp_api/argpolicy.py 2006-07-18 08:32:45 UTC (rev 313) @@ -0,0 +1,574 @@ +# Copyright 2006 Roman Yakovenko. +# Distributed under the Boost Software License, Version 1.0. (See +# accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) +# +# Initial author: Matthias Baas + +import sys, os.path, copy +from pygccxml import declarations +from pyplusplus import code_creators +from pyplusplus import decl_wrappers +from pyplusplus import file_writers +from declwrapper import IDecl + +# ArgPolicyManager +class ArgPolicyManager: + """Manages all argument policies for a project. + + Before the code creator tree is created, setArgPolicy() has to be + called on any method that requires this decoration. + After the decoration is done and the code creator tree has been + built, the updateCreators() method has be called to update the tree. + Finally, the writeFiles() method can be called to write the additional + wrapper files. + """ + + def __init__(self, includePaths=[]): + """Constructor. + + The includePaths argument should be the same list that was passed + to the module builder constructor. It is used to remove the + unnecessary parts from include file names. + + @param includePaths: List of include paths + @type includePaths: list of str + """ + + self.includePaths = includePaths + + # Key: Class declaration / Value: list of WrapperManagers + self.classes = {} + + # Key: Include file name / Value: Irrelevant + self.includes = {} + + # The root of the code creator tree + self.creator_root = None + + # setArgPolicy + def setArgPolicy(self, decl, policies): + """Assign argument policies to a declaration. + + @param decl: The calldef declaration that receives the policies + @param policies: A sequence of policy objects. + """ + # Get the class declaration that belongs to decl + cls = self._classDecl(decl) + if cls==None: + raise NotImplemented, "Arg policies on free functions are not supported yet" + # Create the wrapper "code creator"... + wm = WrapperManager(decl, policies) + wm.wrappername = self._wrapperName(cls, decl) + if cls in self.classes: + self.classes[cls].append(wm) + else: + self.classes[cls] = [wm] + + # Ignore the declaration and replace it with the wrapper... + decl.exclude() + arglist = wm.wrapperargs[1:] + if len(arglist)>0: + dummydecl = decl_wrappers.member_function_t(arguments=arglist) +# c = code_creators.function_t(dummydecl) + c = code_creators.mem_fun_t(dummydecl) +# args = c._keywords_args() + args = c.keywords_args() + cls.add_code('def( "%s", %s, %s)'%(decl.alias, wm.wrappername, args)) + else: + cls.add_code('def( "%s", %s)'%(decl.alias, wm.wrappername)) +# IDecl([cls]).cdef(decl.alias, wm.wrappername) + # Add the header to the include files... + incname = self._outputFilename(cls)+file_writers.multiple_files_t.HEADER_EXT + if incname not in self.includes: + self.includes[incname] = 1 + + # updateCreators + def updateCreators(self, root): + """Modifies the code creator tree. + + This method has to be called after the code creator tree was built + and before the files were written. + + @param root: The root of the code creator tree + @type root: module_t + """ + self.creator_root = root + for inc in self.includes: + root.adopt_include(code_creators.include_t(inc)) + + # writeFiles + def writeFiles(self, path): + """Write additional source files. + + @param path: The output directory + @type path: str + """ + + # Key: sigid / Value: list of (decl, proto) + sigids = {} + # This dict contains the ambiguous wrappers with the same sigid + # Key: sigid / Value: Reference to list in sigids + ambiguous = {} + + for cls in self.classes: + basename = self._outputFilename(cls) + hname = basename+file_writers.multiple_files_t.HEADER_EXT + cppname = basename+file_writers.multiple_files_t.SOURCE_EXT + + guard = "__%s_pyplusplus_wrapper__"%cls.alias + hsrc = "#ifndef %s\n"%guard + hsrc += "#define %s\n\n"%guard + hsrc += "#include <boost/python.hpp>\n" + # Add the include file where the original class is defined... + incd = code_creators.include_directories_t() + incd.user_defined = self.includePaths + header = incd.normalize_header(cls.location.file_name) + hsrc += "#include <%s>\n\n"%header + + # Include all 'global' include files... + cppsrc = "" + include_creators = filter(lambda creator: isinstance( creator, code_creators.include_t) +# and not isinstance(creator, code_creators.precompiled_header_t) + , self.creator_root.creators) + includes = map(lambda include_creator: include_creator.create() + , include_creators ) + cppsrc += os.linesep.join(includes) + cppsrc += "\n\n" + +# cppsrc += '#include "%s"\n\n'%hname + wms = self.classes[cls] + for wm in wms: + sigid,proto,src = wm.create() + if sigid in sigids: + sigids[sigid].append((wm.decl, proto)) + ambiguous[sigid] = sigids[sigid] + else: + sigids[sigid] = [(wm.decl, proto)] + hsrc += "%s;\n"%proto + cppsrc += (60*"/")+"\n" + cppsrc += "// Original:\n// %s\n"%wm.decl + cppsrc += (60*"/")+"\n" + cppsrc += "%s\n"%src + + hsrc += "\n#endif\n" + + file_writers.writer_t.write_file(os.path.join(path, hname), hsrc) + file_writers.writer_t.write_file(os.path.join(path, cppname), cppsrc) + + # Have there been any ambiguous wrappers? + if ambiguous!={}: + print 70*"=" + print "ERROR: There are ambiguous wrapper functions:" + for sigid in ambiguous: + print 70*"-" + for i, (decl,proto) in enumerate(ambiguous[sigid]): + print "(%d) %s"%(i+1, decl) + print "->" + for i, (decl,proto) in enumerate(ambiguous[sigid]): + print "(%d) %s"%(i+1, proto) + print 70*"-" + print "***ABORTING because of %d ambiguous wrapper functions***"%(len(ambiguous)) + sys.exit(1) + + + def _wrapperName(self, clsdecl, memberdecl): + """Return the name of the wrapper function. + """ + # The id of the memberdecl is added to make the name unique if there + # are several (overloaded) functions with the same name +# declid = abs(id(memberdecl)) + # Use a hash of the declaration string (i.e. the signature) because + # this doesn't change the source file between runs + declid = hex(abs(hash(str(memberdecl)))) + return "%s_%s_wrapper_%s"%(clsdecl.alias, memberdecl.alias, declid) + + def _outputFilename(self, cls): + """Return the output file name of the wrapper file (without extension). + """ + return "%s_wrappers"%cls.alias + + # _classDecl + def _classDecl(self, decl): + """Return the class declaration that belongs to a member declaration. + """ + while decl.parent!=None: + parent = decl.parent + if isinstance(parent, declarations.class_t): + return parent + decl = parent + return None + + +class ArgTransformerBase: + """Base class for an argument policy class. + """ + def __init__(self): + pass + +# def requireInclude(self, include): +# pass + + def prepareWrapper(self, wm): + pass + + def preCall(self, wm): + pass + + def postCall(self, wm): + pass + + def cleanup(self, wm): + pass + +# Output +class Output(ArgTransformerBase): + def __init__(self, idx): + ArgTransformerBase.__init__(self) + self.idx = idx + + def __str__(self): + return "Output(%d)"%(self.idx) + + def prepareWrapper(self, wm): + arg = wm.removeArg(self.idx) + + reftype = arg.type + if not (isinstance(reftype, declarations.reference_t) or + isinstance(reftype, declarations.pointer_t)): + raise ValueError, 'Output variable %d ("%s") must be a reference or a pointer (got %s)'%(self.idx, arg.name, arg.type) + + wm.declareLocal(arg.name, str(reftype.base)) + wm.appendResult(arg.name) + + if isinstance(reftype, declarations.pointer_t): + wm.replaceCallArg(self.idx, "&%s"%arg.name) + +# InputArray +class InputArray(ArgTransformerBase): + def __init__(self, idx, size): + ArgTransformerBase.__init__(self) + self.idx = idx + self.size = size + self.argname = None + self.basetype = None + self.carray = None + self.ivar = None + + def __str__(self): + return "InputArray(%d,%d)"%(self.idx, self.size) + + def prepareWrapper(self, wm): + # Remove the original argument... + arg = wm.removeArg(self.idx) + if not (isinstance(arg.type, declarations.pointer_t) or + isinstance(arg.type, declarations.array_t)): + raise ValueError, "Argument %d (%s) must be a pointer."%(self.idx, arg.name) + + # ...and replace it with a Python object. + newarg = declarations.argument_t(arg.name, "boost::python::object") + wm.insertArg(self.idx, newarg) + + self.argname = arg.name + self.basetype = str(arg.type.base).replace("const", "").strip() + + # Declare a variable that will hold the C array... + self.carray = wm.declareLocal("c_"+arg.name, self.basetype, size=self.size) + # and a int which is used for the loop + self.ivar = wm.declareLocal("i", "int") + + wm.replaceCallArg(self.idx, self.carray) + + def preCall(self, wm): + res = "" + res += "// Copy the sequence '%s' into '%s'...\n"%(self.argname, self.carray) + res += 'if (%s.attr("__len__")()!=%d)\n'%(self.argname, self.size) + res += '{\n' + res += ' PyErr_SetString(PyExc_ValueError, "Invalid sequence size (expected %d)");\n'%self.size + res += ' boost::python::throw_error_already_set();\n' + res += '}\n' + res += "for(%s=0; %s<%d; %s++)\n"%(self.ivar, self.ivar, self.size, self.ivar) + res += " %s[%s] = boost::python::extract< %s >(%s[%s]);"%(self.carray, self.ivar, self.basetype, self.argname , self.ivar) + return res + +# OutputArray +class OutputArray(ArgTransformerBase): + def __init__(self, idx, size): + ArgTransformerBase.__init__(self) + self.idx = idx + self.size = size + self.argname = None + self.basetype = None + self.pyval = None + self.cval = None + self.ivar = None + + def __str__(self): + return "OutputArray(%d,%d)"%(self.idx, self.size) + + def prepareWrapper(self, wm): + # Remove the original argument... + arg = wm.removeArg(self.idx) + if not (isinstance(arg.type, declarations.pointer_t) or + isinstance(arg.type, declarations.array_t)): + raise ValueError, "Argument %d (%s) must be a pointer."%(self.idx, arg.name) + + self.argname = arg.name + self.basetype = str(arg.type.base).replace("const", "").strip() + + # Declare a variable that will hold the C array... + self.cval = wm.declareLocal("c_"+self.argname, self.basetype, size=self.size) + # Declare a Python list which will receive the output... + self.pyval = wm.declareLocal(self.argname, "boost::python::list") + # ...and add it to the result + wm.appendResult(arg.name) + + # Declare an int which is used for the loop + self.ivar = wm.declareLocal("i", "int") + + wm.replaceCallArg(self.idx, self.cval) + + def postCall(self, wm): + res = "" + res += "// Copy the sequence '%s' into '%s'...\n"%(self.cval, self.pyval) + res += "for(%s=0; %s<%d; %s++)\n"%(self.ivar, self.ivar, self.size, self.ivar) + res += " %s.append(%s[%s]);"%(self.pyval, self.cval , self.ivar) + return res + + +# WrapperManager +class WrapperManager: + """ + represents source code for a wrapper function/method + """ + + def __init__(self, decl, transformers): + + self.decl = decl + self.transformers = transformers + + # The original function name + self.funcname = decl.name + + # Create the arg list of the original function (including the + # return type) + argnames = map(lambda a: a.name, decl.arguments) + if str(decl.return_type)=="void": + ret = None + argnames = [None]+argnames + else: + resname = self._makeNameUnique("res", argnames) + ret = declarations.argument_t(resname, decl.return_type, None) + argnames = [resname]+argnames + self.funcargs = [ret]+decl.arguments + # Initialize the argument strings used for invoking the original + # function + self.callargs = argnames + + # The name of the wrapper function + self.wrappername = "%s_wrapper"%decl.name + # The arguments of the wrapper function + self.wrapperargs = copy.copy(self.funcargs) + + # Result tuple + self.resulttuple = [] + + self.declared_vars = {} + self.local_var_list = [] + + # Return the original return value by default + if self.funcargs[0]!=None: + self.appendResult(self.funcargs[0].name) + + for trans in self.transformers: + trans.prepareWrapper(self) + + + # removeArg + def removeArg(self, idx): + """Remove an argument from the wrapper function. + + @param idx: Argument index of the original function + @type idx: int + @returns: Returns the argument_t object that was removed (or None + if idx is 0 and the function has no return type) + """ + if idx==0 and self.funcargs[0]==None: + raise ValueError, 'Function "%s" has no return value'%self.decl + argname = self.funcargs[idx].name + for i,arg in enumerate(self.wrapperargs): + if arg!=None: + if arg.name==argname: + res = self.wrapperargs[i] + if i>0: + del self.wrapperargs[i] + else: + self.wrapperargs[0] = None + return res + + raise ValueError, 'Argument %d ("%s") not found on the wrapper function'%(idx, argname) + + # insertArg + def insertArg(self, idx, arg): + """Insert a new argument into the argument list of the wrapper function. + + @param idx: New argument index + @type idx: int + @param arg: New argument object + @type arg: argument_t + """ + if idx>=0: + self.wrapperargs.insert(idx, arg) + else: + self.wrapperargs.insert(len(self.wrapperargs)+idx+1, arg) + + # appendResult + def appendResult(self, varname): + self.resulttuple.append(varname) + + # removeResult + def removeResult(self, varname): + self.resulttuple.remove(varname) + + # replaceCallArg + def replaceCallArg(self, idx, callstr): + self.callargs[idx] = callstr + + # declareLocal + def declareLocal(self, name, type, size=None, default=None): + """Declare a local variable and return its final name. + """ + name = self._makeNameUnique(name, self.declared_vars) + self.declared_vars[name] = (type,size,default) + self.local_var_list.append((name, type, size, default)) + return name + + # create + def create(self): + """ + Returns the signature id, the prototype and the function definition. + """ + + # Declare the args that are "output" only and that have been + # converted to local variables... +# newargnames = map(lambda a: a.name, self.wrapperargs[1:]) +# for a in self.funcargs[1:]: +# if a.name not in newargnames: +# self.declareLocal(a.name, str(a.type)) + + if self.funcargs[0]!=None: + self.declareLocal(self.funcargs[0].name, self.funcargs[0].type) + + if len(self.resulttuple)==0: + rettype = "void" + elif len(self.resulttuple)==1: + varname = self.resulttuple[0] + rettype = self.declared_vars[varname][0] + elif len(self.resulttuple)>1: + rettype = "boost::python::tuple" +# self.wrapperargs[0].type = boost_python_t("tuple") + +# if self.wrapperargs[0]==None: +# rettype = "void" +# else: +# rettype = str(self.wrapperargs[0].type) + + # Function header... + a = map(lambda a: str(a), self.wrapperargs[1:]) + asig = map(lambda a: str(a.type), self.wrapperargs[1:]) + cls = self._classDecl(self.decl) + if cls!=None: + a = ["%s& self"%cls.name]+a + asig = ["%s&"%cls.name]+asig + proto = "%s %s(%s)"%(rettype, self.wrappername, ", ".join(a)) + sigid = "%s(%s)"%(self.wrappername, ", ".join(asig)) + # Remove the default values for the function definition... + a2 = map(lambda s: s.split("=")[0], a) + proto2 = "%s %s(%s)"%(rettype, self.wrappername, ", ".join(a2)) + res = "%s\n{\n"%(proto2) + + # Add the declarations... + for varname,type,size,default in self.local_var_list: + if default==None: + res += " %s %s"%(type, varname) + else: + res += " %s %s = %s"%(type, varname, default) + if size!=None: + res += "[%d]"%size + res += ";\n" + res += "\n" + + # Precall code... + src = map(lambda trans: trans.preCall(self), self.transformers) + src = filter(lambda x: x!=None, src) + src = "\n\n".join(src) + res += self._indent(2, src) +# for trans in self.transformers: +# code = trans.preCall(self) +# if code!=None: +# res += self._indent(2, code) + + # Add the function call... + res += "\n // Invoke the wrapped function\n" + res += " " + if self.callargs[0]!=None: + res += "%s = "%self.callargs[0] + if cls!=None: + res += "self." + res += "%s(%s);\n\n"%(self.funcname, ", ".join(self.callargs[1:])) + + # Postcall code... + src = map(lambda trans: trans.postCall(self), self.transformers) + src = filter(lambda x: x!=None, src) + src = "\n\n".join(src) + res += self._indent(2, src) +# for trans in self.transformers: +# code = trans.postCall(self) +# if code!=None: +# res += self._indent(2, code) + + # Return the result + if len(self.resulttuple)==1: + res += " return %s;\n"%self.resulttuple[0] + elif len(self.resulttuple)>1: + res += " return boost::python::make_tuple(%s);\n"%(", ".join(self.resulttuple)) + + res += "}\n" + return sigid, proto, res.replace("\n\n\n", "\n\n") + + # _indent + def _indent(self, n, code): + """Indent source code. + """ + if code=="": + return "" + return "\n".join(map(lambda s: (n*" ")+s, code.split("\n")))+"\n" + + # _classDecl + def _classDecl(self, decl): + """Return the class declaration that belongs to a member declaration. + """ + while decl.parent!=None: + parent = decl.parent + if isinstance(parent, declarations.class_t): + return parent + decl = parent + return None + + # _makeNameUnique + def _makeNameUnique(self, name, names): + """Make a variable name unique so that there's no clash with other names. + + names must be a dict or a list. + """ + if name not in names: + return name + + n = 2 + while 1: + newname = "%s_%d"%(name, n) + if newname not in names: + return newname + n += 1 + + Added: pyplusplus_dev/contrib/pypp_api/pypp_api/decltypes.py =================================================================== --- pyplusplus_dev/contrib/pypp_api/pypp_api/decltypes.py (rev 0) +++ pyplusplus_dev/contrib/pypp_api/pypp_api/decltypes.py 2006-07-18 08:32:45 UTC (rev 313) @@ -0,0 +1,73 @@ +# Copyright 2006 Roman Yakovenko. +# Distributed under the Boost Software License, Version 1.0. (See +# accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) +# +# Initial author: Matthias Baas + +"""This module contains some basic definitions. +""" + +NAMESPACE = 0x01 +CLASS = 0x02 +MEMBER_FUNCTION = 0x04 +METHOD = MEMBER_FUNCTION # this also includes constructors +FREE_FUNCTION = 0x08 +FUNCTION = FREE_FUNCTION +CONSTRUCTOR = 0x10 # limit to constructors +ENUM = 0x20 +VARIABLE = 0x40 + +CALLABLE = METHOD | FUNCTION | CONSTRUCTOR + +# cpp +class cpp: + """This class wraps C++ source code for default values. + + This class is used together with the 'arg' class to provide + C++ source code as default value. Example: + + method.cdef("foo", "&Foo::foo", arg("ptr", cpp("bp::object()"))) + + The cpp class prevents the generation of apostrophes (as it would + happen when only a string would be passed). + """ + def __init__(self, src): + self.src = src + + def __str__(self): + return self.src + +# arg +class arg: + """Provide keyword arguments for methods/functions. + + This class is equivalent to the Boost.Python arg class + and is used together with the Decl.cdef method. + """ + + def __init__(self, name, default=None): + """Constructor. + + @param name: Argument name + @type name: str + @param default: Optional default value + """ + self.name = name + self.default = default + + def __str__(self): + res = 'arg("%s")'%self.name + if self.default!=None: + res += "=%s"%self.py2cpp(self.default) + return res + + def py2cpp(self, val): + """Convert a Python value to a C++ value. + """ + if type(val)==bool: + return str(val).lower() + elif type(val)==str: + return '"%s"'%val + else: + return str(val) Added: pyplusplus_dev/contrib/pypp_api/pypp_api/declwrapper.py =================================================================== --- pyplusplus_dev/contrib/pypp_api/pypp_api/declwrapper.py (rev 0) +++ pyplusplus_dev/contrib/pypp_api/pypp_api/declwrapper.py 2006-07-18 08:32:45 UTC (rev 313) @@ -0,0 +1,748 @@ +# Copyright 2006 Roman Yakovenko. +# Distributed under the Boost Software License, Version 1.0. (See +# accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) +# +# Initial author: Matthias Baas + +"""This module contains the 'Declaration wrapper' object. +""" + +import sys, os.path, inspect, types +from filters import * +import decltypes +import pygccxml +import pyplusplus +import selection + +# Create an alias for the builtin type() function +_type = type + +allow_empty_queries = False +default_recursive = False +query_log = None +decoration_log = None + +# If this is set to True an attempt to decorate a declaration will +# result in an error (this is set after the code creators have been created) +decl_lock = False + +# IDecl +class IDecl: + """Declaration interface. + + This class represents the interface to the declaration tree. Its + main purpose is to "decorate" the nodes in the tree with + information about how the binding is to be created. Instances of + this class are never created by the user, instead they are + returned by the API. + + You can think of this class as a container for declaration nodes + that are selected by matching a set of filters. + + @group Selection interface: Decl, Namespace, Class, Constructor, Method, Function, Enum, Var, Decls, Namespaces, Classes, Constructors, Methods, Functions, Enums, Vars + @group Decoration interface: expose, ignore, exclude, finalize, rename, setPolicy, setArgPolicy, disableKeywordArgs, setHeldType, addMethod, cdef, staticmethod + """ + + def __init__(self, decls, filter=None, modulebuilder=None): + """Constructor. + + @param decls: One or more declarations that should be stored in this instance + @type decls: declaration_t or list of declaration_t + @param filter: Filter string (only for informational purposes) + @type filter: str + @param modulebuilder: The module builder object this object belongs to + """ + global query_log + + self.modulebuilder = modulebuilder + + if type(decls)!=list: + decls = [decls] + + # A sequence containing the underlying pygccxml declaration nodes. + # (actually these are decl_wrapper nodes) + self.decl_handles = decls + + # Determine where this instance was created... + filename,funcname,linenr,sourceline = self._sourceInfo() + self.filename = filename + self.funcname = funcname + self.linenr = linenr + self.sourceline = sourceline + + # Dump info about this query... + if query_log: + print >>query_log, 70*"-" + if funcname==None: + print >>query_log, "%s, %d: %s"%(self.filename, self.linenr, self.sourceline) + else: + print >>query_log, "%s, %s(), %d: %s"%(self.filename, self.funcname, self.linenr, self.sourceline) + if filter!=None: + print >>query_log, "Filter: %s"%filter + for decl in self.decl_handles: + print >>query_log, " -> %s"%decl + if len(self.decl_handles)==0: + print >>query_log, " <no result>" + elif len(self.decl_handles)>1: + print >>query_log, " (%d declarations)"%len(self.decl_handles) + + + def __str__(self): + """Return a descriptive string.""" + if len(self.decl_handles)==0: + return "Decl: <empty>" + elif len(self.decl_handles)==1: + ds = getattr(self.decl_handles[0], "name", "?") + return 'Decl: "%s"'%(ds) + else: + return 'Decl: (%d declarations)'%(len(self.decl_handles)) + + def __iter__(self): + return self.iterContained() + + # iterContained + def iterContained(self): + """Iterate over all contained nodes. + + The iterator yields Decl objects. + """ + for decl in self._iterContained(): + yield IDecl([decl], modulebuilder=self.modulebuilder) + + # expose + def expose(self, flag=True): + """Expose the declarations in the generated module. + + If flag is True all contained declarations are marked + for being exposed, otherwise they are marked for being ignored. + + @param flag: Determines whether the declaration is actually exposed or ignored. + @type flag: bool + @returns: Returns self + @see: L{ignore()} + """ + self._checkLock() + for d in self._iterContained(): + if decoration_log!=None: + self._logDecoration("expose", d) + d.include() + return self + + # ignore + def ignore(self, flag=True): + """Ignore the declarations in the generated module. + + If flag is True all contained declarations are marked + for being ignored, otherwise they are marked for being exposed. + + @param flag: Determines whether the declaration is actually ignored or exposed. + @type flag: bool + @return: Returns self + @see: L{expose()} + """ + self._checkLock() + for d in self._iterContained(): + if decoration_log!=None: + self._logDecoration("ignore", d) + d.exclude() + return self + + # exclude + def exclude(self, flag=True): + """This is an alias for L{ignore()}.""" + return self.ignore(flag) + + # rename + def rename(self, name): + """Rename a declaration. + + The declaration will receive a new name under which it is exposed + in the Python module. + If there are currently several declarations referenced the new + name is assigned to all of them. However, only the declarations + that were directly matched will receive a new name, children are + always ignored. + + @param name: New name for the declaration + @type name: str + """ + self._checkLock() + for decl in self._iterContained(recursive=False): + if decoration_log!=None: + self._logDecoration("rename(%s)"%name, decl) + decl.rename(name) + return self + + # finalize + def finalize(self): + """Finalize virtual member functions or an entire class. + + Prevents the generation of wrappers for virtual member functions. + """ + self._checkLock() + for decl in self._iterContained(): + if decoration_log!=None: + self._logDecoration("finalize", decl) + decl.finalize() + return self + + # setPolicy + def setPolicy(self, policy): + """Set policies for functions or methods. + + @param policy: Policy to apply to the contained functions/methods. + @type policy: ...policy... + """ + self._checkLock() + for decl in self._iterContained(): + if decoration_log!=None: + self._logDecoration("setPolicy(...)", decl) + decl.call_policies = policy + + # setHeldType + def setHeldType(self, heldtype): + """Explicitly set the held type. + + Ex: C{setHeldType("boost::shared_ptr<Class>")} + """ + self._checkLock() + for decl in self._iterContained(): + if decoration_log!=None: + self._logDecoration("setHeldType(%s)"%heldtype, decl) + decl.held_type = heldType + return self + + # disableKeywordArgs + def disableKeywordArgs(self): + """Disable generation of keyword arguments. + """ + + self._checkLock() + for decl in self._iterContained(): + if decoration_log!=None: + self._logDecoration("disableKeyWord", decl) + decl.use_keywords = False +# if ( isinstance(decl, calldef_t) and +# not isinstance(decl, destructor_t) and +# getattr(decl, "access_type", None)!=PRIVATE): +# decl._use_keywords = False + return self + + # setArgPolicy + def setArgPolicy(self, *policies): + """Append argument policies. + + This method takes a variable number of arguments. Each argument + must be an ArgPolicy object. + """ + self._checkLock() + for decl in self._iterContained(): + if decoration_log!=None: + ps = ", ".join(map(lambda x: str(x), policies)) + self._logDecoration("setArgPolicy(%s)"%ps, decl) + self.modulebuilder.mArgPolicyManager.setArgPolicy(decl, policies) + + # addMethod + def addMethod(self, name, impl): + """Add a new method to a class. + + Adds a new method to a class. The implementation is given as a + C/C++ function that is defined elsewhere. + The return value is a Decl object that can be + used to further customize the method. + + @Note: This method isn't implemented yet! + + @param name: The method name as it will appear in the Python module + @type name: str + @param impl: The name of the C/C++ function that implements the method. + @type impl: str + """ + self.cdef(name, impl) + + + # def + def cdef(self, name, fn=None, *args): + """Apply a raw def() statement. + + This method is equivalent to the Boost.Python def() method. + Example:: + + Class("Foo").cdef("spam", "cspam", return_internal_reference(), (arg("a"), arg("b", 0)), "The spam method") + + It is up to the user to ensure that the C/C++ function cspam + is declared and implemented somewhere. + + If fn is None, the string name is not quoted. You can use this form + to wrap constructors or operators. Example:: + + Class("Foo").cdef("bp::init< const MFoo& >()") + + @param name: Name of the Python method or a valid Boost.Python construct + @type name: str + @param fn: Name of the C++ function that implements the method or None + @type fn: str + @param args: There can be up to three additional arguments in any order: A doc string, the call policies and the keywords. + @see: L{staticmethod()} + """ + + self._checkLock() + doc,policies,keywords = self._parseDefArgs(args) + if fn==None: + args = ['%s'%name] + else: + args = ['"%s"'%name, fn] + if policies!=None: + pass # todo + if keywords!=None: + args.append("(%s)"%", ".join(map(lambda x: "bp::"+str(x), keywords))) + if doc!=None: + a = map(lambda x: "%s\\n"%x, doc.split("\n")) + while len(a)>0 and a[-1]=="\\n": + a = a[:-1] + if len(a)>0: + # Remove the newline in the last line + a[-1] = a[-1][:-2] + args.append('%s'%"\n".join(map(lambda x: '"%s"'%x, a))) + src = 'def( %s )'%(", ".join(args)) + for decl in self._iterContained(recursive=False): + # Add the 'def' source code... + decl.add_code(src) + + return self + + # staticmethod + def staticmethod(self, name): + """Apply a raw staticmethod() statement. + + @param name: Name of the method. + @type name: str + @see: L{cdef()} + """ + + self._checkLock() + for decl in self._iterContained(recursive=False): + src = 'staticmethod( "%s" )'%name + decl.add_code(src) + + return self + + + # Decl + def Decls(self, + name=None, + fullname=None, + type=None, + retval=None, + args=None, + anyarg=None, + signature=None, + header=None, + headerdir=None, + accesstype=None, + filter=None, + recursive=None, + allow_empty=None, + assert_count=None + ): + """Obtain a Decl object referencing one or more declarations. + + Filters all contained declarations and returns a new Decl + object that only contains declarations matching the filtering + rules as specified by the arguments. If an argument is None, + that particular filtering operation is disabled. If several + arguments are provided, all of them must be matched. + + For any filter that is based on strings (such as name) the + following rules apply: + + - A string must match exactly the corresponding attribute of the + declaration (C{name="wxFrame"} will only return the class + "wxFrame"). + - A string that is bracketed by a leading and trailing slash '/' is + interpreted as a regular expression (C{name="/wx.*/"} will return + all classes that begin with "wx"). + + Any argument can also be passed a list of values which duplicates + the filter. These filter are concatenated with OR, so a declaration + has to match only one of the filters. For example, you can select all + classes starting with either "wx" or "WX" by setting + C{name=["/wx.*/", "/WX.*/"}]. + + The user defined filter function filter must accept a Decl + object as argument and has to return True when the declaration + is matched. + + @param name: Select declarations by name + @type name: str + @param fullname: Select declarations by name (which includes namespaces) + @type fullname: str + @param type: Select declarations by type. The type is given by a combination of flags (CLASS, MEMBER_FUNCTION/METHOD, FREE_FUNCTION/FUNCTION, ENUM, ...) + @type type: int + @param retval: Select functions/methods based on their return value (this implies the type flags MEMBER_FUNCTION | FREE_FUNCTION) + @type retval: str + @param args: Select functions/methods bases on their arguments (this implies the type flags MEMBER_FUNCTION | FREE_FUNCTION) + @type args: list of str + @param anyarg: Select all functions/methods that have the specified argument somewhere in their argument list (this implies the type flags MEMBER_FUNCTION | FREE_FUNCTION) + @type anyarg: str + @param signature: Select declarations by their signature (this implies the type flags MEMBER_FUNCTION | FREE_FUNCTION) + @type signature: str + @param header: Select declarations by the header file in which they are defined + @type header: str + @param headerdir: Select declarations by the directory in which their header file is located + @type headerdir: str + @param accesstype: Access type (PUBLIC or PROTECTED). This implies the type flag MEMBER_FUNCTION. + @param filter: User defined filter function + @type callable + @param recursive: Extend the search to grandchildren? If not specified, a global (customizable) default value is used. + @type recursive: bool + @param allow_empty: Allow empty results. If not specified, a global (customizable) default value is used. + @type allow_empty: bool + @param assert_count: Check the number of matched declarations in the resulting Decl object + @type assert_count: int + @returns: Returns a Decl object that may reference an arbitrary number of declarations. + @rtype: IDecl + @see: Namespace(), Class(), Method(), Function(), Enum() + """ + global allow_empty_queries, default_recursive + + itype = 0 + filters = [] + + if recursive==None: + recursive = default_recursive + if allow_empty==None: + allow_empty = allow_empty_queries + + def addFilter(arg, filtercls): + if arg!=None: + if _type(arg)==list: + filters.append(OrFilter(map(lambda x: filtercls(x), arg))) + else: + filters.append(filtercls(arg)) + + # name filter + addFilter(name, NameFilter) + # fullname filter + addFilter(fullname, FullNameFilter) + # retval filter + if retval!=None: + addFilter(retval, RetValFilter) + itype |= CALLABLE + # args filter + if args!=None: + filters.append(ArgsFilter(args)) + itype |= CALLABLE + # anyarg filter + if anyarg!=None: + raise NotImplementedError, "anyarg filter is not yet implemented" + # signature filter + if signature!=None: + raise NotImplementedError, "signature filter is not yet implemented" + # header filter + addFilter(header, HeaderFilter) + # headerdir filter + addFilter(headerdir, HeaderDirFilter) + # accesstype filter + if accesstype!=None: + addFilter(accesstype, AccessTypeFilter) + itype |= METHOD + # custom filters + if filter!=None: + if _type(filter)==list: + filters.extend(map(lambda x: CustomFilter(x), filter)) + else: + filters.append(CustomFilter(filter)) + + # XXX + if itype!=0: + if type==None: + type = 0 + if (type & CALLABLE)==0: + type |= itype + addFilter(type, TypeFilter) + + # Add parent filters... + pfs = [] + for decl in self.decl_handles: + pfs.append(ParentFilter(decl, recursive)) + if len(pfs)>0: + if len(pfs)==1: + filters.append(pfs[0]) + else: + filters.append(OrFilter(pfs)) + + # Create the final filter by combining the individual filters + # with AND... + if len(filters)==0: + filter = TrueFilter() + elif len(filters)==1: + filter = filters[0] + else: + filter = AndFilter(filters) + +# print "Filter:",filter + if len(self.decl_handles)==0: + decls = [] + else: + decls = selection.select(self.decl_handles, filter) + + res = IDecl(decls, str(filter), modulebuilder=self.modulebuilder) + count = res.count + if allow_empty and count==0: + return res + if count==0: + raise RuntimeError, "Query produced no results (filter: %s)"%filter + res_count = res.count + # If all contained declarations are from one single set of overloads + # then treat that as one single declaration + if res._checkOverloads(): + res_count = 1 + if assert_count!=None: + if res_count!=assert_count: + raise RuntimeError, "Query produced the wrong number of results (%d instead of %d)"%(res_count, assert_count) + return res + + + # Namespace + def Namespaces(self, name=None, type=0, **args): + """Obtain a Decl object referencing one or more namespaces. + + This method is equivalent to calling Decl() with the flag NAMESPACE + set in its type filter. + + See Decl() for a full description of this method. + + @returns: Returns a Decl object that may reference an arbitrary number of declarations. + @see: L{Decl()} + """ + return self.Decls(name=name, type=type|NAMESPACE, **args) + + # Classes + def Classes(self, name=None, type=0, **args): + return self.Decls(name=name, type=type|CLASS, **args) + + # Methods + def Methods(self, name=None, type=0, **args): + return self.Decls(name=name, type=type|METHOD|CONSTRUCTOR, **args) + + # Constructors + def Constructors(self, name=None, type=0, **args): + return self.Decls(name=name, type=type|CONSTRUCTOR, **args) + + # Functions + def Functions(self, name=None, type=0, **args): + return self.Decls(name=name, type=type|FUNCTION, **args) + + # Enums + def Enums(self, name=None, type=0, **args): + return self.Decls(name=name, type=type|ENUM, **args) + + # Vars + def Vars(self, name=None, type=0, **args): + return self.Vars(name=name, type=type|VARIABLE, **args) + + # Decl + def Decl(self, name=None, **args): + return self.Decls(name, assert_count=1, **args) + + # Namespace + def Namespace(self, name=None, **args): + return self.Namespaces(name, assert_count=1, **args) + + # Class + def Class(self, name=None, **args): + return self.Classes(name, assert_count=1, **args) + + # Method + def Method(self, name=None, **args): + return self.Methods(name, assert_count=1, **args) + + # Constructor + def Constructor(self, name=None, **args): + return self.Constructors(name, assert_count=1, **args) + + # Function + def Function(self, name=None, **args): + return self.Functions(name, assert_count=1, **args) + + # Enum + def Enum(self, name=None, **args): + return self.Enums(name, assert_count=1, **args) + + # Var + def Var(self, name=None, **args): + return self.Vars(name, assert_count=1, **args) + + + # Private methods: + + def _getCount(self): + """Return the number of matched declarations. + """ + return len(self.decl_handles) + + count = property(_getCount, None, None, "The number of matched declarations.") + + def _getTotalCount(self): + """Return the total number of contained declarations (including the children). + """ + return len(list(self)) + + totalcount = property(_getTotalCount, None, None, "The total number of contained declarations.") + + def _checkLock(self): + """Check that the decoration is not locked. + + If it is locked, an exception is thrown. + """ + global decl_lock + if decl_lock: + raise RuntimeError, "You have to decorate the declarations before the code creators have been built." + + def _checkOverloads(self): + """Check if all contained declarations are from the same set of overloads. + + @returns: True if all contained declarations are from the same set of overloads. + @rtype: bool + """ + if len(self.decl_handles)==0: + return False + # Get a list with the overloaded functions + overloads = getattr(self.decl_handles[0], "overloads", None) + if overloads==None: + return False + + # Check if the decls are all contained in overloads... + for decl in self.decl_handles[1:]: + if decl not in overloads: + return False + + return True + + def _parseDefArgs(self, args): + """Determine which of the args is the doc string, call policies and keywords argument. + + @returns: Returns a tuple (doc, policies, keywords). + """ + if len(args)>3: + raise ValueError, "Too many arguments (%d)"%len(args) + + doc = None + policies = None + keywords = None +# call_policy_t = pyplusplus.code_creators.call_policies.call_policy_t + call_policy_t = pyplusplus.decl_wrappers.call_policies.call_policy_t + for a in args: + if isinstance(a, types.StringTypes): + doc = a + elif isinstance(a, call_policy_t): + policies = a + elif type(a)==tuple: + keywords = a + elif isinstance(a, decltypes.arg): + keywords = (a,) + else: + raise ValueError, "Invalid argument: %s"%a + return doc,policies,keywords + + def _iterContained(self, recursive=True): + """Iterate over all contained declarations. + + The generator yields pygccxml declaration objects. + + @param recursive: Determines whether the method also yields children nodes + @type recursive: bool + """ + global _timestamp + _timestamp += 1 + for rootdecl in self.decl_handles: + for decl in self._iterdecls(rootdecl, recursive=recursive): + # Get the time stamp and check if this declaration hasn't + # been visited yet + ts = getattr(decl, "_timestamp", -1) + if ts!=_timestamp: + decl._timestamp = _timestamp + yield decl + + # _iterdecls + def _iterdecls(self, rootdecl, recursive=True): + """Depth first iteration over one or more declaration trees. + + rootdecl can be either a single declaration, a list of + declarations or None. A declaration must be an object derived + from the declaration_t class. If recursive is False, only the + root is returned. + + @param recursive: Determines whether the method also yields children nodes + @type recursive: bool + """ + if rootdecl==None: + return + + if type(rootdecl) is not list: + rootdecl = [rootdecl] + + for root in rootdecl: + yield root + if recursive: + children = getattr(root, "declarations", None) + for node in self._iterdecls(children): + yield node + + # _logDecoration + def _logDecoration(self, operation, decl): + """Write a line into the decoration log. + + @param operation: Name of the decoration operation + @type operation: str + @param decl: Declaration that was decorated + @type decl: declaration_t + """ + global decoration_log + if self.funcname==None: + print >>decoration_log, "%s,%d;%s; %s "%(self.filename, self.linenr, operation, decl) + else: + print >>decoration_log, "%s,%s(),%d;%s; %s "%(self.filename, self.funcname, self.linenr, operation, decl) + + # _sourceInfo + def _sourceInfo(self): + """Determine where in the user's source code this instance was created. + + Returns a tuple (filename, funcname, linenr, sourceline) that + describes the source code line that created this instance. + funcname is None if the source code is not located inside a function. + + @returns: (filename, funcname, linenr, sourceline) + @rtype: 4-tuple + """ + frame = inspect.currentframe() + try: + records = inspect.getouterframes(frame) + # Determine the directory where this source file is located + apidir = os.path.dirname(records[0][1]) + for rec in records[1:]: + file = rec[1] + dir = os.path.dirname(file) + # Return the first record that is not within the API directory + # (this must then be code from the user) + if dir!=apidir: + linenr = rec[2] + funcname = rec[3] + srcindex = rec[5] + sourceline = rec[4][srcindex] + sourceline = sourceline.strip() + if funcname=="?": + funcname = None + return file,funcname,linenr,sourceline + finally: + del frame + + # We should never get here... + return "?", None, 0, "?" + + +# This value is used to determine if a declaration was already visited or not +_timestamp = 0 + Added: pyplusplus_dev/contrib/pypp_api/pypp_api/filters.py =================================================================== --- pyplusplus_dev/contrib/pypp_api/pypp_api/filters.py (rev 0) +++ pyplusplus_dev/contrib/pypp_api/pypp_api/filters.py 2006-07-18 08:32:45 UTC (rev 313) @@ -0,0 +1,446 @@ +# Copyright 2006 Roman Yakovenko. +# Distributed under the Boost Software License, Version 1.0. (See +# accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) +# +# Initial author: Matthias Baas + +"""This module contains the filters that are used to select declarations. +""" + +import re, os.path +from pygccxml.declarations import * +from decltypes import * +from treerange import TreeRange + +# _StringMatcher +class _StringMatcher: + """Helper class to match strings. + + This class can be used to match a string with a pattern that may + either be an ordinary string or contain a regular expression + enclosed in '/'. + """ + + def __init__(self, pattern): + """Constructor. + + pattern may contain a regular expression enclosed between two '/'. + + @param pattern: The pattern used for matching + @type pattern: str + """ + + self.regexp = None + self.pattern = pattern + + # Is pattern a regular expression? + if len(pattern)>=2 and pattern[0]=="/" and pattern[-1]=="/": + self.regexp = re.compile(pattern[1:-1]) + + # match + def match(self, txt): + """Check if a string matches the pattern. + + @param txt: The string to match + @type txt: str + @returns: True if txt matches the pattern + """ + if self.regexp==None: + return txt==self.pattern + else: + m = self.regexp.match(txt) + if m==None: + return False + # It was only a successful match when the entire string was matched + return m.end()==len(txt) + + +# FilterBase +class FilterBase: + """Base class for all filters. + """ + def __init__(self): + pass + + def __call__(self, decl): + raise NotImplementedError, "filters must always implement the __call__() method." + + def __invert__(self): + """NOT-operator (~)""" + return NotFilter(self) + + def __and__(self, other): + """AND-operator (&)""" + return AndFilter([self, other]) + + def __or__(self, other): + """OR-Operator (|)""" + return OrFilter([self, other]) + + def filterRange(self): + """Return the range of the filter. + + A return value of None means the filter's range is unlimited. + + @returns: Filter range or None + @rtype: TreeRange + """ + return None + + def parentConstraints(self): + """Return the parent constraints. + + *** obsolete *** + + A filter can use this method to indicate that it will always + return False if the parent is not a particular node. + + The following return values are possible: + + - C{None}: There are no constraints. The filter may return True on any node. + - C{[]}: The filter will always return False. + - C{[(parent, recursive),...]}: The parent constraints. + + A single parent constraint (I{parent}, I{recursive}) means that the + filter may only return True on children of I{parent}. If I{recursive} + is set to True these can also be grand-children, otherwise they + are only the direct children. On all other nodes, the filter will + always return False so the search algorithm may decide not to visit + them at all to speed up the search. + + @returns: None, an empty list or a list of tuples (parent, recursive). + """ + return None + +# TrueFilter +class TrueFilter(FilterBase): + """Returns always True. + """ + def __call__(self, decl): + return True + + def __str__(self): + return "True" + +# FalseFilter +class FalseFilter(FilterBase): + """Returns always False. + """ + def __call__(self, decl): + return False + + def __str__(self): + return "False" + +# AndFilter +class AndFilter(FilterBase): + """Combine several other filters with AND. + """ + def __init__(self, filters): + FilterBase.__init__(self) + self.filters = filters + + def __str__(self): + return " & ".join(map(lambda x: "(%s)"%str(x), self.filters)) + + def __call__(self, decl): + for f in self.filters: + if not f(decl): + return False + return True + + def filterRange(self): + res = None + for f in self.filters: + rng = f.filterRange() + if rng!=None: + if res==None: + res = rng + else: + res = res.intersect(rng) + return res + + +# OrFilter +class OrFilter(FilterBase): + "... [truncated message content] |