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