Created
October 2, 2013 08:48
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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