"""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