Source code for xdress.cythongen

"""Generates a Cython wrappers from description dictionaries.
This module relies heavily on the type system to convert between C/C++, Cython, and
Python types in a seamless way.  While this module does not explicitly rely on the
auto-describer, it sure helps!  The functions in this module are conceptually
easy to understand -- given class descriptions they generate strings of Cython
code -- their implementations do a lot of heavy lifting.

This module is available as an xdress plugin by the name ``xdress.cythongen``.
Note that while the module does not rely on the autodescriber, the plugin does.

:author: Anthony Scopatz <scopatz@gmail.com>

Cython Generation API
=====================
"""
from __future__ import print_function
import os
import sys
import math
import warnings
from numbers import Number

from .plugins import Plugin
from .types.matching import TypeMatcher, MatchAny
from .types.system import TypeSystem
from .utils import indent, expand_default_args, isclassdesc, isfuncdesc, \
    isvardesc, newoverwrite, sortedbytype, _lang_exts, Arg
from .version import cython_version, cython_version_info

if sys.version_info[0] >= 3:
    basestring = str

MATCH_REF = TypeMatcher((MatchAny, '&'))

AUTOGEN_WARNING = \
"""################################################
#                 WARNING!                     #
# This file has been auto-generated by xdress. #
# Do not modify!!!                             #
#                                              #
#                                              #
#                    Come on, guys. I mean it! #
################################################
"""

[docs]def gencpppxd(env, exceptions=True, ts=None): """Generates all cpp_*.pxd Cython header files for an environment of modules. Parameters ---------- env : dict Environment dictonary mapping target module names to module description dictionaries. exceptions : bool or str, optional Cython exception annotation. Set to True to automatically detect exception types, False for when exceptions should not be included, and a str (such as '+' or '-1') to apply to everywhere. ts : TypeSystem, optional A type system instance. Returns ------- cpppxds : dict Maps environment target names to Cython cpp_*.pxd header files strings. """ ts = ts or TypeSystem() cpppxds = {} for name, mod in env.items(): if mod['srcpxd_filename'] is None: continue cpppxds[name] = modcpppxd(mod, exceptions, ts=ts) return cpppxds
def _addotherclsnames(t, classes, name, others, ts): if t is None or t == (None, '*'): return spt = ts.strip_predicates(t) if spt in classes: others[name].add(spt) elif ts.isfunctionpointer(spt): for subt in spt[1:]: spsubt = ts.strip_predicates(subt) if spsubt in classes: others[name].add(spsubt)
[docs]def cpppxd_sorted_names(mod, ts): """Sorts the variable names in a cpp_*.pxd module so that C/C++ declarations happen in the proper order. """ classes = set([name for name, desc in mod.items() if isclassdesc(desc)]) clssort = sorted(c for c in classes if isinstance(c, basestring)) clssort += sorted(c for c in classes if not isinstance(c, basestring)) othercls = {} for name in clssort: desc = mod[name] othercls[name] = set() for pitem in desc['parents']: _addotherclsnames(pitem, classes, name, othercls, ts) for aname, atype in desc['attrs'].items(): _addotherclsnames(atype, classes, name, othercls, ts) for mkey, mval in desc['methods'].items(): mname, margs = mkey[0], mkey[1:] _addotherclsnames(mval['return'], classes, name, othercls, ts) for marg in margs: _addotherclsnames(marg[1], classes, name, othercls, ts) clssort.sort(key=lambda x: len(othercls[x])) names = clssort[:1] for name in clssort[1:]: if name in names: continue for i, n in enumerate(names[:]): if name in othercls[n]: names.insert(i, name) break if othercls[name] <= set(names[:i+1] + [name]): names.insert(i+1, name) break else: names.append(name) names += sortedbytype([name for name, desc in mod.items() if isvardesc(desc)]) names += sortedbytype([name for name, desc in mod.items() if isfuncdesc(desc)]) return names
[docs]def modcpppxd(mod, exceptions=True, ts=None): """Generates a cpp_*.pxd Cython header file for exposing a C/C++ module to other Cython wrappers based off of a dictionary description of the module. Parameters ---------- mod : dict Module description dictonary. exceptions : bool or str, optional Cython exception annotation. Set to True to automatically detect exception types, False for when exceptions should not be included, and a str (such as '+' or '-1') to apply to everywhere. ts : TypeSystem, optional A type system instance. Returns ------- cpppxd : str Cython cpp_*.pxd header file as in-memory string. """ ts = ts or TypeSystem() m = {'extra': mod.get('extra', ''), "srcpxd_filename": mod.get("srcpxd_filename", "")} attrs = [] cimport_tups = set() classnames = _classnames_in_mod(mod, ts) with ts.local_classes(classnames, frozenset(['c'])): for name in cpppxd_sorted_names(mod, ts): desc = mod[name] incfiles = desc['name']['incfiles'] if 0 == len(incfiles): msg = "cythongen requires an include file for {0}, none found" raise ValueError(msg.format(name)) elif 1 < len(incfiles): msg = "multiple include files found for {0}, choosing the first: {1}" warnings.warn(msg.format(name, incfiles[0]), RuntimeWarning) if isvardesc(desc): ci_tup, attr_str = varcpppxd(desc, exceptions, ts) elif isfuncdesc(desc): ci_tup, attr_str = funccpppxd(desc, exceptions, ts) elif isclassdesc(desc): ci_tup, attr_str = classcpppxd(desc, exceptions, ts) else: continue cimport_tups |= ci_tup attrs.append(attr_str) if mod.get('language', None) == 'c': cimport_tups.discard((ts.stlcontainers,)) m['cimports'] = "\n".join(sorted(ts.cython_cimport_lines(cimport_tups))) m['attrs_block'] = "\n".join(attrs) t = '\n\n'.join([AUTOGEN_WARNING, '{cimports}', '{attrs_block}', '{extra}']) cpppxd = t.format(**m) return cpppxd
_cpppxd_var_template = \ """# function signatures cdef extern from "{header_filename}" {namespace}: {variables_block} {extra} """
[docs]def varcpppxd(desc, exceptions=True, ts=None): """Generates a cpp_*.pxd Cython header snippet for exposing a C/C++ variable to other Cython wrappers based off of a dictionary description. Parameters ---------- desc : dict Function description dictonary. exceptions : bool or str, optional Cython exception annotation. Set to True to automatically detect exception types, False for when exceptions should not be included, and a str (such as '+' or '-1') to apply to everywhere. ts : TypeSystem, optional A type system instance. Returns ------- cimport_tups : set of tuples Set of Cython cimport tuples for cpp_*.pxd header file. cpppxd : str Cython cpp_*.pxd header file as in-memory string. """ ts = ts or TypeSystem() t = ts.canon(desc['type']) d = {'name': desc['name']['tarname'], 'header_filename': desc['name']['incfiles'][0], 'namespace': _format_ns(desc), } inc = set(['c']) cimport_tups = set() ts.cython_cimport_tuples(t, cimport_tups, inc) vlines = [] if ts.isenum(t): vlines.append("cdef enum {0}:".format(d['name'])) enames = [name for name, val in t[1][2][2]] vlines += indent(enames, 4, join=False) else: ct = ts.cython_ctype(t) vlines.append("{0} {1}".format(ct, d['name'])) d['variables_block'] = indent(vlines, 4) if 0 == len(d['variables_block'].strip()): return set(), '' d['extra'] = desc.get('extra', {}).get('cpppxd', '') cpppxd = _cpppxd_var_template.format(**d) extra = desc['extra'] if 'srcpxd_filename' not in extra: ext = _lang_exts[name.language] extra['srcpxd_filename'] = '{0}_{1}.pxd'.format(ext, d['name']['tarbase']) return cimport_tups, cpppxd
_cpppxd_func_template = \ """# function signatures cdef extern from "{header_filename}" {namespace}: {functions_block} {extra} """
[docs]def funccpppxd(desc, exceptions=True, ts=None): """Generates a cpp_*.pxd Cython header snippet for exposing a C/C++ function to other Cython wrappers based off of a dictionary description. Parameters ---------- desc : dict Function description dictonary. exceptions : bool or str, optional Cython exception annotation. Set to True to automatically detect exception types, False for when exceptions should not be included, and a str (such as '+' or '-1') to apply to everywhere. ts : TypeSystem, optional A type system instance. Returns ------- cimport_tups : set of tuples Set of Cython cimport tuples for cpp_*.pxd header file. cpppxd : str Cython cpp_*.pxd header file as in-memory string. """ ts = ts or TypeSystem() d = {'name': desc['name']['tarname'], 'header_filename': desc['name']['incfiles'][0], 'namespace': _format_ns(desc), } inc = set(['c']) cimport_tups = set() flines = [] funcitems = sorted(expand_default_args(desc['signatures'].items())) for fkey, frtn in funcitems: fname, fargs = fkey[0], fkey[1:] fbasename = fname if isinstance(fname, basestring) else fname[0] cppname = ts.cpp_funcname(fname) cyname = ts.cython_funcname(fname) if fbasename.startswith('_'): continue # private if any([a[1] is None or a[1][0] is None for a in fargs + (frtn,)]): continue argfill = ", ".join([ts.cython_ctype(a[1]) for a in fargs]) for a in fargs: ts.cython_cimport_tuples(a[1], cimport_tups, inc) estr = _exception_str(exceptions, desc['name']['language'], frtn, ts) if fname == cppname == cyname: line = "{0}({1}) {2}".format(fname, argfill, estr) else: line = '{0} "{1}" ({2}) {3}'.format(cyname, cppname, argfill, estr) rtype = ts.cython_ctype(frtn) ts.cython_cimport_tuples(frtn, cimport_tups, inc) line = rtype + " " + line if line not in flines: flines.append(line) d['functions_block'] = indent(flines, 4) if 0 == len(d['functions_block'].strip()): return set(), '' d['extra'] = desc.get('extra', {}).get('cpppxd', '') cpppxd = _cpppxd_func_template.format(**d) extra = desc['extra'] if 'srcpxd_filename' not in extra: extra['srcpxd_filename'] = '{0}_{1}.pxd'.format(d['name']['tarbase']) return cimport_tups, cpppxd
_cpppxd_class_template = \ """cdef extern from "{header_filename}" {namespace}: cdef {construct_kind} {name}{alias}{parents}: # constructors {constructors_block} # attributes {attrs_block} # methods {methods_block} pass {extra} """
[docs]def classcpppxd(desc, exceptions=True, ts=None): """Generates a cpp_*.pxd Cython header snippet for exposing a C/C++ class or struct to other Cython wrappers based off of a dictionary description of the class or struct. Parameters ---------- desc : dict Class description dictonary. exceptions : bool or str, optional Cython exception annotation. Set to True to automatically detect exception types, False for when exceptions should not be included, and a str (such as '+' or '-1') to apply to everywhere. ts : TypeSystem, optional A type system instance. Returns ------- cimport_tups : set of tuples Set of Cython cimport tuples for cpp_*.pxd header file. cpppxd : str Cython cpp_*.pxd header file as in-memory string. """ ts = ts or TypeSystem() src_lang = desc['name']['language'] pars = ', '.join([ts.cython_ctype(p) for p in desc['parents']]) d = {'parents': '('+pars+')' if pars else '', 'header_filename': desc['name']['incfiles'][0],} d['namespace'] = _format_ns(desc) name = desc['name']['tarname'] if isinstance(desc['type'], basestring): d['name'] = ts.cython_ctype(name) d['alias'] = '' else: d['name'] = ts.cython_classname(name)[1] d['alias'] = ' ' + _format_alias(desc, ts) construct_kinds = {'union': 'union'} if src_lang == 'c++': construct_kinds.update({'struct': 'cppclass', 'class': 'cppclass'}) d['construct_kind'] = construct_kinds[desc.get('construct', 'class')] else: # C construct_kinds.update({'struct': 'struct'}) d['construct_kind'] = construct_kinds[desc.get('construct', 'struct')] inc = set(['c']) cimport_tups = set() for parent in desc['parents']: ts.cython_cimport_tuples(parent, cimport_tups, inc) alines = [] attritems = sorted(desc['attrs'].items()) for aname, atype in attritems: if aname.startswith('_'): continue actype = ts.cython_ctype(atype) if '{type_name}' in actype: aline = actype.format(type_name=aname) else: aline = "{0} {1}".format(actype, aname) alines.append(aline) ts.cython_cimport_tuples(atype, cimport_tups, inc) d['attrs_block'] = indent(alines, 8) mlines = [] clines = [] dargs = expand_default_args(desc['methods'].items()) methitems = sorted(x for x in dargs if isinstance(x[0][0], basestring)) methitems += sorted(x for x in dargs if not isinstance(x[0][0], basestring)) default_constructor = ((d['name'],), None) if d['construct_kind'] == 'cppclass' and default_constructor not in methitems: methitems.insert(0, default_constructor) for mkey, mrtn in methitems: mname, margs = mkey[0], mkey[1:] mbasename = mname if isinstance(mname, basestring) else mname[0] mcppname = ts.cpp_funcname(mname) mcyname = ts.cython_funcname(mname) if mbasename.startswith('_') or mbasename.startswith('~'): continue # private or destructor argfill = ", ".join([ts.cython_ctype(a[1]) for a in margs]) for a in margs: ts.cython_cimport_tuples(a[1], cimport_tups, inc) estr = _exception_str(exceptions, src_lang, mrtn, ts) if mname == mcppname == mcyname: line = "{0}({1}) {2}".format(mname, argfill, estr) else: line = '{0} "{1}" ({2}) {3}'.format(mcyname, mcppname, argfill, estr) if mrtn is None: # this must be a constructor line = "{0}({1}) {2}".format(d['name'], argfill, estr) if line not in clines: clines.append(line) else: # this is a normal method if MATCH_REF.matches(mrtn): mrtn = mrtn[0] rtype = ts.cython_ctype(mrtn) ts.cython_cimport_tuples(mrtn, cimport_tups, inc) line = rtype + " " + line if line not in mlines: mlines.append(line) d['methods_block'] = indent(mlines, 8) d['constructors_block'] = indent(clines, 8) if src_lang == 'c++' else '' d['extra'] = desc.get('extra', {}).get('cpppxd', '') cpppxd = _cpppxd_class_template.format(**d) extra = desc['extra'] if 'srcpxd_filename' not in desc: desc['srcpxd_filename'] = extra['srcpxd_filename'] return cimport_tups, cpppxd
[docs]def genpxd(env, classes=(), ts=None, max_callbacks=8): """Generates all pxd Cython header files for an environment of modules. Parameters ---------- env : dict Environment dictonary mapping target module names to module description dictionaries. classes : sequence, optional Listing of all class names that are handled by cythongen. This may be the same dictionary as in genpyx() ts : TypeSystem, optional A type system instance. max_callbacks : int, optional The default maximum number of callbacks for function pointers. Returns ------- pxds : str Maps environment target names to Cython pxd header files strings. """ ts = ts or TypeSystem() pxds = {} for name, mod in env.items(): if mod['pxd_filename'] is None: continue pxds[name] = modpxd(mod, classes, ts=ts, max_callbacks=max_callbacks) return pxds
[docs]def pxd_sorted_names(mod): """Sorts the names in a module to make sure that pxd declarations happen in the proper order.""" classnames = [] othernames = [] for name, desc in mod.items(): if isclassdesc(desc): if name in classnames: continue parents = desc['parents'] if not parents: classnames.insert(0, name) continue for parent in parents: if parent not in classnames and parent in mod: classnames.append(parent) classnames.append(name) else: othernames.append(name) names = classnames + sortedbytype(othernames) return names
[docs]def modpxd(mod, classes=(), ts=None, max_callbacks=8): """Generates a pxd Cython header file for exposing C/C++ data to other Cython wrappers based off of a dictionary description. Parameters ---------- mod : dict Module description dictonary. classes : sequence, optional Listing of all class names that are handled by cythongen. This may be the same dictionary as in modpyx(). ts : TypeSystem, optional A type system instance. max_callbacks : int, optional The default maximum number of callbacks for function pointers. Returns ------- pxd : str Cython .pxd header file as in-memory string. """ ts = ts or TypeSystem() m = {'extra': mod.get('extra', ''), "pxd_filename": mod.get("pxd_filename", "")} attrs = [] cimport_tups = set() classnames = _classnames_in_mod(mod, ts) with ts.local_classes(classnames): for name in pxd_sorted_names(mod): desc = mod[name] if isclassdesc(desc): ci_tup, attr_str = classpxd(desc, classes, ts=ts, max_callbacks=max_callbacks) else: # no need to wrap functions again continue cimport_tups |= ci_tup attrs.append(attr_str) cimport_tups.discard((mod["name"],)) if mod.get('language', None) == 'c': cimport_tups.discard((ts.stlcontainers,)) m['cimports'] = "\n".join(sorted(ts.cython_cimport_lines(cimport_tups))) m['attrs_block'] = "\n".join(attrs) t = '\n\n'.join([AUTOGEN_WARNING, '{cimports}', '{attrs_block}', '{extra}']) pxd = t.format(**m) return pxd
_pxd_class_template = \ """{function_pointer_block} cdef class {name}{parents}: {body} pass {extra} """
[docs]def classpxd(desc, classes=(), ts=None, max_callbacks=8): """Generates a ``*pxd`` Cython header snippet for exposing a C/C++ class to other Cython wrappers based off of a dictionary description. Parameters ---------- desc : dict Class description dictonary. classes : sequence, optional Listing of all class names that are handled by cythongen. This may be the same dictionary as in modpyx(). ts : TypeSystem, optional A type system instance. max_callbacks : int, optional The default maximum number of callbacks for function pointers. Returns ------- cimport_tups : set of tuples Set of Cython cimport tuples for .pxd header file. pxd : str Cython ``*.pxd`` header snippet for class. """ ts = ts or TypeSystem() extra = desc['extra'] if 'pxd_filename' not in extra: extra['pxd_filename'] = '{0}.pxd'.format(desc['name']['tarbase']) pars = ', '.join([ts.cython_cytype(p) for p in desc['parents']]) d = {'parents': '('+pars+')' if pars else ''} name = desc['name']['tarname'] d['name'] = ts.cython_classname(name)[1] max_callbacks = desc.get('extra', {}).get('max_callbacks', max_callbacks) mczeropad = int(math.log10(max_callbacks)) + 1 cimport_tups = set() for parent in desc['parents']: ts.cython_cimport_tuples(parent, cimport_tups, set(['cy'])) from_cpppxd = desc['srcpxd_filename'].rsplit('.', 1)[0] tarname = desc['name']['tarname'] d['name_type'] = ts.cython_ctype(tarname) ts.cython_cimport_tuples(tarname, cimport_tups, set(['c'])) body = [] if desc['parents'] else ['cdef void * _inst', 'cdef public bint _free_inst'] attritems = sorted(desc['attrs'].items()) fplines = [] for aname, atype in attritems: if aname.startswith('_'): continue # skip private _, _, cachename, iscached = ts.cython_c2py(aname, atype, cache_prefix=None) if iscached: ts.cython_cimport_tuples(atype, cimport_tups) if _isclassptr(atype, classes): atype_nopred = ts.strip_predicates(atype) cyt = ts.cython_cytype(atype_nopred) elif _isclassdblptr(atype, classes): cyt = 'list' else: cyt = ts.cython_cytype(atype) decl = "cdef public {0} {1}".format(cyt, cachename) body.append(decl) if ts.isfunctionpointer(atype): apyname, acname = _mangle_function_pointer_name(aname, name) acdecl = "cdef public " + ts.cython_ctype(('function',)+ atype[1:]) for i in range(max_callbacks): suffix = "{0:0{1}}".format(i, mczeropad) apyname_i, acname_i = apyname + suffix, acname + suffix fplines.append("cdef public object " + apyname_i) fplines.append(acdecl.format(type_name=acname_i)) body.append("cdef unsigned int _{0}_vtab_i".format(aname)) fplines.append("cdef unsigned int _current_{0}_vtab_i".format(apyname)) if len(fplines) > 0: fplines.append("cdef unsigned int _MAX_CALLBACKS_" + d['name']) d['body'] = indent(body or ['pass']) d['function_pointer_block'] = '\n'.join(fplines) d['extra'] = desc.get('extra', {}).get('pxd', '') pxd = _pxd_class_template.format(**d) return cimport_tups, pxd
[docs]def genpyx(env, classes=None, ts=None, max_callbacks=8): """Generates all pyx Cython implementation files for an environment of modules. Parameters ---------- env : dict Environment dictonary mapping target module names to module description dictionaries. classes : dict, optional Dictionary which maps all class names that are required to their own descriptions. This is required for resolving class heirarchy dependencies. If None, this will be computed here. ts : TypeSystem, optional A type system instance. max_callbacks : int, optional The default maximum number of callbacks for function pointers. Returns ------- pyxs : str Maps environment target names to Cython pxd header files strings. """ ts = ts or TypeSystem() if classes is None: # get flat namespace of class descriptions classes = {} for envname, mod in env.items(): for modname, desc in mod.items(): if isclassdesc(desc): classes[desc['name']] = desc # gen files pyxs = {} for name, mod in env.items(): if mod['pyx_filename'] is None: continue pyxs[name] = modpyx(mod, classes=classes, ts=ts, max_callbacks=max_callbacks) return pyxs
_pyx_mod_template = AUTOGEN_WARNING + \ '''"""{docstring} """ {cimports} {imports} {attrs_block} {extra} '''
[docs]def modpyx(mod, classes=None, ts=None, max_callbacks=8): """Generates a pyx Cython implementation file for exposing C/C++ data to other Cython wrappers based off of a dictionary description. Parameters ---------- mod : dict Module description dictonary. classes : dict, optional Dictionary which maps all class names that are required to their own descriptions. This is required for resolving class heirarchy dependencies. ts : TypeSystem, optional A type system instance. max_callbacks : int, optional The default maximum number of callbacks for function pointers. Returns ------- pyx : str Cython pyx header file as in-memory string. """ ts = ts or TypeSystem() m = {'extra': mod.get('extra', ''), 'docstring': mod.get('docstring', "no docstring, please file a bug report!"), "pyx_filename": mod.get("pyx_filename", "")} attrs = [] import_tups = set() cimport_tups = set() classnames = _classnames_in_mod(mod, ts) with ts.local_classes(classnames): for name, desc in mod.items(): if isvardesc(desc): i_tup, ci_tup, attr_str = varpyx(desc, ts=ts) elif isfuncdesc(desc): i_tup, ci_tup, attr_str = funcpyx(desc, ts=ts) elif isclassdesc(desc): i_tup, ci_tup, attr_str = classpyx(desc, classes=classes, ts=ts, max_callbacks=max_callbacks) else: continue import_tups |= i_tup cimport_tups |= ci_tup attrs.append(attr_str) # Add dispatcher for template functions template_funcs = _template_funcnames_in_mod(mod) template_dispatcher = _gen_template_func_dispatcher(template_funcs, ts) attrs.append(template_dispatcher) # Add dispatcher for template classes template_classes = _template_classnames_in_mod(mod) template_dispatcher = _gen_template_class_dispatcher(template_classes, ts) attrs.append(template_dispatcher) import_tups.discard((mod["name"],)) #cimport_tups.discard((mod["name"],)) # remain commented for decls if mod.get('language', None) == 'c': import_tups.discard((ts.stlcontainers,)) cimport_tups.discard((ts.stlcontainers,)) m['imports'] = "\n".join(sorted(ts.cython_import_lines(import_tups))) m['cimports'] = "\n".join(sorted(ts.cython_cimport_lines(cimport_tups))) if 'numpy' in m['cimports']: m['imports'] += "\n\nnp.import_array()" m['attrs_block'] = "\n".join(attrs) t = '\n\n'.join([AUTOGEN_WARNING, '{cimports}', '{attrs_block}', '{extra}']) pyx = _pyx_mod_template.format(**m) return pyx
def _gen_template_pyfill(arg, kind, ts): """Generates the fill values for an argument of a type into a template type t. """ if kind is Arg.TYPE: rtn = ts.cython_pytype(arg) elif kind is Arg.LIT: rnt = str(arg) elif kind is Arg.VAR: rtn = arg elif isinstance(arg, Number): rtn = str(arg) elif isinstance(arg, basestring): try: rtn = ts.cython_pytype(arg) except TypeError: rtn = arg return rtn def _gen_template_func_dispatcher(templates, ts): """Generates a dictionary-based dispacher for template functions. """ if 0 == len(templates): return "" templates = sorted(templates) disp = ['', "#", "# Function Dispatchers", "#",] alreadyinitd = set() for t in templates: initline = "{0} = {{}}".format(t[0]) if initline not in alreadyinitd: disp.append("") disp.append("# {0} dispatcher".format(t[0])) disp.append(initline) alreadyinitd.add(initline) args = t[1:] pytype = ts.cython_funcname(t) kinds = ts.argument_kinds.get(t, ((Arg.NONE,))*(len(t)-1)) if 0 == len(args): raise ValueError("type {0!r} not a template".format(t)) elif 1 == len(args): disp.append("{0}[{1!r}] = {2}".format(t[0], t[1], pytype)) disp.append("{0}[{1}] = {2}".format(t[0], _gen_template_pyfill(t[1], kinds[0], ts), pytype)) else: rs = [repr(_) for _ in t[1:]] pyts = [_gen_template_pyfill(x, k, ts) for x, k in zip(t[1:], kinds)] disp.append("{0}[{1}] = {2}".format(t[0], ", ".join(rs), pytype)) disp.append("{0}[{1}] = {2}".format(t[0], ", ".join(pyts), pytype)) return "\n".join(disp) def _gen_template_class_dispatcher(templates, ts): """Generates a dictionary-based dispacher for template classes. """ if 0 == len(templates): return "" templates = sorted(templates) disp = ['', "#", "# Class Dispatchers", "#",] alreadyinitd = set() for t in templates: initline = "{0} = {{}}".format(t[0]) if initline not in alreadyinitd: disp.append("") disp.append("# {0} Dispatcher".format(t[0])) disp.append(initline) alreadyinitd.add(initline) args = t[1:-1] pytype = ts.cython_pytype(t) kinds = ts.argument_kinds.get(t, ((Arg.NONE,))*(len(t)-1)) if 0 == len(args): raise ValueError("type {0!r} not a template".format(t)) elif 1 == len(args): disp.append("{0}[{1!r}] = {2}".format(t[0], t[1], pytype)) disp.append("{0}[{1}] = {2}".format(t[0], _gen_template_pyfill(t[1], kinds[0], ts), pytype)) else: rs = [repr(_) for _ in t[1:-1]] pyts = [_gen_template_pyfill(x, k, ts) for x, k in zip(t[1:], kinds)] disp.append("{0}[{1}] = {2}".format(t[0], ", ".join(rs), pytype)) disp.append("{0}[{1}] = {2}".format(t[0], ", ".join(pyts), pytype)) return "\n".join(disp) def _gen_property_get(name, t, ts, cached_names=None, inst_name="self._inst", classes=()): """This generates a Cython property getter for a variable of a given name and type.""" lines = ['def __get__(self):'] decl, body, rtn, iscached = ts.cython_c2py(name, t, inst_name=inst_name) if decl is not None: if _isclassptr(t, classes): decl, _, _, _ = ts.cython_c2py(name, t[0], inst_name=inst_name) lines += indent(decl, join=False) if body is not None: lines += indent(body, join=False) if iscached and cached_names is not None: cached_names.append(rtn) lines += indent("return {0}".format(rtn), join=False) return lines def _gen_property_set(name, t, ts, inst_name="self._inst", cached_name=None, classes=()): """This generates a Cython property setter for a variable of a given name and type.""" lines = ['def __set__(self, value):'] decl, body, rtn = ts.cython_py2c('value', t) if decl is not None: lines += indent(decl, join=False) if body is not None: lines += indent(body, join=False) lines += indent("{0}.{1} = {2}".format(inst_name, name, rtn), join=False) if cached_name is not None: lines += indent("{0} = None".format(cached_name), join=False) return lines def _gen_property(name, t, ts, doc=None, cached_names=None, inst_name="self._inst", classes=()): """This generates a Cython property for a variable of a given name and type.""" lines = ['property {0}:'.format(name)] lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False) oldcnlen = 0 if cached_names is None else len(cached_names) lines += indent(_gen_property_get(name, t, ts, cached_names=cached_names, inst_name=inst_name, classes=classes), join=False) lines += [''] newcnlen = 0 if cached_names is None else len(cached_names) cached_name = cached_names[-1] if newcnlen == 1 + oldcnlen else None lines += indent(_gen_property_set(name, t, ts, inst_name=inst_name, cached_name=cached_name, classes=classes), join=False) lines += ['', ""] return lines def _gen_function_pointer_property(name, t, ts, doc=None, cached_names=None, inst_name="self._inst", classname='', max_callbacks=8): """This generates a Cython property for a function pointer variable.""" lines = ['property {0}:'.format(name)] # get section lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False) oldcnlen = 0 if cached_names is None else len(cached_names) lines += indent(_gen_property_get(name, t, ts, cached_names=cached_names, inst_name=inst_name), join=False) # set section mczeropad = int(math.log10(max_callbacks)) + 1 lines += [""] newcnlen = 0 if cached_names is None else len(cached_names) cached_name = cached_names[-1] if newcnlen == 1 + oldcnlen else None setlines = indent(_gen_property_set(name, ('void', '*'), ts, inst_name=inst_name, cached_name=cached_name), join=False) lines += setlines[:1] lines += indent(indent(['if not callable(value):', (' raise ValueError("{0!r} is not callable but ' + classname + '.' + name + ' is a function pointer!".format(value))')], join=False), join=False) #lines += setlines[1:] pyname, cname = _mangle_function_pointer_name(name, classname) pynames = [pyname + "{0:0{1}}".format(i, mczeropad) for i in \ range(max_callbacks)] cnames = [cname + "{0:0{1}}".format(i, mczeropad) for i in \ range(max_callbacks)] if max_callbacks == 1: suffix = '0' extraset = ('global {pyname}\n' '{cached_name} = value\n' '{pyname} = value\n' '{inst_name}.{name} = {cname}\n' ).format(name=name, pyname=pyname + suffix, cname=cname + suffix, cached_name=cached_name, inst_name=inst_name) elif max_callbacks > 1: extraset = ['cdef unsigned int vtab_i', '{cached_name} = value'.format(cached_name=cached_name), "global " + ', '.join(pynames) + \ ', _current_{0}_vtab_i'.format(pyname),] selectlines = [] for i, pyname_i in enumerate(pynames): selectlines.append("elif {0} is None:".format(pyname_i)) selectlines.append(" vtab_i = {0}".format(i)) selectlines[0] = selectlines[0][2:] extraset += selectlines extraset += ['else:', (' warnings.warn("Ran out of available callbacks for ' '{0}.{1}, overriding existing callback.", RuntimeWarning)' ).format(classname, name), ' vtab_i = _current_{0}_vtab_i'.format(pyname), ' _current_{0}_vtab_i = (_current_{0}_vtab_i+1)%{1}'.format( pyname, max_callbacks), 'self._{0}_vtab_i = vtab_i'.format(name),] setvallines = [] for i, (pyname_i, cname_i) in enumerate(zip(pynames, cnames)): setvallines.append("elif vtab_i == {0}:".format(i)) setvallines.append(" {pyname} = value".format(pyname=pyname_i)) setvallines.append(" {inst_name}.{name} = {cname}".format( inst_name=inst_name, name=name, cname=cname_i)) setvallines[0] = setvallines[0][2:] extraset += setvallines else: msg = "The max number of callbacks for {0} must be >=1, got {1}." raise RuntimeError(msg.format(classname, max_callbacks)) lines += indent(indent(extraset, join=False), join=False) lines.append('') lines += ["def _deref_{0}_callback(self):".format(name), ' "Warning: this can have dangerous side effects!"', ' cdef unsigned int vtab_i', ' {cached_name} = None'.format(cached_name=cached_name), " if self._{0}_vtab_i < {1}:".format(name, max_callbacks+1), ' vtab_i = self._{0}_vtab_i'.format(name), " self._{0}_vtab_i = {1}".format(name, max_callbacks+1), ] dereflines = [] for i, pyname_i in enumerate(pynames): dereflines.append("elif vtab_i == {0}:".format(i)) dereflines.append(" global {0}".format(pyname_i)) dereflines.append(" {0} = None".format(pyname_i)) dereflines[0] = dereflines[0][2:] lines += indent(indent(dereflines, join=False), join=False) lines += ['', ""] return lines def _gen_function_pointer_wrapper(name, t, ts, classname='', max_callbacks=8): """This generates a Cython wrapper for a function pointer variable.""" pyname, cname = _mangle_function_pointer_name(name, classname) mczeropad = int(math.log10(max_callbacks)) + 1 lines = ["#\n# Function pointer helpers for {1}.{0}\n#".format(name, classname), "_current_{0}_vtab_i = 0".format(pyname), ""] for i in range(max_callbacks): suffix = "{0:0{1}}".format(i, mczeropad) pyname_i, cname_i = pyname + suffix, cname + suffix decl, body, rtn = ts.cython_py2c(pyname_i, t, proxy_name=cname_i) lines += [pyname_i + " = None", ''] lines += rtn.splitlines() lines.append('') lines += ['', ""] return lines def _gen_argfill(args, defaults): """Generate argument list for a function, and return (argfill, names). If any argument names or empty, the corresponding entry in names will be '_n' for some integer n. """ counter = 0 taken = frozenset(a[0] for a in args) names = [] afill = [] for (name, t), (kind, default) in zip(args, defaults): if not name: # Empty name, generate a fresh dummy symbol while 1: name = '_%d'%counter counter += 1 if name not in taken: break names.append(name) if kind is Arg.NONE: afillval = name elif kind is Arg.LIT: afillval = "{0}={1!r}".format(name, default) elif kind is Arg.VAR: afillval = "{0}={1}".format(name, default) elif kind is Arg.TYPE: raise ValueError("default argument value cannot be a type: " "{0}".format(name)) else: raise ValueError("default argument value cannot be determined: " "{0}".format(name)) afill.append(afillval) return ", ".join(afill), names def _gen_function(name, name_mangled, args, rtn, defaults, ts, doc=None, inst_name="self._inst", is_method=False): argfill, names = _gen_argfill(args, defaults) if is_method: argfill = "self, " + argfill lines = ['def {0}({1}):'.format(name_mangled, argfill)] lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False) decls = [] argbodies = [] argrtns = {} for n,a in zip(names, args): adecl, abody, artn = ts.cython_py2c(n, a[1]) if adecl is not None: decls += indent(adecl, join=False) if abody is not None: argbodies += indent(abody, join=False) argrtns[n] = artn rtype_orig = ts.cython_ctype(rtn) rtype = rtype_orig.replace('const ', "").replace(' &', '') hasrtn = rtype not in set(['None', None, 'NULL', 'void']) argvals = ', '.join(argrtns[n] for n in names) fcall = '{0}.{1}({2})'.format(inst_name, name, argvals) if hasrtn: fcdecl, fcbody, fcrtn, fccached = ts.cython_c2py('rtnval', rtn, cached=False, view=False) decls += indent("cdef {0} {1}".format(rtype, 'rtnval'), join=False) if 'const ' in rtype_orig: func_call = indent('rtnval = <{0}> {1}'.format(rtype, fcall), join=False) else: func_call = indent('rtnval = {0}'.format(fcall), join=False) if fcdecl is not None: decls += indent(fcdecl, join=False) if fcbody is not None: func_call += indent(fcbody, join=False) func_rtn = indent("return {0}".format(fcrtn), join=False) else: func_call = indent(fcall, join=False) func_rtn = [] lines += decls lines += argbodies lines += func_call lines += func_rtn lines += ['', ""] return lines def _gen_default_constructor(desc, attrs, ts, doc=None, srcpxd_filename=None): src_lang = desc['name']['language'] args = ['self'] + [a + "=None" for a, _ in attrs] + ['*args', '**kwargs'] argfill = ", ".join(args) lines = ['def __init__({0}):'.format(argfill)] lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False) ct = ts.cython_ctype(desc['type']) if desc['construct'] == 'class': fcall = 'self._inst = new {0}()'.format(ct) elif desc['construct'] in ('struct', 'union'): construct_template = 'self._inst = malloc(sizeof({0}))\n' if src_lang == 'c++' and desc['construct'] == 'struct': # Only call the default constructor when it makes sense. construct_template += '(<{0} *> self._inst)[0] = {0}()' fcall = construct_template.format(ct) else: raise ValueError('construct must be either "class", "struct" or "union".') lines.extend(indent(fcall, join=False)) for a, _ in attrs: lines.append(indent("if {0} is not None:".format(a))) lines.append(indent("self.{0} = {0}".format(a), 8)) lines += ['', ""] return lines def _gen_constructor(name, name_mangled, classname, args, defaults, ts, doc=None, srcpxd_filename=None, inst_name="self._inst", construct="class", src_lang='c++'): argfill, names = _gen_argfill(args, defaults) lines = ['def {0}(self, {1}):'.format(name_mangled, argfill)] lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False) decls = [] argbodies = [] argrtns = {} for n,a in zip(names, args): adecl, abody, artn = ts.cython_py2c(n, a[1]) if adecl is not None: decls += indent(adecl, join=False) if abody is not None: argbodies += indent(abody, join=False) argrtns[n] = artn argvals = ', '.join(argrtns[n] for n in names) classname = classname if srcpxd_filename is None else \ "{0}.{1}".format(srcpxd_filename.rsplit('.', 1)[0], classname) if construct == 'class': fcall = 'self._inst = new {0}({1})'.format(classname, argvals) elif construct in ('struct', 'union'): construct_template = 'self._inst = malloc(sizeof({0}))\n' if src_lang == 'c++' and construct == 'struct': # Only call the default constructor when it makes sense. construct_template += '(<{0} *> self._inst)[0] = {0}({1})' fcall = construct_template.format(classname, argvals) else: raise ValueError('construct must be either "class", "struct", or "union".') func_call = indent(fcall, join=False) lines += decls lines += argbodies lines += func_call lines += ['', ""] return lines def _gen_dispatcher(name, name_mangled, ts, doc=None, hasrtn=True, is_method=True): argfill = ", ".join(['self', '*args', '**kwargs']) if is_method is True: # string to format for arg checking arg_chk_str = "if types <= self.{0}_argtypes:" # string to format for dispatching and returning dispatch_str_ret = "return self.{0}(*args, **kwargs)" dispatch_str_no_ret = "self.{0}(*args, **kwargs)" # Make self a method argument or not argfill = ", ".join(['self', '*args', '**kwargs']) else: arg_chk_str = "if types <= {0}_argtypes:" dispatch_str_ret = "return {0}(*args, **kwargs)" dispatch_str_no_ret = "{0}(*args, **kwargs)" argfill = ", ".join(['*args', '**kwargs']) lines = ['def {0}({1}):'.format(name, argfill)] lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False) types = ["types = set([(i, type(a)) for i, a in enumerate(args)])", "types.update([(k, type(v)) for k, v in kwargs.items()])",] lines += indent(types, join=False) refinenum = lambda x: (sum([int(ts.isrefinement(a[1])) for a in x[0][1:]]), len(x[0]), x[1]) mangitems = sorted(name_mangled.items(), key=refinenum) mtypeslines = [] lines += indent("# vtable-like dispatch for exactly matching types", join=False) for key, mangled_name in mangitems: cargs = key[1:] arang = range(len(cargs)) anames = [ca[0] for ca in cargs] pytypes = [ts.cython_pytype(ca[1]) for ca in cargs] mtypes = ", ".join( ["({0}, {1})".format(i, pyt) for i, pyt in zip(arang, pytypes)] + \ ['("{0}", {1})'.format(n, pyt) for n, pyt in zip(anames, pytypes)]) mtups = '(' + mtypes + ')' if 0 < len(mtypes) else mtypes mtypeslines.append(mangled_name + "_argtypes = frozenset(" + mtups + ")") cond = [arg_chk_str.format(mangled_name),] if hasrtn: rline = dispatch_str_ret.format(mangled_name) else: rline = [dispatch_str_no_ret.format(mangled_name), "return"] cond += indent(rline, join=False) lines += indent(cond, join=False) lines = sorted(mtypeslines) + [''] + lines lines += indent("# duck-typed dispatch based on whatever works!", join=False) refineopp = lambda x: (-1*sum([int(ts.isrefinement(a[1])) for a in x[0][1:]]), len(x[0]), x[1]) mangitems = sorted(name_mangled.items(), key=refineopp) for key, mangled_name in mangitems: lines += indent('try:', join=False) if hasrtn: rline = dispatch_str_ret.format(mangled_name) else: rline = [dispatch_str_no_ret.format(mangled_name), "return"] lines += indent(indent(rline, join=False), join=False) lines += indent(["except (RuntimeError, TypeError, NameError):", indent("pass", join=False)[0],], join=False) errmsg = "raise RuntimeError('method {0}() could not be dispatched')".format(name) lines += indent(errmsg, join=False) lines += [''] return lines def _class_heirarchy(cls, ch, classes): if not classes[cls]['parents']: return if 0 == len(ch) or ch[0] != cls: ch.insert(0, cls) for p in classes[cls]['parents'][::-1]: ch.insert(0, p) _class_heirarchy(p, ch, classes) def _method_instance_names(desc, classes, key, rtn, ts): classnames = [] _class_heirarchy(desc['name']['tarname'], classnames, classes) for classname in classnames: classrtn = classes.get(classname, {}).get('methods', {})\ .get(key, {}).get('return', NotImplemented) if rtn != classrtn: continue class_ctype = ts.cython_ctype(classname) inst_name = "(<{0} *> self._inst)".format(class_ctype) return inst_name, classname tarname = desc['name']['tarname'] return "(<{0} *> self._inst)".format(ts.cython_ctype(tarname)), tarname def _count0(x): c = {} for v in x: v0 = v[0] c[v0] = c.get(v0, 0) + 1 return c def _doc_add_sig(doc, name, args, defaults, ismethod=True): if doc.startswith(name): return doc sig = ['self'] if ismethod else [] sig.append(_gen_argfill(args, defaults)[0]) newdoc = "{0}({1})\n{2}".format(name, ", ".join(sig), doc) return newdoc _pyx_class_template = \ '''{function_pointer_block} cdef class {name}{parents}: {class_docstring} {cdefattrs} # constuctors def __cinit__(self, *args, **kwargs): self._inst = NULL self._free_inst = True # cached property defaults {property_defaults} {constructor_block} # attributes {attrs_block} # methods {methods_block} pass {extra} '''
[docs]def classpyx(desc, classes=None, ts=None, max_callbacks=8): """Generates a ``*.pyx`` Cython wrapper implementation for exposing a C/C++ class based off of a dictionary description. The environment is a dictionary of all class names known to their descriptions. Parameters ---------- desc : dict Class description dictonary. classes : dict, optional Dictionary which maps all class names that are required to their own descriptions. This is required for resolving class heirarchy dependencies. ts : TypeSystem, optional A type system instance. max_callbacks : int, optional The default maximum number of callbacks for function pointers. Returns ------- pyx : str Cython ``*.pyx`` implementation file as in-memory string. """ ts = ts or TypeSystem() if classes is None: classes = {desc['name']['tarname']: desc} src_lang = desc['name']['language'] nodocmsg = "no docstring for {0}, please file a bug report!" pars = ', '.join([ts.cython_cytype(p) for p in desc['parents']]) d = {'parents': '('+pars+')' if pars else '', 'namespace': desc['namespace'], } srcname = desc['name']['srcname'] name = desc['name']['tarname'] d['name'] = ts.cython_classname(name)[1] class_doc = desc.get('docstrings', {}).get('class', nodocmsg.format(desc['name'])) d['class_docstring'] = indent('\"\"\"{0}\"\"\"'.format(class_doc)) class_ctype = ts.cython_ctype(name) inst_name = "(<{0} *> self._inst)".format(class_ctype) import_tups = set() cimport_tups = set() for parent in desc['parents']: ts.cython_import_tuples(parent, import_tups) ts.cython_cimport_tuples(parent, cimport_tups) cdefattrs = [] mc = desc.get('extra', {}).get('max_callbacks', max_callbacks) alines = [] pdlines = [] fplines = [] cached_names = [] attritems = sorted(desc['attrs'].items()) for aname, atype in attritems: if aname.startswith('_'): continue # skip private adoc = desc.get('docstrings', {}).get('attrs', {})\ .get(aname, nodocmsg.format(aname)) if ts.isfunctionpointer(atype): alines += _gen_function_pointer_property(aname, atype, ts, adoc, cached_names=cached_names, inst_name=inst_name, classname=name, max_callbacks=mc) fplines += _gen_function_pointer_wrapper(aname, atype, ts, max_callbacks=mc, classname=name) pdlines.append("self._{0}_vtab_i = {1}".format(aname, mc+1)) else: alines += _gen_property(aname, atype, ts, adoc, cached_names=cached_names, inst_name=inst_name, classes=classes) ts.cython_import_tuples(atype, import_tups) ts.cython_cimport_tuples(atype, cimport_tups) if len(fplines) > 0: fplines.append("_MAX_CALLBACKS_{0} = {1}".format(name, mc)) d['attrs_block'] = indent(alines) d['function_pointer_block'] = "\n".join(fplines) pdlines += ["{0} = None".format(n) for n in cached_names] d['property_defaults'] = indent(indent(pdlines, join=False)) mlines = [] clines = [] methcounts = _count0(desc['methods']) currcounts = dict([(k, 0) for k in methcounts]) mangled_mnames = {} mitems = list(desc['methods'].items()) methitems = sorted(x for x in mitems if isinstance(x[0][0], basestring)) methitems += sorted(x for x in mitems if not isinstance(x[0][0], basestring)) for mkey, mval in methitems: mname, margs = mkey[0], mkey[1:] mrtn = mval['return'] mdefs = mval['defaults'] mbasename = mname if isinstance(mname, basestring) else mname[0] mcyname = ts.cython_funcname(mname) if mbasename.startswith('_'): continue # skip private if any([a[1] is None or a[1][0] is None for a in margs]): continue if 1 < methcounts[mname]: mname_mangled = "_{0}_{1}_{2:0{3}}".format(d['name'], mcyname, currcounts[mname], int(math.log(methcounts[mname], 10)+1)).lower() else: mname_mangled = mcyname currcounts[mname] += 1 mangled_mnames[mkey] = mname_mangled for a in margs: ts.cython_import_tuples(a[1], import_tups) ts.cython_cimport_tuples(a[1], cimport_tups) minst_name, mcname = _method_instance_names(desc, classes, mkey, mrtn, ts) if mcname != d['name']: ts.cython_import_tuples(mcname, import_tups) ts.cython_cimport_tuples(mcname, cimport_tups) if mrtn is None: # this must be a constructor if mname not in (d['name'], '__init__', srcname if isinstance(srcname, basestring) else srcname[:-1]): continue # skip destuctors if 1 == methcounts[mname]: mname_mangled = '__init__' mangled_mnames[mkey] = mname_mangled mdoc = desc.get('docstrings', {}).get('methods', {}).get(mname, '') mdoc = _doc_add_sig(mdoc, mcyname, margs, mdefs) construct = desc['construct'] if construct in ('struct', 'union'): cimport_tups.add(('libc.stdlib', 'malloc')) clines += _gen_constructor(mcyname, mname_mangled, d['name'], margs, mdefs, ts, doc=mdoc, srcpxd_filename=desc['srcpxd_filename'], inst_name=minst_name, construct=construct, src_lang=src_lang) if 1 < methcounts[mname] and currcounts[mname] == methcounts[mname]: # write dispatcher nm = {} for k, v in mangled_mnames.items(): if isinstance(k[0], basestring) and k[0] == mbasename: nm[k] = v elif k[0] == srcname[:-1]: nm[k] = v clines += _gen_dispatcher('__init__', nm, ts, doc=mdoc, hasrtn=False) else: # this is a normal method ts.cython_import_tuples(mrtn, import_tups) ts.cython_cimport_tuples(mrtn, cimport_tups) mdoc = desc.get('docstrings', {}).get('methods', {})\ .get(mname, nodocmsg.format(mname)) mdoc = _doc_add_sig(mdoc, mcyname, margs, mdefs) mlines += _gen_function(mcyname, mname_mangled, margs, mrtn, mdefs, ts, mdoc, inst_name=minst_name, is_method=True) if 1 < methcounts[mname] and currcounts[mname] == methcounts[mname]: # write dispatcher nm = dict([(k, v) for k, v in mangled_mnames.items() \ if k[0] == mbasename]) mlines += _gen_dispatcher(mcyname, nm, ts, doc=mdoc) if 0 == len(desc['methods']) or 0 == len(clines): # provide a default constructor mdocs = desc.get('docstrings', {}).get('methods', {}) mdoc = mdocs.get(desc['name']['tarname'], False) or mdocs.get('__init__', '') attrsargs = [(Arg.LIT, "None")] * len(attritems) mdoc = _doc_add_sig(mdoc, '__init__', attritems, attrsargs) clines += _gen_default_constructor(desc, attritems, ts, doc=mdoc) cimport_tups.add(('libc.stdlib', 'malloc')) if not desc['parents']: clines += ["def __dealloc__(self):"] clines += indent("if self._free_inst and self._inst is not NULL:", join=False) clines += indent(indent("free(self._inst)", join=False), join=False) cimport_tups.add(('libc.stdlib', 'free')) # Add dispatcher for templated methods template_meths = _template_method_names(desc['methods']) template_dispatcher = _gen_template_func_dispatcher(template_meths, ts) mlines.append(indent(template_dispatcher)) d['methods_block'] = indent(mlines) d['constructor_block'] = indent(clines) d['extra'] = desc.get('extra', {}).get('pyx', '') d['cdefattrs'] = indent(cdefattrs) pyx = _pyx_class_template.format(**d) if 'pyx_filename' not in desc: desc['pyx_filename'] = '{0}.pyx'.format(d['name'].lower()) return import_tups, cimport_tups, pyx
[docs]def varpyx(desc, ts=None): """Generates a ``*.pyx`` Cython wrapper implementation for exposing a C/C++ variable based off of a dictionary description. Parameters ---------- desc : dict Variable description dictonary. ts : TypeSystem, optional A type system instance. Returns ------- pyx : str Cython ``*.pyx`` implementation as in-memory string. """ ts = ts or TypeSystem() nodocmsg = "no docstring for {0}, please file a bug report!" inst_name = desc['extra']['srcpxd_filename'].rsplit('.', 1)[0] import_tups = set() cimport_tups = set(((inst_name,),)) name = desc['name'] t = ts.canon(desc['type']) vlines = [] if ts.isenum(t): doc = '"' * 3 + "{0} is enumeration {1} of {2}" + '"' * 3 for ename, val in t[1][2][2]: vlines.append("{0} = {1}.{0}".format(ename, inst_name)) vlines.append(doc.format(ename, val, name)) vlines.append("") else: decl, body, rtn, iscached = ts.cython_c2py(name, t, view=False, cached=False, inst_name=inst_name) vlines.append(decl) vlines.append(body) vlines.append(name + " = " + rtn) docstring = desc.get('docstring', None) if docstring is None: docstring = '"' * 3 + nodocmsg.format(name) + '"' * 3 vlines.append(docstring) vlines.append(desc.get('extra', {}).get('pyx', '')) pyx = '\n'.join(vlines) extra = desc['extra'] if 'pyx_filename' not in extra: extra['pyx_filename'] = '{0}.pyx'.format(desc['name']['tarbase']) return import_tups, cimport_tups, pyx
[docs]def funcpyx(desc, ts=None): """Generates a ``*.pyx`` Cython wrapper implementation for exposing a C/C++ function based off of a dictionary description. Parameters ---------- desc : dict function description dictonary. ts : TypeSystem, optional A type system instance. Returns ------- pyx : str Cython ``*.pyx`` implementation as in-memory string. """ ts = ts or TypeSystem() nodocmsg = "no docstring for {0}, please file a bug report!" inst_name = desc['extra']['srcpxd_filename'].rsplit('.', 1)[0] import_tups = set() cimport_tups = set(((inst_name,),)) # For renaming ftopname = desc['name']['tarname'] fcytopname = ts.cython_funcname(ftopname) flines = [] funccounts = _count0(desc['signatures']) currcounts = dict([(k, 0) for k in funccounts]) mangled_fnames = {} funcitems = sorted(desc['signatures'].items()) for fkey, fval in funcitems: fname, fargs = fkey[0], fkey[1:] frtn = fval['return'] fdefs = fval['defaults'] fbasename = fname if isinstance(fname, basestring) else fname[0] #fcppname = ts.cpp_funcname(fname) fcyname = ts.cython_funcname(fname) if fbasename.startswith('_'): continue # skip private if any([a[1] is None or a[1][0] is None for a in fargs + (frtn,)]): continue if 1 < funccounts[fname]: fname_mangled = "_{0}_{1:0{2}}".format(fcytopname, currcounts[fname], int(math.log(funccounts[fname], 10)+1)).lower() else: fname_mangled = fcytopname currcounts[fname] += 1 mangled_fnames[fkey] = fname_mangled for a in fargs: ts.cython_import_tuples(a[1], import_tups) ts.cython_cimport_tuples(a[1], cimport_tups) ts.cython_import_tuples(frtn, import_tups) ts.cython_cimport_tuples(frtn, cimport_tups) fdoc = desc.get('docstring', nodocmsg.format(fcyname)) fdoc = _doc_add_sig(fdoc, fcyname, fargs, fdefs, ismethod=False) flines += _gen_function(fcyname, fname_mangled, fargs, frtn, fdefs, ts, fdoc, inst_name=inst_name, is_method=False) if 1 < funccounts[fname] and currcounts[fname] == funccounts[fname]: # write dispatcher nm = dict([(k, v) for k, v in mangled_fnames.items() if k[0] == fname]) flines += _gen_dispatcher(fcytopname, nm, ts, doc=fdoc, is_method=False) flines.append(desc.get('extra', {}).get('pyx', '')) pyx = '\n'.join(flines) extra = desc['extra'] if 'pyx_filename' not in extra: extra['pyx_filename'] = '{0}.pyx'.format(extra['name']['tarbase']) return import_tups, cimport_tups, pyx # # Plugin #
[docs]class XDressPlugin(Plugin): """The main cython generator plugin. """ requires = ('xdress.autodescribe',) """This plugin requires autodescribe.""" defaultrc = {'max_callbacks': 8} rcdocs = { "max_callbacks": "The maximum number of callbacks for function pointers", } def update_argparser(self, parser): parser.add_argument('--max-callbacks', type=int, dest="max_callbacks", help=self.rcdocs["max_callbacks"]) def setup(self, rc): if rc.max_callbacks < 1: raise ValueError("max_callbacks must be greater than or equal to 1") if cython_version is None: warnings.warn('cython does not seem to be installed', RuntimeWarning) elif cython_version_info[:2] <= (0, 17): warnings.warn('cython code generated by xdress requires cython v0.18+, ' 'cython version {0} found'.format(cython_version), RuntimeWarning) def execute(self, rc): print("cythongen: creating C/C++ API wrappers") env = rc.env classes = {} for modname, mod in env.items(): for name, desc in mod.items(): if isclassdesc(desc): classes[name] = desc # generate all files cpppxds = gencpppxd(env, ts=rc.ts) pxds = genpxd(env, classes, ts=rc.ts, max_callbacks=rc.max_callbacks) pyxs = genpyx(env, classes, ts=rc.ts, max_callbacks=rc.max_callbacks) # write out all files for key, cpppxd in cpppxds.items(): newoverwrite(cpppxd, os.path.join(rc.packagedir, env[key]['srcpxd_filename']), rc.verbose) for key, pxd in pxds.items(): newoverwrite(pxd, os.path.join(rc.packagedir, env[key]['pxd_filename']), rc.verbose) for key, pyx in pyxs.items(): newoverwrite(pyx, os.path.join(rc.packagedir, env[key]['pyx_filename']), rc.verbose) # # Misc Helpers Below #
def _format_ns(desc): ns = desc.get('namespace', None) if ns is None: return "" elif len(ns) == 0: return "" else: return 'namespace "{0}"'.format(ns) def _format_alias(desc, ts): ns = desc.get('namespace', None) cpp_name = ts.cpp_type(desc['name']['tarname']) if ns is None or len(ns) == 0: return '"{0}"'.format(cpp_name) else: return '"{0}::{1}"'.format(ns, cpp_name) def _mangle_function_pointer_name(name, classname): pyref = "_xdress_{0}_{1}_proxy".format(classname, name) cref = pyref + "_func" return pyref, cref def _isclassptr(t, classes): return (not isinstance(t, basestring) and t[1] == '*' and isinstance(t[0], basestring) and t[0] in classes) def _isclassdblptr(t, classes): if 2 != len(t): return False return _isclassptr(t[0], classes) and t[1] == '*' _exc_c_base = frozenset(['int16', 'int32', 'int64', 'int128', 'float32', 'float64', 'float128']) _exc_ptr_matcher = TypeMatcher((MatchAny, '*')) def _exception_str(exceptions, lang, rtntype, ts): if not exceptions: return "" if isinstance(exceptions, basestring): return "except " + exceptions if lang == 'c': if rtntype is None: return "except -1" # helpful when we accidentally mis-guessed C for C++ rtntype = ts.canon(rtntype) return "" # The following does not work in general since valid vs invalid returns # cannot be known. #if rtntype in _exc_c_base: # return "except -1" #elif ts.isenum(rtntype): # return "except -1" #elif _exc_ptr_matcher.matches(rtntype): # return "except -1" #else: # return "" elif lang == 'c++': return "except +" else: return "" def _template_method_names(methods): methnames = set() for sig, val in methods.items(): name = sig[0] if isinstance(name, basestring): continue elif val['return'] is None: # ignore constructor / destructors continue methnames.add(name) return methnames def _template_funcnames_in_mod(mod): funcnames = set() for name, desc in mod.items(): if isinstance(name, basestring) or not isfuncdesc(desc): continue funcnames.add(name) return funcnames def _classnames_in_mod(mod, ts): classnames = set() for name, desc in mod.items(): if not isclassdesc(desc): continue classnames.add(name) classnames.add(ts.basename(name)) classnames.add(desc['type']) return classnames def _template_classnames_in_mod(mod): classnames = set() for name, desc in mod.items(): if isinstance(name, basestring) or not isclassdesc(desc): continue classnames.add(name) return classnames