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 copy import deepcopy
from pprint import pprint
from numbers import Number

from .utils import indent, indentstr, expand_default_args, isclassdesc, isfuncdesc, \
    isvardesc, newoverwrite, sortedbytype, _lang_exts, Arg
from .plugins import Plugin
from .typesystem import TypeSystem, TypeMatcher, MatchAny
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() 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 = {'struct': 'struct', 'class': 'cppclass'} #d['construct_kind'] = construct_kinds[desc.get('construct', 'class')] lang = desc['name']['language'] construct_kinds = {'c': 'struct', 'c++': 'cppclass'} d['construct_kind'] = construct_kinds[lang] 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, desc['name']['language'], 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) 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) 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): 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'] == 'struct': fcall = ('self._inst = malloc(sizeof({0}))\n' '(<{0} *> self._inst)[0] = {0}()').format(ct) else: raise ValueError('construct must be either "class" or "struct".') 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"): 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 == 'struct': fcall = ('self._inst = malloc(sizeof({0}))\n' '(<{0} *> self._inst)[0] = {0}({1})').format(classname, argvals) else: raise ValueError('construct must be either "class" or "struct".') 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} 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 == 'struct': 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) 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:", 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