diff options
author | Kevin Schlosser <kdschlosser@users.noreply.github.com> | 2024-06-20 14:02:25 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-20 22:02:25 +0200 |
commit | ec80fe49fa3a3e239109949dd5ef2f84326f0fc5 (patch) | |
tree | 9330860a27b9de617935c990c9557da26c961792 /docs/doc_builder.py | |
parent | 25e993a1372a9e98187c37331a7d0705475ae431 (diff) | |
download | lvgl-ec80fe49fa3a3e239109949dd5ef2f84326f0fc5.tar.gz lvgl-ec80fe49fa3a3e239109949dd5ef2f84326f0fc5.zip |
feat: add API JSON generator (#5677)
Co-authored-by: Liam <30486941+liamHowatt@users.noreply.github.com>
Diffstat (limited to 'docs/doc_builder.py')
-rw-r--r-- | docs/doc_builder.py | 770 |
1 files changed, 705 insertions, 65 deletions
diff --git a/docs/doc_builder.py b/docs/doc_builder.py index ec9cd46b7..c22ba52fb 100644 --- a/docs/doc_builder.py +++ b/docs/doc_builder.py @@ -1,9 +1,79 @@ import os +import sys from xml.etree import ElementTree as ET base_path = '' xml_path = '' +EMIT_WARNINGS = True +DOXYGEN_OUTPUT = True + +MISSING_FUNC = 'MissingFunctionDoc' +MISSING_FUNC_ARG = 'MissingFunctionArgDoc' +MISSING_FUNC_RETURN = 'MissingFunctionReturnDoc' +MISSING_FUNC_ARG_MISMATCH = 'FunctionArgMissing' +MISSING_STRUCT = 'MissingStructureDoc' +MISSING_STRUCT_FIELD = 'MissingStructureFieldDoc' +MISSING_UNION = 'MissingUnionDoc' +MISSING_UNION_FIELD = 'MissingUnionFieldDoc' +MISSING_ENUM = 'MissingEnumDoc' +MISSING_ENUM_ITEM = 'MissingEnumItemDoc' +MISSING_TYPEDEF = 'MissingTypedefDoc' +MISSING_VARIABLE = 'MissingVariableDoc' +MISSING_MACRO = 'MissingMacroDoc' + + +def warn(warning_type, *args): + if EMIT_WARNINGS: + args = ' '.join(str(arg) for arg in args) + + if warning_type is None: + output = f'\033[31;1m {args}\033[0m\n' + else: + output = f'\033[31;1m{warning_type}: {args}\033[0m\n' + + sys.stdout.write(output) + sys.stdout.flush() + + +def build_docstring(element): + docstring = None + if element.tag == 'parameterlist': + return None + + if element.text: + docstring = element.text.strip() + + for item in element: + ds = build_docstring(item) + if ds: + if docstring: + docstring += ' ' + ds + else: + docstring = ds.strip() + + if element.tag == 'para': + if docstring: + docstring = '\n\n' + docstring + + if element.tag == 'ref': + docstring = f':ref:`{docstring}`' + + if element.tail: + if docstring: + docstring += ' ' + element.tail.strip() + else: + docstring = element.tail.strip() + + return docstring + + +def read_as_xml(d): + try: + return ET.fromstring(d) + except: # NOQA + return None + def load_xml(fle): fle = os.path.join(xml_path, fle + '.xml') @@ -39,7 +109,24 @@ namespaces = {} files = {} +# things to remove from description +# <para> </para> + + +class STRUCT_FIELD(object): + + def __init__(self, name, type, description, file_name, line_no): + self.name = name + self.type = type + self.description = description + self.file_name = file_name + self.line_no = line_no + + class STRUCT(object): + _missing = MISSING_STRUCT + _missing_field = MISSING_STRUCT_FIELD + template = '''\ .. doxygenstruct:: {name} :project: lvgl @@ -52,36 +139,83 @@ class STRUCT(object): def __init__(self, parent, refid, name, **_): if name in structures: self.__dict__.update(structures[name].__dict__) - return + else: + structures[name] = self + self.parent = parent + self.refid = refid + self.name = name + self.types = set() + self._deps = None + self.header_file = '' + self.description = None + self.fields = [] + self.file_name = None + self.line_no = None + + if parent and refid: + root = load_xml(refid) - structures[name] = self - self.parent = parent - self.refid = refid - self.name = name - self.types = set() - self._deps = None - self.header_file = '' + for compounddef in root: + if compounddef.attrib['id'] != self.refid: + continue - root = load_xml(refid) + for child in compounddef: + if child.tag == 'includes': + self.header_file = os.path.splitext(child.text)[0] + continue - for compounddef in root: - if compounddef.attrib['id'] != self.refid: - continue + elif child.tag == 'location': + self.file_name = child.attrib['file'] + self.line_no = child.attrib['line'] - for child in compounddef: - if child.tag == 'includes': - self.header_file = os.path.splitext(child.text)[0] + elif child.tag == 'detaileddescription': + self.description = build_docstring(child) - if child.tag != 'sectiondef': - continue + elif child.tag == 'sectiondef': + for memberdef in child: + t = get_type(memberdef) + description = None + name = '' + file_name = None + line_no = None - for memberdef in child: - t = get_type(memberdef) + for element in memberdef: + if element.tag == 'location': + file_name = element.attrib['file'] + line_no = element.attrib['line'] - if t is None: - continue + elif element.tag == 'name': + name = element.text + + elif element.tag == 'detaileddescription': + description = build_docstring(element) - self.types.add(t) + field = STRUCT_FIELD(name, t, description, file_name, line_no) + self.fields.append(field) + + if t is None: + continue + + self.types.add(t) + + if not self.description: + warn(self._missing, self.name) + warn(None, 'FILE:', self.file_name) + warn(None, 'LINE:', self.line_no) + warn(None) + + for field in self.fields: + if not field.description: + warn(self._missing_field, self.name) + warn(None, 'FIELD:', field.name) + warn(None, 'FILE:', field.file_name) + warn(None, 'LINE:', field.line_no) + warn(None) + + def get_field(self, name): + for field in self.fields: + if field.name == name: + return field @property def deps(self): @@ -117,6 +251,9 @@ class STRUCT(object): class UNION(STRUCT): + _missing = MISSING_UNION + _missing_field = MISSING_UNION_FIELD + template = '''\ .. doxygenunion:: {name} :project: lvgl @@ -148,12 +285,48 @@ class VARIABLE(object): def __init__(self, parent, refid, name, **_): if name in variables: self.__dict__.update(variables[name].__dict__) - return + else: + variables[name] = self + self.parent = parent + self.refid = refid + self.name = name + self.description = None + self.type = '' + self.file_name = None + self.line_no = None - variables[name] = self - self.parent = parent - self.refid = refid - self.name = name + if parent is not None: + root = load_xml(parent.refid) + + for compounddef in root: + if compounddef.attrib['id'] != parent.refid: + continue + + for child in compounddef: + if ( + child.tag == 'sectiondef' and + child.attrib['kind'] == 'var' + ): + for memberdef in child: + if memberdef.attrib['id'] == refid: + break + else: + continue + + self.type = get_type(memberdef) + + for element in memberdef: + if element.tag == 'location': + self.file_name = element.attrib['file'] + self.line_no = element.attrib['line'] + elif element.tag == 'detaileddescription': + self.description = build_docstring(element) + + if not self.description: + warn(MISSING_VARIABLE, self.name) + warn(None, 'FILE:', self.file_name) + warn(None, 'LINE:', self.line_no) + warn(None) def __str__(self): return self.template.format(name=self.name) @@ -172,17 +345,92 @@ class NAMESPACE(object): def __init__(self, parent, refid, name, **_): if name in namespaces: self.__dict__.update(namespaces[name].__dict__) - return + else: + namespaces[name] = self + self.parent = parent + self.refid = refid + self.name = name + self.description = None + self.line_no = None + self.file_name = None + self.enums = [] + self.funcs = [] + self.vars = [] + self.typedefs = [] + self.structs = [] + self.unions = [] + self.classes = [] + + # root = load_xml(refid) + # + # for compounddef in root: + # if compounddef.attrib['id'] != refid: + # continue + # + # for sectiondef in compounddef: + # if sectiondef.tag != 'sectiondef': + # continue + # + # enum + # typedef + # func + # struct + # union + # + # + # cls = globals()[sectiondef.attrib['kind'].upper()] + # if cls == ENUM: + # if sectiondef[0].text: + # sectiondef.attrib['name'] = sectiondef[0].text.strip() + # enums_.append(cls(self, **sectiondef.attrib)) + # else: + # sectiondef.attrib['name'] = None + # enums_.append(cls(self, **sectiondef.attrib)) + # + # elif cls == ENUMVALUE: + # if enums_[-1].is_member(sectiondef): + # enums_[-1].add_member(sectiondef) + # + # else: + # sectiondef.attrib['name'] = sectiondef[0].text.strip() + # cls(self, **sectiondef.attrib) - namespaces[name] = self - self.parent = parent - self.refid = refid + def __str__(self): + return self.template.format(name=self.name) + + +class FUNC_ARG(object): + + def __init__(self, name, type): self.name = name + self.type = type + self.description = None + + +groups = {} + + +class GROUP(object): + template = '''\ +.. doxygengroup:: {name} + :project: lvgl +''' + + def __init__(self, parent, refid, name, **_): + if name in groups: + self.__dict__.update(functions[name].__dict__) + else: + functions[name] = self + self.parent = parent + self.refid = refid + self.name = name + self.description = None def __str__(self): return self.template.format(name=self.name) + class FUNCTION(object): template = '''\ .. doxygenfunction:: {name} @@ -192,15 +440,20 @@ class FUNCTION(object): def __init__(self, parent, refid, name, **_): if name in functions: self.__dict__.update(functions[name].__dict__) - return - - functions[name] = self - self.parent = parent - self.refid = refid - self.name = name - self.types = set() - self.restype = None - self._deps = None + else: + functions[name] = self + self.parent = parent + self.refid = refid + self.name = name + self.types = set() + self.restype = None + self.args = [] + self._deps = None + self.description = None + self.res_description = None + self.file_name = None + self.line_no = None + self.void_return = False if parent is not None: root = load_xml(parent.refid) @@ -212,10 +465,14 @@ class FUNCTION(object): for child in compounddef: if child.tag != 'sectiondef': continue + if child.attrib['kind'] != 'func': continue for memberdef in child: + if 'id' not in memberdef.attrib: + continue + if memberdef.attrib['id'] == refid: break else: @@ -232,11 +489,88 @@ class FUNCTION(object): self.restype = get_type(memberdef) for child in memberdef: + if child.tag == 'type': + if child.text and child.text.strip() == 'void': + self.void_return = True + if child.tag == 'param': t = get_type(child) if t is not None: self.types.add(t) + for element in child: + if element.tag == 'declname': + arg = FUNC_ARG(element.text, t) + self.args.append(arg) + + for child in memberdef: + if child.tag == 'location': + self.file_name = child.attrib['file'] + self.line_no = child.attrib['line'] + + elif child.tag == 'detaileddescription': + self.description = build_docstring(child) + for element in child: + if element.tag != 'para': + continue + + for desc_element in element: + if desc_element.tag == 'simplesect' and desc_element.attrib['kind'] == 'return': + self.res_description = build_docstring(desc_element) + + if desc_element.tag != 'parameterlist': + continue + + for parameter_item in desc_element: + parameternamelist = parameter_item[0] + if parameternamelist.tag != 'parameternamelist': + continue + + parameter_name = parameternamelist[0].text + + try: + parameterdescription = parameter_item[1] + if parameterdescription.tag == 'parameterdescription': + parameter_description = build_docstring(parameterdescription) + else: + parameter_description = None + except IndexError: + parameter_description = None + + if parameter_name is not None: + for arg in self.args: + if arg.name != parameter_name: + continue + + arg.description = parameter_description + break + else: + warn(MISSING_FUNC_ARG_MISMATCH, self.name) + warn(None, 'ARG:', parameter_name) + warn(None, 'FILE:', self.file_name) + warn(None, 'LINE:', self.line_no) + warn(None) + + if not self.description: + warn(MISSING_FUNC, self.name) + warn(None, 'FILE:', self.file_name) + warn(None, 'LINE:', self.line_no) + warn(None) + else: + for arg in self.args: + if not arg.description: + warn(MISSING_FUNC_ARG, self.name) + warn(None, 'ARG:', arg.name) + warn(None, 'FILE:', self.file_name) + warn(None, 'LINE:', self.line_no) + warn(None) + + if not self.res_description and not self.void_return: + warn(MISSING_FUNC_RETURN, self.name) + warn(None, 'FILE:', self.file_name) + warn(None, 'LINE:', self.line_no) + warn(None) + if self.restype in self.types: self.restype = None @@ -277,6 +611,7 @@ class FUNCTION(object): class FILE(object): + def __init__(self, _, refid, name, node, **__): if name in files: self.__dict__.update(files[name].__dict__) @@ -296,8 +631,13 @@ class FILE(object): cls = globals()[member.attrib['kind'].upper()] if cls == ENUM: - member.attrib['name'] = member[0].text.strip() - enums_.append(cls(self, **member.attrib)) + if member[0].text: + member.attrib['name'] = member[0].text.strip() + enums_.append(cls(self, **member.attrib)) + else: + member.attrib['name'] = None + enums_.append(cls(self, **member.attrib)) + elif cls == ENUMVALUE: if enums_[-1].is_member(member): enums_[-1].add_member(member) @@ -316,14 +656,102 @@ class ENUM(object): def __init__(self, parent, refid, name, **_): if name in enums: self.__dict__.update(enums[name].__dict__) - return + else: - enums[name] = self + enums[name] = self - self.parent = parent - self.refid = refid - self.name = name - self.members = [] + self.parent = parent + self.refid = refid + self.name = name + self.members = [] + self.description = None + self.file_name = None + self.line_no = None + + if parent is not None: + root = load_xml(parent.refid) + + for compounddef in root: + if compounddef.attrib['id'] != parent.refid: + continue + + for child in compounddef: + if child.tag != 'sectiondef': + continue + + if child.attrib['kind'] != 'enum': + continue + + for memberdef in child: + if 'id' not in memberdef.attrib: + continue + + if memberdef.attrib['id'] == refid: + break + else: + continue + + break + else: + continue + + break + else: + return + # raise RuntimeError(f'not able to locate enum {name} ({refid})') + + for element in memberdef: + if element.tag == 'location': + self.file_name = element.attrib['file'] + self.line_no = element.attrib['line'] + + if element.tag == 'detaileddescription': + self.description = build_docstring(element) + elif element.tag == 'enumvalue': + item_name = None + item_description = None + item_file_name = None + item_line_no = None + + for s_element in element: + if s_element.tag == 'name': + item_name = s_element.text + elif s_element.tag == 'detaileddescription': + item_description = build_docstring(s_element) + + elif s_element.tag == 'location': + item_file_name = child.attrib['file'] + item_line_no = child.attrib['line'] + + if item_name is not None: + for ev in self.members: + if ev.name != item_name: + continue + break + else: + ev = ENUMVALUE( + self, + element.attrib['id'], + item_name + ) + + self.members.append(ev) + + ev.description = item_description + + if not self.description: + warn(MISSING_ENUM, self.name) + warn(None, 'FILE:', self.file_name) + warn(None, 'LINE:', self.line_no) + warn(None) + + for member in self.members: + if not member.description: + warn(MISSING_ENUM_ITEM, self.name) + warn(None, 'MEMBER:', member.name) + warn(None, 'FILE:', self.file_name) + warn(None, 'LINE:', self.line_no) + warn(None) def is_member(self, member): return ( @@ -332,11 +760,16 @@ class ENUM(object): ) def add_member(self, member): + name = member[0].text.strip() + for ev in self.members: + if ev.name == name: + return + self.members.append( ENUMVALUE( self, member.attrib['refid'], - member[0].text.strip() + name ) ) @@ -350,6 +783,29 @@ class ENUM(object): defines = {} +def build_define(element): + define = None + + if element.text: + define = element.text.strip() + + for item in element: + ds = build_define(item) + if ds: + if define: + define += ' ' + ds + else: + define = ds.strip() + + if element.tail: + if define: + define += ' ' + element.tail.strip() + else: + define = element.tail.strip() + + return define + + class DEFINE(object): template = '''\ .. doxygendefine:: {name} @@ -359,13 +815,75 @@ class DEFINE(object): def __init__(self, parent, refid, name, **_): if name in defines: self.__dict__.update(defines[name].__dict__) - return + else: + defines[name] = self - defines[name] = self + self.parent = parent + self.refid = refid + self.name = name + self.description = None + self.file_name = None + self.line_no = None + self.params = None + self.initializer = None - self.parent = parent - self.refid = refid - self.name = name + if parent is not None: + root = load_xml(parent.refid) + + for compounddef in root: + if compounddef.attrib['id'] != parent.refid: + continue + + for child in compounddef: + if child.tag != 'sectiondef': + continue + + if child.attrib['kind'] != 'define': + continue + + for memberdef in child: + if memberdef.attrib['id'] == refid: + break + else: + continue + + break + else: + continue + + break + else: + return + + for element in memberdef: + if element.tag == 'location': + self.file_name = element.attrib['file'] + self.line_no = element.attrib['line'] + + elif element.tag == 'detaileddescription': + self.description = build_docstring(element) + + elif element.tag == 'param': + for child in element: + if child.tag == 'defname': + if self.params is None: + self.params = [] + + if child.text: + self.params.append(child.text) + + elif element.tag == 'initializer': + initializer = build_define(element) + if initializer is None: + self.initializer = '' + else: + self.initializer = initializer + + if not self.description: + warn(MISSING_MACRO, self.name) + warn(None, 'FILE:', self.file_name) + warn(None, 'LINE:', self.line_no) + warn(None) def __str__(self): return self.template.format(name=self.name) @@ -381,6 +899,9 @@ class ENUMVALUE(object): self.parent = parent self.refid = refid self.name = name + self.description = None + self.file_name = None + self.line_no = None def __str__(self): return self.template.format(name=self.name) @@ -395,15 +916,17 @@ class TYPEDEF(object): def __init__(self, parent, refid, name, **_): if name in typedefs: self.__dict__.update(typedefs[name].__dict__) - return + else: + typedefs[name] = self - typedefs[name] = self - - self.parent = parent - self.refid = refid - self.name = name - self.type = None - self._deps = None + self.parent = parent + self.refid = refid + self.name = name + self.type = None + self._deps = None + self.description = None + self.file_name = None + self.line_no = None if parent is not None: root = load_xml(parent.refid) @@ -419,6 +942,9 @@ class TYPEDEF(object): continue for memberdef in child: + if 'id' not in memberdef.attrib: + continue + if memberdef.attrib['id'] == refid: break else: @@ -432,6 +958,20 @@ class TYPEDEF(object): else: return + for element in memberdef: + if element.tag == 'location': + self.file_name = element.attrib['file'] + self.line_no = element.attrib['line'] + + if element.tag == 'detaileddescription': + self.description = build_docstring(element) + + if not self.description: + warn(MISSING_TYPEDEF, self.name) + warn(None, 'FILE:', self.file_name) + warn(None, 'LINE:', self.line_no) + warn(None) + self.type = get_type(memberdef) @property @@ -622,7 +1162,7 @@ def get_includes(name1, name2, obj, includes): if not is_name_match(name1, name2): return - if obj.parent is not None: + if obj.parent is not None and hasattr(obj.parent, 'header_file'): header_file = obj.parent.header_file elif hasattr(obj, 'header_file'): header_file = obj.header_file @@ -638,12 +1178,112 @@ def get_includes(name1, name2, obj, includes): includes.add((header_file, html_files[header_file])) +class XMLSearch(object): + + def __init__(self, temp_directory): + global xml_path + import subprocess + import re + import sys + + bp = os.path.abspath(os.path.dirname(__file__)) + + lvgl_path = os.path.join(temp_directory, 'lvgl') + src_path = os.path.join(lvgl_path, 'src') + + doxy_path = os.path.join(bp, 'Doxyfile') + + with open(doxy_path, 'rb') as f: + data = f.read().decode('utf-8') + + data = data.replace( + '#*#*LV_CONF_PATH*#*#', + os.path.join(temp_directory, 'lv_conf.h') + ) + data = data.replace('*#*#SRC#*#*', '"{0}"'.format(src_path)) + + with open(os.path.join(temp_directory, 'Doxyfile'), 'wb') as f: + f.write(data.encode('utf-8')) + + status, br = subprocess.getstatusoutput("git branch") + _, gitcommit = subprocess.getstatusoutput("git rev-parse HEAD") + br = re.sub('\* ', '', br) + + urlpath = re.sub('release/', '', br) + + os.environ['LVGL_URLPATH'] = urlpath + os.environ['LVGL_GITCOMMIT'] = gitcommit + + p = subprocess.Popen( + f'cd "{temp_directory}" && doxygen Doxyfile', + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True + ) + + out, err = p.communicate() + if p.returncode: + if out: + sys.stdout.write(out) + sys.stdout.flush() + if err: + sys.stderr.write(err) + sys.stdout.flush() + + sys.exit(p.returncode) + + xml_path = os.path.join(temp_directory, 'xml') + + self.index = load_xml('index') + + for compound in self.index: + compound.attrib['name'] = compound[0].text.strip() + if compound.attrib['kind'] in ('example', 'page', 'dir'): + continue + + globals()[compound.attrib['kind'].upper()]( + None, + node=compound, + **compound.attrib + ) + + def get_macros(self): + return list(defines.values()) + + def get_enum_item(self, e_name): + for enum, obj in enums.items(): + for enum_item in obj.members: + if enum_item.name == e_name: + return enum_item + + def get_enum(self, e_name): + return enums.get(e_name, None) + + def get_function(self, f_name): + return functions.get(f_name, None) + + def get_variable(self, v_name): + return variables.get(v_name, None) + + def get_union(self, u_name): + return unions.get(u_name, None) + + def get_structure(self, s_name): + return structures.get(s_name, None) + + def get_typedef(self, t_name): + return typedefs.get(t_name, None) + + def get_macro(self, m_name): + return defines.get(m_name, None) + + def run(project_path, temp_directory, *doc_paths): global base_path global xml_path global lvgl_src_path global api_path - + base_path = temp_directory xml_path = os.path.join(base_path, 'xml') api_path = os.path.join(base_path, 'API') @@ -651,7 +1291,7 @@ def run(project_path, temp_directory, *doc_paths): if not os.path.exists(api_path): os.makedirs(api_path) - + iter_src('API', '') index = load_xml('index') |