python-efl/scripts/eolian_generate.py

722 lines
23 KiB
Python
Executable File

#!/usr/bin/python
import os
import textwrap
import keyword
from argparse import ArgumentParser
parser = ArgumentParser(description="Python generator for eolian")
# parser.add_argument(
# '--header-file', required=True,
# help="Filename of the library header")
parser.add_argument('-v', '--verbose', action="count")
parser.add_argument('-o', '--output', default="/tmp", help="max is -vvv")
parser.add_argument('paths', nargs="+")
args = parser.parse_args()
import logging
handler = logging.StreamHandler()
formatter = logging.Formatter("%(name)s %(levelname)s: %(message)s")
handler.setFormatter(formatter)
log = logging.getLogger("efl.eolian")
log.addHandler(handler)
level = logging.ERROR
if args.verbose:
level -= 10 * args.verbose
log.setLevel(level)
from efl import eolian
eolian.init()
DOCSTRINGS_ENABLED = True
import re
docstring_replacements = (
(re.compile(r"@brief "), r""),
(re.compile(r"@ingroup .+", re.S), r""),
(re.compile(r"@see (.+)(?!@)"), r":see: \1"),
(re.compile(r"@return "), r":return: "),
(re.compile(r"@(note|warning) "), r".. \1:: "),
(re.compile(r"@(p|c) (\w+)"), r"``\2``"),
(re.compile(r"@(b) (\w+)"), r"**\2**"),
(re.compile(r"@(i) (\w+)"), r"*\2*"),
(re.compile(r"EINA_TRUE"), r"True"),
(re.compile(r"EINA_FALSE"), r"False"),
(re.compile(r"NULL"), r"None"),
)
complex_types = (
"Eina_List"
)
param_type_mapping = {
# c_type: (py_type, conversion, c_param_convert)
"int": (None, None, None),
"double": (None, None, None),
"Evas_Coord": ("int", None, None),
"Eina_Bool": ("bint", None, None),
"char *": (
"",
"if isinstance({0}, unicode): {0} = PyUnicode_AsUTF8String({0})",
"<{1}>if {0} is not None else NULL"
),
"Evas_Object *": ("_Eo_Base", None, "{0}.obj"),
"Eo *": ("_Eo_Base", None, "{0}.obj"),
}
return_type_mapping = {
# c_type: (py_type, conversion)
"int": (None, None),
"double": (None, None),
"Evas_Coord": ("int", None),
"Eina_Bool": ("bint", None),
"char *": ("unicode", '{0}.decode("utf-8")'),
"Elm_Object_Item *": (
"_ObjectItem", 'object_item_to_python({0})'
),
"Evas_Object *": ("_Eo_Base", 'object_from_instance({0})'),
"Eo *": ("_Eo_Base", 'object_from_instance({0})'),
}
class Generator(object):
tab = " "
def __init__(self):
self.result = []
self.level = 0
def indent(self):
self.level += 1
def dedent(self):
self.level -= 1
def write(self, text=None, wrapped=False, i_ind=None, s_ind=None):
if not i_ind:
i_ind = self.level * self.tab
if not s_ind:
s_ind = self.level * self.tab
if text:
if wrapped:
self.result.append(textwrap.fill(
text,
initial_indent=i_ind,
subsequent_indent=s_ind,
width=79
))
else:
self.result.append(i_ind + text)
else:
self.result.append("")
def docstring_write(self, docstrings):
if not DOCSTRINGS_ENABLED or not docstrings:
return
elif len(docstrings) == 1:
self.write('"""' + docstrings[0] + '"""')
else:
self.write('"""')
for docs in docstrings:
if docs.startswith("- ") or \
docs.startswith(".. ") or \
docs.startswith(":"):
self.write(
docs, wrapped=True,
s_ind=self.level * self.tab + " "
)
elif docs.startswith(" - "):
self.write(
docs, wrapped=True,
s_ind=self.level * self.tab + " "
)
else:
self.write(docs, wrapped=True)
self.write('"""')
def printout(self):
return "\n".join(self.result)
class PyxGenerator(Generator):
def method_header_write(self, name, params, clsm=False):
params2 = []
if clsm:
self.write("@classmethod")
params2.append("cls")
else:
params2.append("self")
for t, n in params:
if t in param_type_mapping:
c_t = t.replace("const ", "").replace("unsigned ", "")
py_t = param_type_mapping[c_t][0]
if py_t is not None:
t = py_t
params2.append(" ".join((t, n)).strip())
params2 = ", ".join(params2)
define = "def %s(%s):" % (name, params2)
self.write(define)
def cdefs_write(self, cdefs):
self.write("cdef:")
self.indent()
types = {}
for t, n in cdefs:
types.setdefault(t, []).append(n)
for t, names in types.items():
if "*" in t:
# complex, write one per line
for n in names:
self.write("%s %s" % (t, n))
else:
# simple, all on one line
self.write("%s %s" % (t, ", ".join(names)))
self.dedent()
class Method(object):
def __init__(self, eo_prefix):
self.params = []
self.py_params = []
self.c_params = []
self.cdefs = []
self.returns = []
self.docs = []
self.ret_type = None
self.py_name = None
self.eo_prefix = eo_prefix
@classmethod
def parse(cls, func, eo_prefix):
self = cls.__new__(cls)
self.__init__(eo_prefix)
self.func = func
self.py_name = func.name
if keyword.iskeyword(self.py_name):
self.py_name += "_"
self.c_name = "_".join((self.eo_prefix, func.name))
func_desc = func.description_get("comment")
if func_desc:
func_desc = func_desc.split("\n\n")
for desc in func_desc:
for pat, repl in docstring_replacements:
desc = pat.sub(repl, desc)
if desc:
self.docs.append(desc)
self.docs.append("")
for p in func.parameters_list:
pdir, ptype, name, desc = p.information
ptype2 = ptype.replace("const ", "").replace("unsigned ", "")
if not ptype2 in param_type_mapping:
for t in eolian.type_find_by_alias(ptype2):
print(t)
raise TypeError("Unknown param type: %s" % (ptype2))
self.params.append((ptype, name))
if pdir == eolian.ParameterDir.IN:
self.py_params.append((ptype, name))
self.c_params.append((ptype, name))
self.docs.append(":param %s: %s" % (name, desc))
self.docs.append(":type %s: %s" % (name, ptype))
elif pdir == eolian.ParameterDir.OUT:
self.cdefs.append((ptype, name))
self.c_params.append((ptype, "&%s" % (name)))
self.returns.append((ptype, name))
else:
self.py_params.append((ptype, name))
self.c_params.append((ptype, name))
self.returns.append((ptype, name))
ret_type = func.return_type_get(func.type)
if ret_type:
ret_type2 = ret_type.replace("const ", "").replace("unsigned ", "")
if not ret_type2 in return_type_mapping:
for t in eolian.type_find_by_alias(ret_type2):
print(t)
raise TypeError("Unknown return type: %s" % (ret_type2))
self.ret_type = ret_type
ret_desc = func.return_comment_get(func.type)
if self.returns:
self.docs.append(
":return: " + ", ".join([r[1] for r in self.returns]))
self.docs.append(
":rtype: " + ", ".join([r[0] for r in self.returns]))
elif self.ret_type:
if ret_desc:
self.docs.append(":return: " + ret_desc)
self.docs.append(":rtype: " + self.ret_type)
return self
def pyx_generate(self, gen, param_conv=None):
gen.method_header_write(self.py_name, self.py_params)
gen.indent()
RET_PARAM = "py_efl_ret"
if self.docs:
gen.docstring_write(self.docs)
if self.ret_type:
t = self.ret_type
if t in return_type_mapping:
c_t = t.replace("const ", "").replace("unsigned ", "")
py_t = return_type_mapping[c_t][0]
if py_t is not None:
t = py_t
self.cdefs.append((t, RET_PARAM))
if self.cdefs:
gen.cdefs_write(self.cdefs)
conv = getattr(self, "params_conv", None)
if conv:
gen.write(conv)
for t, n in self.py_params:
t = t.replace("const ", "")
if t in param_type_mapping:
conv = param_type_mapping[t][1]
if conv:
gen.write(conv.format(n, t))
for i, (t, n) in enumerate(self.c_params):
t2 = t.replace("const ", "")
if t2 in param_type_mapping:
conv = param_type_mapping[t2][2]
if conv:
self.c_params[i] = (t, conv.format(n, t))
c_params = ", ".join([c[1] for c in self.c_params])
c_call = "eo_do(self.obj, %s(%s))" % (self.c_name, c_params)
if self.ret_type:
r_type = self.ret_type
if r_type in return_type_mapping:
conv = return_type_mapping[r_type][1]
if conv:
c_call = conv.format(c_call)
c_call = RET_PARAM + " = " + c_call
gen.write(c_call)
if self.returns:
ret = self.returns[:]
for i, (t, n) in enumerate(self.returns):
t = t.replace("const ", "")
if t in return_type_mapping:
conv = return_type_mapping[t][1]
if conv:
gen.write("conv_%s = %s" % (n, conv.format(n)))
ret[i] = t, "conv_%s" % (n)
gen.write("return %s" % (", ".join([r[1] for r in ret])))
elif self.ret_type:
ret = RET_PARAM
r_type = self.ret_type
if r_type in return_type_mapping:
conv = return_type_mapping[r_type][1]
if conv:
ret = conv.format(ret)
gen.write("return %s" % (ret))
gen.dedent()
gen.write()
def cdef_generate(self, gen):
rtype = self.ret_type
if not rtype:
rtype = "void"
line = " ".join((rtype, self.c_name))
line += "("
line += ", ".join([" ".join((t, n)) for t, n in self.params])
line += ")"
gen.write(line)
class Property(object):
def __init__(self, eo_prefix):
super(type(self), self).__init__()
self.docs = []
self.eo_prefix = eo_prefix
@classmethod
def parse(cls, func, eo_prefix):
self = cls.__new__(cls)
self.__init__(eo_prefix)
self.func = func
getter_desc = func.description_get("comment_get")
setter_desc = func.description_get("comment_set")
for func_desc in getter_desc, setter_desc:
if func_desc:
func_desc = func_desc.split("\n\n")
for desc in func_desc:
for pat, repl in docstring_replacements:
desc = pat.sub(repl, desc)
if desc:
self.docs.append(desc)
self.docs.append("")
self.getter = None
self.setter = None
for param in func.property_values_list:
self.docs.append(":type %s: %s" % (param.name, param.type))
if func.type == eolian.FunctionType.PROP_SET or \
func.type == eolian.FunctionType.PROPERTY:
# Create setter
m = self.setter = Method(eo_prefix)
m.py_name = "__set__"
m.c_name = "_".join((self.eo_prefix, func.name, "set"))
py_params = []
for p in func.property_values_list:
pdir, ptype, name, desc = p.information
assert pdir == eolian.ParameterDir.IN, "prop has other than IN"
ptype2 = ptype.replace("const ", "").replace("unsigned ", "")
if not ptype2 in param_type_mapping:
for t in eolian.type_find_by_alias(ptype2):
print(t)
raise TypeError("Unknown param type: %s" % (ptype2))
m.params.append((ptype, name))
py_params.append(name)
m.c_params.append((ptype, name))
assert py_params, (
"params should not be empty for setter of: %s" % (func.name)
)
py_params = ", ".join(py_params)
m.params_conv = "%s = value" % (py_params)
m.py_params = (("", "value"),)
m.ret_type = func.return_type_get(eolian.FunctionType.PROP_SET)
if func.type == eolian.FunctionType.PROP_GET or \
func.type == eolian.FunctionType.PROPERTY:
# Create getter
m = self.getter = Method(eo_prefix)
m.py_name = "__get__"
m.c_name = "_".join((self.eo_prefix, func.name, "get"))
for p in func.property_values_list:
pdir, ptype, name, desc = p.information
assert pdir == eolian.ParameterDir.IN, "prop has other than IN"
ptype2 = ptype.replace("const ", "").replace("unsigned ", "")
if not ptype2 in param_type_mapping:
for t in eolian.type_find_by_alias(ptype2):
print(t)
raise TypeError("Unknown param type: %s" % (ptype2))
m.params.append((ptype, name))
m.cdefs.append((ptype, name))
m.c_params.append((ptype, "&%s" % (name)))
m.returns.append((ptype, name))
m.ret_type = func.return_type_get(eolian.FunctionType.PROP_GET)
return self
def pyx_generate(self, gen):
gen.write("property %s:" % (self.func.name))
gen.indent()
gen.docstring_write(self.docs)
if self.setter:
self.setter.pyx_generate(gen)
if self.getter:
self.getter.pyx_generate(gen)
gen.dedent()
def cdef_generate(self, gen):
if self.setter:
self.setter.cdef_generate(gen)
if self.getter:
self.getter.cdef_generate(gen)
class Constructor(Method):
def pyx_generate(self, gen):
cls_get = self.eo_prefix + "_class_get()"
gen.method_header_write(
self.py_name, [("", "parent")] + self.py_params, clsm=True)
gen.indent()
gen.docstring_write(self.docs)
c_params = ", ".join([c[1] for c in self.c_params])
gen.write("self = cls.__new__(cls)")
gen.write(
"cdef Eo *obj = eo_add_custom"
"(%s, parent.obj if parent is not None else NULL, %s(%s))" % (
cls_get, self.c_name, c_params
)
)
gen.write("self._set_obj(obj)")
gen.write("return self")
gen.dedent()
gen.write()
class DefaultConstructor(Constructor):
def pyx_generate(self, gen):
cls_get = self.eo_prefix + "_class_get()"
gen.write("def __init__(self, parent=None):")
gen.indent()
gen.write(
"cdef Eo *obj = eo_add"
"(%s, parent.obj if parent is not None else NULL)" % (
cls_get
)
)
gen.write("self._set_obj(obj)")
gen.dedent()
gen.write()
def cdef_generate(self, gen):
pass
class Class(object):
def __init__(self):
self.docs = []
self.inherits = []
self.ctors = []
self.methods = []
self.props = []
self.default_ctor = None
self.header_path = None
@classmethod
def parse(klass, cls):
self = klass.__new__(klass)
self.__init__()
self.cls = cls
header_path = cls.filename
if not header_path:
return
header_path = os.path.basename(header_path + ".h")
self.header_path = header_path
self.inherits = cls.inherits_list
desc = self.cls.description
if desc:
desc = desc.split("\n\n")
for d in desc:
for pat, repl in docstring_replacements:
d = pat.sub(repl, d)
if d:
self.docs.append(d)
self.docs.append("")
for k, v in (
type(self.cls).__dict__.items()
):
if k.startswith("__") or hasattr(v, "__call__") or \
k == "description":
continue
if k == "events" or k == "implements" or k == "inherits_list":
objs = v.__get__(self.cls)
if not objs:
continue
self.docs.append(
"- %s: " % (k)
)
for obj in objs:
self.docs.append(
" - %s" % (obj)
)
else:
self.docs.append(
"- %s: %s" % (k, v.__get__(self.cls))
)
ctors = None
prefix = self.prefix = cls.eo_prefix
if not prefix:
return self
if cls.type == eolian.ClassType.REGULAR: # \
#or cls.type == eolian.ClassType.MIXIN:
ctors = cls.functions_list_get(eolian.FunctionType.CTOR)
if ctors:
for ctor in ctors:
try:
o = Constructor.parse(ctor, prefix)
except Exception:
log.exception(
"Skipping %s.%s because of an exception"
% (cls.name, ctor.name))
continue
self.ctors.append(o)
self.default_ctor = DefaultConstructor(prefix)
props = cls.functions_list_get(eolian.FunctionType.PROPERTY)
for prop in props:
try:
o = Property.parse(prop, prefix)
except Exception:
log.exception(
"Skipping %s.%s because of an exception"
% (cls.name, prop.name))
continue
self.props.append(o)
methods = cls.functions_list_get(eolian.FunctionType.METHOD)
for method in methods:
try:
o = Method.parse(method, prefix)
except Exception:
log.exception(
"Skipping %s.%s because of an exception"
% (cls.name, method.name))
continue
self.methods.append(o)
return self
def pyx_generate(self, gen):
if self.ctors or self.methods or self.props:
gen.write('cdef extern from "%s":' % (self.header_path))
gen.indent()
for o in self.ctors + self.methods + self.props:
o.cdef_generate(gen)
gen.dedent()
gen.write()
inherits = []
for i in self.inherits:
i_cls = eolian.class_find_by_name(i)
if i_cls:
if i_cls.type == eolian.ClassType.INTERFACE or \
i_cls.type == eolian.ClassType.MIXIN:
continue
else:
log.warn("Class %s is unknown" % (i))
inherits.append("_" + i)
assert len(inherits) < 2, (
"Multiple inheritance is not supported in extension classes.\n"
"Class: %r, inherits: %r" % (cls, inherits))
if not inherits:
inherits = ("object", )
inherits = ", ".join(inherits)
gen.write("cdef class _%s(%s):" % (self.cls.name, inherits))
gen.indent()
gen.docstring_write(self.docs)
if not self.ctors and not self.props and not self.methods and \
not self.default_ctor:
gen.write("pass")
else:
for o in (
[self.default_ctor] +
self.ctors +
self.methods +
self.props
):
if o:
o.pyx_generate(gen)
gen.dedent()
gen.write()
def py_generate(self, gen):
inherits = ["_" + self.cls.name]
for i in self.inherits:
i_cls = eolian.class_find_by_name(i)
if i_cls:
if i_cls.type == eolian.ClassType.REGULAR or \
i_cls.type == eolian.ClassType.ABSTRACT:
continue
else:
log.warn("Class %s is unknown" % (i))
inherits.append("_" + i)
for i in inherits:
pfix = i.split("_")[1]
m = pfix.lower()
gen.write("from %s import %s" % (m, i))
gen.write()
gen.write()
gen.write("class %s(%s):" % (self.cls.name, ", ".join(inherits)))
gen.indent()
gen.write("def __init__(self, parent=None, *args, **kwargs):")
gen.indent()
gen.write(
"_%s.__init__(self, parent=None, *args, **kwargs)" % (
self.cls.name
)
)
gen.dedent()
gen.dedent()
gen.write()
py_path = os.path.join(args.output, "__init__" + ".py")
if os.path.exists(py_path):
os.remove(py_path)
for path in args.paths:
for dirpath, dirnames, filenames in os.walk(path):
eolian.directory_scan(dirpath)
eolian.all_eo_files_parse()
for filename in filenames:
if filename.endswith(".eo"):
f = os.path.join(dirpath, filename)
if not eolian.eo_file_parse(f):
log.warn("Errors in parsing %s" % (f))
cls = eolian.class_find_by_file(f)
try:
gencls = Class.parse(cls)
except Exception:
log.exception(
"Skipping %s because of an exception" % (cls.name))
continue
pyxgen = PyxGenerator()
gencls.pyx_generate(pyxgen)
f_base = os.path.splitext(filename)[0]
pxi_path = os.path.join(args.output, f_base + ".pxi")
o = pyxgen.printout()
if o:
with open(pxi_path, "w") as f:
f.write(o.encode("utf-8"))
log.info(pxi_path + " written")
pygen = Generator()
gencls.py_generate(pygen)
o = pygen.printout()
if o:
with open(py_path, "a") as f:
f.write(o.encode("utf-8"))
f.write("\n")
log.info(py_path + " appended")
eolian.shutdown()