python-efl/scripts/eolian_generate.py

828 lines
26 KiB
Python
Executable File

#!/usr/bin/python
import os
import textwrap
import keyword
#from traceback import format_exc
from collections import Counter
from argparse import ArgumentParser
parser = ArgumentParser(description="Python generator for eolian")
parser.add_argument(
'-v', '--verbose', action="count", help="max is -vvv")
parser.add_argument(
'--enable-docstrings', action="store_true")
parser.add_argument(
'--with-legacy-api', action="store_true")
parser.add_argument(
'-o', '--output', default="/tmp", help="defaults to /tmp")
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()
from converters import convert_in_param, convert_out_param, conv_type_ret, \
conv_cls_name, EolianTypeError
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 args.enable_docstrings 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, filepath=None):
result = "\n".join(self.result)
if filepath:
with open(filepath, "w") as f:
f.write(result.encode("utf-8"))
f.write("\n")
log.info(filepath + " written")
else:
print(result)
def clear(self):
self.result = []
self.level = 0
class PyxGenerator(Generator):
RET_PARAM = "py_efl_ret"
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:
params2.append(" ".join((t, n)).strip())
params2 = ", ".join(params2)
define = "def %s(%s):" % (name, params2)
self.write(define)
def out_cdefs_write(self, out_cdefs):
if not out_cdefs:
return
self.write("cdef:")
self.indent()
types = {}
for t, n in out_cdefs:
types.setdefault(t, []).append(n)
for t, names in types.items():
#t = t.c_type
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()
def c_call_write(self, c_name, c_params, ret_type=None):
# TODO: Eina_Bool ret value from do_start, useful?
self.write("_eo_do_start(self.obj, NULL, EINA_FALSE, CFILE, CFUNC, CLINE)")
c_call = ""
if ret_type:
c_call += "cdef " + ret_type + " " + self.RET_PARAM + " = " + "<{0}>".format(ret_type)
c_call += c_name + "("
if not c_params:
c_call += ")"
self.write(c_call)
else:
self.write(c_call)
self.indent()
for p in c_params:
self.write(p + ",")
self.write(")")
self.dedent()
self.write("_eo_do_end(self.obj)")
class Function(object):
RET_PARAM = "py_efl_ret"
def __init__(self, func, prefix, isget=None):
self.name = func.name
self.func = func
self.prefix = prefix
self.isget = isget
self.c_name = func.full_c_name
self.c_params = []
ftypes = eolian.FunctionType
if (func.type == ftypes.PROP_SET or func.type == ftypes.PROPERTY) and not isget:
ret_type = func.return_type_get(eolian.FunctionType.PROP_SET)
self.c_name += "_set"
for p in func.parameters:
self.c_params.append((p.type.c_type, p.name))
if (func.type == ftypes.PROP_GET or func.type == ftypes.PROPERTY) and isget:
ret_type = func.return_type_get(eolian.FunctionType.PROP_GET)
self.c_name += "_get"
for p in func.parameters:
self.c_params.append((p.type.c_type + "*", p.name))
else:
ret_type = func.return_type_get(eolian.FunctionType.METHOD)
for p in func.parameters:
self.c_params.append((p.type.c_type, p.name))
self.c_ret_type = ret_type.c_type if ret_type.type != eolian.TypeType.UNKNOWN else "void"
def cdef_generate(self, gen):
func = self.func
ftypes = eolian.FunctionType
if (func.type == ftypes.PROP_SET or func.type == ftypes.PROPERTY) and self.isget:
return
if (func.type == ftypes.PROP_GET or func.type == ftypes.PROPERTY) and not self.isget:
return
rtype = self.c_ret_type
if not rtype:
rtype = "void"
line = " ".join((rtype, self.c_name))
line += "("
line += ", ".join([" ".join((t, n)) for t, n in self.c_params])
line += ")"
gen.write(line)
def generate(self, gen):
func = self.func
py_name = None
c_name = self.c_name
header_params = []
c_call_params = []
return_params = []
expand_params = []
conv_in_exps = []
out_cdefs = []
ret_type = None
ftypes = eolian.FunctionType
if func.type == ftypes.METHOD:
py_name = func.name
if keyword.iskeyword(py_name): # Check if name is python reserved
py_name += "_"
func_params = func.parameters
for p in func_params:
di = p.direction
c_type = p.type.c_type
if di == eolian.ParameterDir.IN:
conv_expr, c_type, name = convert_in_param(p.type, p.name, p.is_nonull)
conv_in_exps.append(conv_expr)
header_params.append((c_type, p.name))
c_call_params.append(name)
elif di == eolian.ParameterDir.OUT:
out_cdefs.append((c_type, p.name))
c_type, name = convert_out_param(p.type, p.name)
c_call_params.append("&" + name)
return_params.append((p.type, name))
elif di == eolian.ParameterDir.INOUT:
conv_expr, c_type, name = convert_in_param(p.type, p.name, p.is_nonull)
header_params.append((c_type, p.name))
c_call_params.append(name)
c_type, name = convert_out_param(p.type, p.name)
return_params.append((p.type, p.name))
ret_type = func.return_type_get(eolian.FunctionType.METHOD)
if (func.type == ftypes.PROP_SET or func.type == ftypes.PROPERTY) and not self.isget:
py_name = "__set__"
c_name = "_".join((c_name, "set"))
expand_params = []
for p in list(func.property_values) + list(func.property_keys):
conv_expr, c_type, name = convert_in_param(p.type, p.name, p.is_nonull)
conv_in_exps.append(conv_expr)
expand_params.append(p.name)
c_call_params.append(p.name)
header_params = (("", "value"),)
ret_type = func.return_type_get(eolian.FunctionType.PROP_SET)
if (func.type == ftypes.PROP_GET or func.type == ftypes.PROPERTY) and self.isget:
py_name = "__get__"
c_name = "_".join((c_name, "get"))
for p in list(func.property_values) + list(func.property_keys):
c_type = p.type.c_type
name = p.name
out_cdefs.append((c_type, name))
c_call_params.append("&%s" % (name))
c_type, name = convert_out_param(p.type, name)
return_params.append((p.type, name))
ret_type = func.return_type_get(eolian.FunctionType.PROP_GET)
if not py_name:
return
gen.method_header_write(py_name, header_params)
gen.indent()
if expand_params:
expand_params = ", ".join(expand_params)
gen.write("%s = value" % (expand_params))
for e in conv_in_exps:
if e:
gen.write(e)
gen.out_cdefs_write(out_cdefs)
if ret_type.type != eolian.TypeType.UNKNOWN:
ret_type2 = ret_type.c_type
else:
ret_type2 = None
gen.c_call_write(c_name, c_call_params, ret_type2)
if return_params:
rparams = []
for t, n in return_params:
py_ret_type, c_ret_type = conv_type_ret(t)
rparams.append((c_ret_type, n))
gen.write("return %s" % (", ".join([r[1] for r in rparams])))
elif ret_type.type != eolian.TypeType.UNKNOWN:
ret = self.RET_PARAM
try:
ret_type = convert_out_param(ret_type, ret)
except Exception:
gen.write("# FIXME: Unknown return type")
gen.dedent()
raise
gen.write("return %s" % (ret))
gen.dedent()
gen.write()
def doc_generate(self, gen):
func_desc = func.description_get(eolian.FunctionType.METHOD)
if func_desc:
func_desc = func_desc.split("\n\n")
for desc in func_desc:
for pat, repl in docstring_replacements:
if isinstance(pat, basestring):
desc = desc.replace(pat, repl)
else:
desc = pat.sub(repl, desc)
if desc:
self.docs.append(desc)
self.docs.append("")
self.docs.append(":param %s: %s" % (name, desc))
self.docs.append(":type %s: %s" % (name, ptype))
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)
class Property(object):
def __init__(self, prop, prefix):
self.name = prop.name
self.prop = prop
self.prefix = prefix
self.py_name = prop.name
self.getter = Function(prop, prefix, isget=True)
self.setter = Function(prop, prefix, isget=False)
def cdef_generate(self, gen):
self.getter.cdef_generate(gen)
self.setter.cdef_generate(gen)
def generate(self, gen):
gen.write("property %s:" % (self.py_name))
gen.indent()
try:
self.getter.generate(gen)
self.setter.generate(gen)
except Exception:
gen.write("pass")
gen.write()
raise
finally:
gen.dedent()
class Constructor(object):
def __init__(self, ctors, prefix):
self.prefix = prefix
self.ctors = ctors
def cdef_generate(self, gen):
pass
def generate(self, gen):
cls_get = self.prefix + "_class_get()"
header_params = [("_Base", "parent=None")]
c_ctors = []
for ctor in self.ctors:
func = ctor.function
c_name = func.full_c_name
c_call_params = []
py_name = func.name
if keyword.iskeyword(py_name): # Check if name is python reserved
py_name += "_"
ftypes = eolian.FunctionType
if (func.type == ftypes.PROP_SET or func.type == ftypes.PROPERTY):
c_name = c_name + "_set"
for p in func.parameters:
di = p.direction
assert di == eolian.ParameterDir.IN, "other than IN param for constructor"
c_type = p.type.c_type
conv_expr, c_type, name = convert_in_param(p.type, p.name, p.is_nonull)
header_params.append((c_type, p.name))
c_call_params.append(name)
c_call_params = ", ".join(c_call_params)
c_ctors.append((c_name, c_call_params))
gen.write("def __init__(self, %s):" % (", ".join([" ".join((t, n)).strip() for t, n in header_params])))
gen.indent()
gen.write("cdef Eo *obj = eo_add_ref(")
gen.indent()
gen.write("%s, parent.obj if parent is not None else NULL," % (cls_get))
for ctor in c_ctors:
gen.write("%s(%s)" % (ctor[0], ctor[1]))
gen.write(")")
gen.dedent()
gen.write("self._set_obj(obj)")
gen.dedent()
gen.write()
class Class(object):
def __init__(self):
self.docs = []
self.inherits = []
self.ctor = []
self.methods = []
self.props = []
self.events = []
self.default_ctor = None
self.header_path = None
@classmethod
def parse(klass, cls):
self = klass.__new__(klass)
self.__init__()
class_counter[cls.name] += 1
self.c_name = cls.name
if cls.namespaces:
self.lib_name = ".".join(map(unicode.lower, cls.namespaces))
self.name = cls.name
else:
self.lib_name, self.name = conv_cls_name(cls.name)
header_path = cls.filename
if not header_path:
log.error("Class %s has no filename" % (cls.name))
return
header_path = os.path.basename(header_path + ".h")
self.header_path = header_path
self.inherits = list(cls.inherits)
prefix = cls.eo_prefix
if not prefix:
log.warn("Class %s has no prefix!" % (cls.name))
prefix = cls.name.lower()
self.prefix = prefix
#if cls.type == eolian.ClassType.REGULAR: # or cls.type == eolian.ClassType.MIXIN:
self.ctor.append(Constructor(cls.constructors, prefix))
props = cls.functions_get(eolian.FunctionType.PROPERTY)
for prop in props:
if not prop.scope == eolian.ObjectScope.PUBLIC:
log.debug(
"Skipping non-public property %s.%s",
cls.name, prop.name
)
continue
function_counter["_".join((prefix, prop.name))] += 1
try:
o = Property(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_get(eolian.FunctionType.METHOD)
for method in methods:
if not method.scope == eolian.ObjectScope.PUBLIC:
log.debug(
"Skipping non-public method %s.%s",
cls.name, method.name
)
continue
function_counter["_".join((prefix, method.name))] += 1
try:
o = Function(method, prefix)
except Exception:
log.exception(
"Skipping %s.%s because of an exception"
% (cls.name, method.name))
continue
self.methods.append(o)
generated_class_counter[cls.name] += 1
for event in cls.events:
self.events.append(Event(event))
return self
def cdefs_generate(self, gen):
if self.ctor or self.methods or self.props:
for o in self.ctor + self.methods + self.props + self.events:
try:
o.cdef_generate(gen)
except Exception:
log.exception("Error generating cdefs for %r", o)
def pyx_generate(self, gen):
inherits = []
for i in self.inherits:
i_cls = eolian.Class.get_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))
i = i.rpartition(".")[2]
inherits.append("_" + i)
if len(inherits) > 1:
log.error(
"Multiple inheritance is not supported in extension classes.\n"
"Class: %r" % (self.c_name)
)
return
if not inherits:
inherits = ("object", )
inherits = ", ".join(inherits)
gen.write("cdef class _%s(%s):" % (self.name, inherits))
gen.indent()
if not self.ctor and not self.props and not self.methods:
gen.write("pass")
else:
for o in (
self.ctor +
list(self.events)
):
try:
o.generate(gen)
except EolianTypeError:
log.info("Skipping %r because of unknown type", o)
except Exception:
log.exception("Error while generating %r", o)
for o in (
list(self.methods) +
list(self.props)
):
try:
o.generate(gen)
except EolianTypeError:
log.info("Skipping %r because of unknown type", o)
except Exception:
log.exception("Error while generating %r", o)
else:
generated_function_counter["_".join((o.prefix, o.name))] += 1
gen.dedent()
gen.write()
def py_generate(self, gen):
inherits = ["_" + self.name]
imports = [(self.lib_name, "_" + self.name)]
for i in self.inherits:
i_cls = eolian.Class.get_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))
l, n = conv_cls_name(i)
inherits.append("_" + n)
imports.append((l, "_" + n))
for l, n in imports:
l = l.lower()
gen.write("from %s import %s" % (l, n))
gen.write()
gen.write()
gen.write("class %s(%s):" % (self.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.name
)
)
gen.dedent()
gen.dedent()
gen.write()
class File(object):
def __init__(self, filepath):
self.filepath = filepath
self.pyxgen = PyxGenerator()
self.pygen = Generator()
self.cls = eolian.Class.get_by_file(filepath)
if not self.cls:
raise RuntimeError("Could not get class from %s" % (filepath))
self.gencls = Class.parse(self.cls)
def cdefs_generate(self):
gen = self.pyxgen
# gen.write('cdef extern from "*":')
# gen.indent()
# gen.write('struct CFILE "__FILE__"')
# gen.write('struct CLINE "__LINE__"')
# gen.write('struct CFUNC "__FUNCTION__"')
# gen.dedent()
# gen.write()
# gen.write('cdef extern from "Eina.h":')
# gen.indent()
# gen.write('Eina_Bool eina_main_loop_is(void)')
# gen.dedent()
# gen.write()
# gen.write('cdef extern from "Eo.h":')
# gen.indent()
#gen.write('Eina_Bool _eo_do_start(const Eo *obj, const Eo_Class *cur_klass, Eina_Bool is_super, Eina_Bool is_main_loop, const char *file, const char *func, int line)')
# gen.write('Eina_Bool _eo_do_start(const Eo *obj, const Eo_Class *cur_klass, Eina_Bool is_super, const char *file, const char *func, int line)')
#gen.write('void _eo_do_end(const Eo **ojb)')
# gen.dedent()
# gen.write()
gen.write('cdef extern from "%s":' % (self.gencls.header_path))
gen.indent()
for i in eolian.type_aliases_get_by_file(self.filepath):
base = i.base_type
if base.type == eolian.TypeType.REGULAR:
gen.write("ctypedef %s %s" % (i.name, base.name))
elif base.type == eolian.TypeType.FUNCTION:
ret = base.return_type.c_type
if ret is None:
ret = "void"
gen.write("ctypedef %s (*%s)(%s)" % (
ret,
i.name,
", ".join([i.c_type for i in base.arguments])))
elif base.type == eolian.TypeType.STRUCT:
gen.write("ctypedef struct %s:" % (i.name))
gen.indent()
for f in base.struct_fields:
gen.write("%s %s" % (f.type.c_type, f.name))
gen.dedent()
else:
log.error("Unhandled alias! %s %s %s %s", i.name, i.full_name, i.filename, i.description)
for i in eolian.type_structs_get_by_file(self.filepath):
log.error("Type struct not handled! %s %s %s %s", i.name, i.full_name, i.filename, i.description)
for i in eolian.type_enums_get_by_file(self.filepath):
log.error("Type enum not handled! %s %s %s %s", i.name, i.full_name, i.filename, i.description)
for i in eolian.variable_constants_get_by_file(self.filepath):
log.error("Variable constant not handled! %s", i)
for i in eolian.variable_globals_get_by_file(self.filepath):
log.error("Variable global not handled! %s", i)
self.gencls.cdefs_generate(gen)
gen.dedent()
gen.write()
def pyx_generate(self):
self.gencls.pyx_generate(self.pyxgen)
path = [args.output]
cls_name = self.cls.name.lower()
filename = cls_name + ".pxi"
namespaces = []
for ns in self.cls.namespaces:
namespaces.append(ns.lower())
if not namespaces:
log.warning("Class %s has no namespaces defined" % (self.cls.name))
nstmp = cls_name.partition("_")
namespace = nstmp[0]
filename = nstmp[2] + ".pxi"
else:
namespace = ".".join(namespaces)
filename = ".".join((namespace, filename))
path.append(filename)
pxi_path = os.path.join(*path)
self.pyxgen.printout(filepath=pxi_path)
def py_generate(self):
py_path = os.path.join(args.output, "__init__" + ".py")
if os.path.exists(py_path):
os.remove(py_path)
self.gencls.py_generate(self.pygen)
self.pygen.printout(filepath=py_path)
class Event(object):
def __init__(self, event):
self.event = event
#print(event)
def cdef_generate(self, gen):
gen.write("enum: %s" % (self.event.c_name))
def generate(self, gen):
pass
class_counter = Counter()
generated_class_counter = Counter()
function_counter = Counter()
generated_function_counter = Counter()
for path in args.paths:
for dirpath, dirnames, filenames in os.walk(path):
eolian.directory_scan(dirpath)
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))
try:
eolf = File(filename)
except Exception:
log.exception("Exception while creating %s" % (filename))
continue
try:
eolf.cdefs_generate()
except Exception:
log.exception("Exception while generating cdefs for %s" % (filename))
continue
try:
eolf.pyx_generate()
except Exception:
log.exception("Exception while generating pyx for %s" % (filename))
continue
def report():
print("===============================================")
print("Number of classes: %d" % (len(class_counter)))
print("Number of classes generated: %d" % (len(generated_class_counter)))
if len(class_counter) > 0:
print("Percentage of classes generated: %f" % (
float(len(generated_class_counter)) / float(len(class_counter)) * 100.0
))
print("Number of functions: %d" % (len(function_counter)))
print("Number of functions generated: %d" % (
len(generated_function_counter)))
if len(function_counter) > 0:
print("Percentage of functions generated: %f" % (
float(len(generated_function_counter)) / float(len(function_counter))
* 100.0
))
report()
eolian.shutdown()