Skip to content

Instantly share code, notes, and snippets.

@bjodah
Created October 2, 2013 08:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bjodah/6790854 to your computer and use it in GitHub Desktop.
Save bjodah/6790854 to your computer and use it in GitHub Desktop.
Code generation of typed cython code from annotated pure python code (makes cython optional). Proof of concecpt not nearly as mature as e.g. cython pure mode.
from infer import MetaInfer
from infer import types as t
class Vec2(metaclass=MetaInfer):
# Typed slots:
_typed_init_slots = (
('x', t.double, 0.0),
('y', t.double, 0.0)
)
def norm(self) -> t.double:
return (self.x**2 + self.y**2) ** 0.5
def dot(self, other: t.of_same) -> t.double:
return self.x*other.x + self.y*other.y
v = Vec2(x=3.0, y=4.0)
w = Vec2(0.0, 1.0)
assert v.norm() == 5.0
assert w.norm() == 1.0
assert v.dot(w) == 4.0
# Beware that you loose duck-typing
class Foo:
x=3
y=2
print(v.dot(Foo))
Vec2.export_to_cython('_Vec2.pyx')
import pyximport
pyximport.install()
import _Vec2
class CythonVec2(_Vec2.Vec2):
def norm(self) -> t.double:
return (self.x**2 + self.y**2) ** 0.5
def dot(self, other: t.of_same) -> t.double:
return self.x*other.x + self.y*other.y
v = CythonVec2(x=3.0, y=4.0)
w = CythonVec2(0.0, 1.0)
assert v.norm() == 5.0
assert w.norm() == 1.0
assert v.dot(w) == 4.0
FastVec2 = Vec2.compiled()
v = FastVec2(x=3.0, y=4.0)
w = FastVec2(0.0, 1.0)
assert v.norm() == 5.0
assert w.norm() == 1.0
assert v.dot(w) == 4.0
cdef class ${class_name}:
%for name, cy_type, default in slots:
cdef public ${str(cy_type)} ${name}
%endfor
def __init__(self${', '.join(['']+[str(cy_type) + ' ' + name + ' = ' + str(default) if default != None else str(cy_type) + ' ' + name for name, cy_type, default in slots])}):
%for name, cy_type, default in slots:
self.${name} = ${name}
%endfor
%for name, (cy_type, args, body) in methods.items():
cpdef ${str(cy_type)} ${name}(${class_name} self${', '.join(['']+[str(cy_type_) + ' ' + name_ + ' = ' + str(default) if default != _empty else str(cy_type_) + ' ' + name_ for name_, cy_type_, default in args])}):
${'\n'.join(body.split('\n')[1:])}
%endfor
import inspect
import cython
from mako.template import Template
from mako.exceptions import text_error_template
def render_mako_template_to(template, outpath, subsd):
"""
template: either string of path or file like obj.
"""
if hasattr(template, 'read'):
# set in-file handle to provided template
ifh = template
else:
# Assume template is a string of the path to the template
ifh = open(template, 'rt')
template_str = ifh.read()
with open(outpath, 'wt') as ofh:
try:
rendered = Template(template_str).render(**subsd)
except:
print(text_error_template().render())
raise
ofh.write(rendered)
class IType:
def __init__(self, py_base, cy_base):
self.py_base = py_base
self.cy_base = cy_base
def cy_type(self, _self):
return self.cy_base
class ISpecial:
@classmethod
def get_py_type(cls, _self):
pass
@classmethod
def check_conformity(cls, _val):
pass
class IOfSame(ISpecial):
@classmethod
def get_py_type(cls, _self):
return _self.__class__
@classmethod
def cy_type(cls, _self):
return _self.__name__
@classmethod
def check_conformity(cls, _self, _val):
return isinstance(_val, self.get_py_type(_self))
class types:
void = IType(py_base = None, cy_base = cython.void)
double = IType(py_base = float, cy_base = cython.double)
of_same = IOfSame
@classmethod
def resolve_py_type(cls, _self, typp):
if isinstance(typp, IType):
return typp.py_base
elif isinstance(typp, ISpecial):
return typp.get_py_type(_self)
def _init_slots(_tslots, ori_init):
def __init__(self, *args, **kwargs):
taken = []
nam, typp, valu = zip(*_tslots)
typ = [types.resolve_py_type(self, x) for x in typp]
for i, arg in enumerate(args):
if not isinstance(arg, typ[i]):
raise TypeError('{} not of type {}'.format(arg, typ[i]))
setattr(self, nam[i], arg)
taken.append(nam[i])
for k, v in kwargs.items():
if k in taken:
raise KeyError('{} already assigned'.format(k))
if k not in nam:
raise KeyError('Unknown keyword: {} '.format(k))
t = typ[nam.index(k)]
if not isinstance(v, t):
raise TypeError('{} not of type {}'.format((v, t)))
setattr(self, k, v)
taken.append(k)
for n, t, v in zip(nam, typ, valu):
if n not in taken:
if not isinstance(v, t):
raise TypeError('{} not of type {}'.format((v, t)))
setattr(self, n, v)
if ori_init != None:
ori_init(self, *args, **kwargs)
__init__.__annotations__ = {n: t for n, t, v in _tslots}
return __init__
class MetaInfer(type):
"""
Reads `_typed_init_slots` to:
extend __slots__
intercept __init__ and populate resp. attribute
"""
def __new__(mcl, name, bases, nmspc):
slots = list(nmspc.get('__slots__', ()))
slots += [e[0] for e in nmspc['_typed_init_slots']]
nmspc['__slots__'] = tuple(slots)
ori_init = nmspc.get('__init__', None)
nmspc['__init__'] = _init_slots(nmspc['_typed_init_slots'], ori_init)
return super(MetaInfer, mcl).__new__(mcl, name, bases, nmspc)
def _methods(self):
names, meths = zip(*inspect.getmembers(Dummy, predicate=inspect.isfunction))
methods = {}
for name, meth in inspect.getmembers(self, predicate=inspect.isfunction):
if name in names: continue
sig = inspect.signature(meth)
args = [(name, param.annotation.cy_type(self), param.default) for\
name, param in sig.parameters.items() if name != 'self']
body = inspect.getsource(meth)
methods[name] = sig.return_annotation.cy_type(self), args, body
return methods
def export_to_cython(self, out):
names, itypes, defaults = zip(*self._typed_init_slots)
slots = zip(names, [it.cy_type(self) for it in itypes], defaults)
subsd = {'class_name': self.__name__,
'slots': list(slots),
'methods': self._methods(),
'_empty': inspect._empty}
render_mako_template_to('extension_template.pyx', out, subsd)
def compiled(self):
self.export_to_cython('_'+self.__name__+'.pyx')
import pyximport
pyximport.install()
mod = __import__('_'+self.__name__)
return getattr(mod, self.__name__)
class Dummy(metaclass=MetaInfer):
_typed_init_slots = ()
def __init__(self):
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment