"""doxygen_xml.py Python Interface to Doxygen XML Output DoxygenXml is the primary class defined herein. It uses all the other classes, and is used by: - ./docs/build.py via ./docs/api_doc_builder.py for the LVGL doc build to: - edit Doxyfile to replace 2 string tokens - run Doxygen - load Doxygen XML output and use it to: - generate API `.rst` pages, - populate a set of `doxygen_xml.<...>` dictionaries, and - use those dictionaries to determine in what LVGL documents hyperlinks to API pages might be useful and appends them if they are not already present. - ./scripts/gen_json/gen_json.py => pycparser_monkeypatch.py to - edit Doxyfile to replace 2 string tokens - silently run Doxygen - load Doxygen XML output and use it to: - populate a set of `doxygen_xml.<...>` dictionaries, and - monkey-patch pycparser AST (Abstract Syntax Tree) output to extract API documentation into its JSON output. "Loading Doxygen XML output" consists of - opening and parsing the XML `index.xml` file generated by Doxygen which is a summary of Doxygen-generated documentation linked to additional `.xml` filenames that have more details. Note that `index.xml` has a complete list of documented symbol-names from the C code. `./docs/api_doc_builder.py` searches those names using search term(s) to determine in what LVGL documents hyperlinks to API pages might be useful, and appends them to those documents if they are not already present. `index.xml` structure is described in the `doxygen/doxygen` repository on GitHub under `./templates/xml/index.xsd`, and the structure of the other files is described in `./templates/xml/compound.xsd`. Unlimited-length list of nodes. ... ... ... ... In `index.xml`, every element: - always has attributes "refid" and "kind", - always has exactly 1 element, and - is followed by an unbounded number of elements. Each element's 'kind' attribute will be one of the following: class struct union interface protocol category exception file namespace group page example dir type concept module which is a group that applies to many languages. Since Doxygen is parsing a C-language project, we will only find these in the `index.xml` file: - struct - union - file - page (unused) - dir (unused) In `'index.xml`, every element: - always has attributes "refid" and "kind", - always has exactly 1 element. The contents of the elements differ based on the "kind" of element they are contained by. kind="struct" - sub-elements only have `kind="variable"` attributes. kind="union" - sub-elements only have `kind="variable"` attributes. kind="file" - sub-elements can have `kind` attributes: - define - enum - enumvalue has refid that begins with containing enum's refid - variable - function - typedef - Has child element with macro name. refid contains something that looks like this: "lv__obj_8h_1a8ec0fe9743c5c015162d64df591cd719" /____________________________________________/ /_________/ | | +-- ``id`` attribute of applicable item | +-- stem of `.xml` filename containing details lv__obj_8h.xml has this format: lv_obj.h Section dealing with #include's (list, dependency graph, etc.) ...List of elements. ...kind can be: - define - enum - var - func ... ... ... ...List of all defines in file. ... LV_STATE_DEFAULT = 0x0000 ...List of all the enumeration's enumvalue's. ...List of all enums in the file. ...List of all variables in file. ...List of all functions in file. @file @brief description @file detailed description Only included if XML_PROGRAMLISTING = YES. ... ... ... kind="page" - Has no child members, but does have a child element (e.g. "deprecated" and "todo"). kind="dir" - do not have sub-elements - contents of element is a full path to a directory - If sorted alphabetically, they form a sequence that can be used to create something: E: E:/Dev E:/Dev/lvgl E:/Dev/lvgl/lvgl E:/Dev/lvgl/lvgl/src etc. - refid="..." contains the stem of another `.xml` file that contains more information on the directory, including any documentation on it found by Doxygen. That XML file's structure is: .xml: ------------ full_path_of_dir_wo_trailing_slash subdirectory_path subdirectory_path subdirectory_path ... (list elements --- one for each subdirectory in directory) ... filename filename filename ... (list elements --- one for each file in directory) ... Brief description of directory (Doxygen documentation) Detailed description of directory (Doxygen documentation) list - define - dir (unused) - enum - enumvalue - example (unused) - file - function - page (unused) - struct - typedef - union - variable Most elements have child elements with their own contents depending on the 'kind' of element they are in. This file defines classes, whose class names are the upper-case names above except for the unused ones. These classes are used to build a data structure that matches that of the `index.xml` file. The list of classes is: +-----------+----------------------------------------------------+ | Class | Adds New Instance to this Dictionary in __init__() | +===========+====================================================+ | DEFINE | defines | +-----------+----------------------------------------------------+ | ENUM | enums (have child ENUMVALUE objects) | +-----------+----------------------------------------------------+ | VARIABLE | variables | +-----------+----------------------------------------------------+ | NAMESPACE | namespaces | +-----------+----------------------------------------------------+ | STRUCT | structures (have child STRUCT_FIELD objects) | +-----------+----------------------------------------------------+ | UNION | appears to be dysfunctional based on lacking input | +-----------+----------------------------------------------------+ | TYPEDEF | typedefs | +-----------+----------------------------------------------------+ | FUNCTION | functions (have child FUNC_ARG objects) | +-----------+----------------------------------------------------+ | GROUP | groups | +-----------+----------------------------------------------------+ | FILE | files | +-----------+----------------------------------------------------+ | CLASS | classes | +-----------+----------------------------------------------------+ Additional classes: - FUNC_ARG(object): (becomes child members of FUNCTION objects) - STRUCT_FIELD(object): (becomes child members of STRUCT objects) - ENUMVALUE(object): (becomes child members of ENUM objects) - XMLSearch(object): (used only by ./scripts/gen_json/gen_json.py) Each of the above Dictionary variables has entries with - keys = documented symbol name from C code that Doxygen found - values = instances of classes named above Samples: 'defines': {'ZERO_MEM_SENTINEL': , 'LV_GLOBAL_DEFAULT': , 'LV_ASSERT_OBJ': , 'LV_TRACE_OBJ_CREATE': ,...} 'enums': {'lv_key_t': , 'lv_group_refocus_policy_t': , 'lv_obj_flag_t': , 'lv_obj_class_editable_t': ,...} 'variables': {'lv_global': , 'lv_obj_class': , 'lv_font_montserrat_8': , 'lv_font_montserrat_10': ,...} 'namespaces': {}, 'structures': {'_lv_anim_t::_lv_anim_path_para_t': , '_lv_anim_t': , '_lv_animimg_t': , '_lv_arc_t': ,...} 'unions': {}, 'typedefs': {'lv_global_t': , 'lv_group_focus_cb_t': , 'lv_group_edge_cb_t': ,...} 'functions': {'lv_group_create': , 'lv_group_delete': , 'lv_group_set_default': ,...} Additional dictionaries: 'files': {'lv_global.h': , 'lv_group.h': , 'lv_group_private.h': ,...} """ import os import sys import subprocess from xml.etree import ElementTree import doxygen_config from announce import * 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' # Dictionaries built from Doxygen XML output via `xml.etree.ElementTree` defines = {} # dictionary of doxygen_xml.DEFINE objects enums = {} # dictionary of doxygen_xml.ENUM objects variables = {} # dictionary of doxygen_xml.VARIABLE objects namespaces = {} # dictionary of doxygen_xml.NAMESPACE objects structures = {} # dictionary of doxygen_xml.STRUCT objects typedefs = {} # dictionary of doxygen_xml.TYPEDEF objects functions = {} # dictionary of doxygen_xml.FUNCTION objects groups = {} # dictionary of doxygen_xml.GROUP objects files = {} # dictionary of doxygen_xml.FILE objects classes = {} # dictionary of doxygen_xml.CLASS objects unions = {} # appears to be unused at this time (unions => structures dict). # Module-Global Variables xml_path = '' def run_ext_cmd(cmd_str: str, start_dir: str = None, quiet: bool = False, exit_on_error: bool = True): """ Run external command `cmd_str` (possibly silently) in directory `start_dir` (if provided), and optionally abort execution on error. :param cmd_str: String to pass to OS. :param start_dir: Directory to start in, or `None` to run in "cwd". :param quiet: Should STDOUT and STDERR be suppressed? :param exit_on_error: Should this function abort execution on a non-zero exit status? :return: n/a """ saved_dir = None if start_dir is not None: saved_dir = os.getcwd() os.chdir(start_dir) if quiet: # This method of running Doxygen is used because we do not # want anything going to STDOUT. Running it via `os.system()` # would send its output to STDOUT. p = subprocess.Popen( cmd_str, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True ) out, err = p.communicate() if p.returncode: if out: # Note the `.decode("utf-8")` is required because # `sys.stdout.write()` requires a string, and `out` is # a byte array; generates an exception if passed alone. sys.stdout.write(out.decode("utf-8")) sys.stdout.flush() if err: sys.stderr.write(err.decode("utf-8")) sys.stdout.flush() if exit_on_error: sys.exit(p.returncode) else: announce_start(__file__, f'Running [{cmd_str}] in [{os.getcwd()}]...') return_code = os.system(cmd_str) announce_finish() if return_code != 0 and exit_on_error: print(f'Exiting due to error [{return_code}] running [{cmd_str}].') sys.exit(return_code) if saved_dir is not None: os.chdir(saved_dir) 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 ElementTree.fromstring(d) except: # NOQA return None def load_xml_etree(fle): fle = os.path.join(xml_path, fle + '.xml') with open(fle, 'rb') as f: d = f.read().decode('utf-8') # This code is to correct a bug in Doxygen. That bug incorrectly parses # a typedef, and it causes an error to occur building the docs. The Error # doesn't stop the documentation from being generated, I just don't want # to see the ugly red output. # # if 'typedef void() lv_lru_free_t(void *v)' in d: # d = d.replace( # 'void()\n ' # 'typedef void() lv_lru_free_t(void *v)', # 'void\n ' # 'typedef void(lv_lru_free_t)(void *v)' # ) # with open(fle, 'wb') as f: # f.write(d.encode('utf-8')) return ElementTree.fromstring(d) def get_type(node): def gt(n): for c in n: if c.tag == 'ref': t = c.text.strip() break else: t = node.text.strip() return t.replace('*', '').replace('(', '').replace(')', '').strip() for child in node: if child.tag == 'type': return gt(child) 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 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): """ elements in Doxygen `index.xml`""" _missing = MISSING_STRUCT _missing_field = MISSING_STRUCT_FIELD template = '''\ .. doxygenstruct:: {name} :project: lvgl :members: :protected-members: :private-members: :undoc-members: ''' def __init__(self, parent, refid, name, **_): global structures global unions if type(self) is UNION: # UNION inherits from STRUCT. if name in unions: self.__dict__.update(unions[name].__dict__) else: unions[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 else: # STRUCT type if name in structures: self.__dict__.update(structures[name].__dict__) 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 # Prior to 9-Mar-2025, the code below was never executing since this # __init__() was never called with a `parent` value other than `None`. # Reason: `kind="struct"` only occurs in `index.xml` as a top-level # entry, and not as a child element of `kind="file"` as do # elements with kind = define, var, enum and func. # Original code: # if parent and refid: if refid: root = load_xml_etree(refid) for compounddef in root: if compounddef.attrib['id'] != self.refid: continue for child in compounddef: if child.tag == 'includes': self.header_file = os.path.splitext(child.text)[0] continue elif child.tag == 'location': self.file_name = child.attrib['file'] self.line_no = child.attrib['line'] elif child.tag == 'detaileddescription': self.description = build_docstring(child) elif child.tag == 'sectiondef': for memberdef in child: t = get_type(memberdef) description = None name = '' file_name = None line_no = None # For each struct member... for element in memberdef: if element.tag == 'location': file_name = element.attrib['file'] line_no = element.attrib['line'] elif element.tag == 'name': name = element.text elif element.tag == 'detaileddescription': description = build_docstring(element) field = STRUCT_FIELD(name, t, description, file_name, line_no) self.fields.append(field) if t is None: continue self.types.add(t) if EMIT_WARNINGS: 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): if self._deps is None: self._deps = dict( typedefs=set(), functions=set(), enums=set(), structures=set(), unions=set(), namespaces=set(), variables=set(), ) for type_ in self.types: if type_ in typedefs: self._deps['typedefs'].add(typedefs[type_]) elif type_ in structures: self._deps['structures'].add(structures[type_]) elif type_ in unions: self._deps['unions'].add(unions[type_]) elif type_ in enums: self._deps['enums'].add(enums[type_]) elif type_ in functions: self._deps['functions'].add(functions[type_]) elif type_ in variables: self._deps['variables'].add(variables[type_]) elif type_ in namespaces: self._deps['namespaces'].add(namespaces[type_]) return self._deps def __str__(self): return self.template.format(name=self.name) class UNION(STRUCT): """ elements in Doxygen `index.xml`""" _missing = MISSING_UNION _missing_field = MISSING_UNION_FIELD template = '''\ .. doxygenunion:: {name} :project: lvgl ''' class VARIABLE(object): """ elements in Doxygen `index.xml`""" template = '''\ .. doxygenvariable:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): global variables if name in variables: self.__dict__.update(variables[name].__dict__) 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 if parent is not None: root = load_xml_etree(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) class NAMESPACE(object): """ elements in Doxygen `index.xml`""" template = '''\ .. doxygennamespace:: {name} :project: lvgl :members: :protected-members: :private-members: :undoc-members: ''' def __init__(self, parent, refid, name, **_): global namespaces if name in namespaces: self.__dict__.update(namespaces[name].__dict__) 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) def __str__(self): return self.template.format(name=self.name) class GROUP(object): """ elements in Doxygen `index.xml`""" template = '''\ .. doxygengroup:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): global groups 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 FUNC_ARG(object): """ elements in Doxygen `index.xml`""" def __init__(self, name, _type): self.name = name self.type = _type self.description = None class FUNCTION(object): """ elements in Doxygen `index.xml`""" template = '''\ .. doxygenfunction:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): global functions if name in functions: self.__dict__.update(functions[name].__dict__) 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_etree(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'] != 'func': 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 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 @property def deps(self): if self._deps is None: self._deps = dict( typedefs=set(), functions=set(), enums=set(), structures=set(), unions=set(), namespaces=set(), variables=set(), ) if self.restype is not None: self.types.add(self.restype) for type_ in self.types: if type_ in typedefs: self._deps['typedefs'].add(typedefs[type_]) elif type_ in structures: self._deps['structures'].add(structures[type_]) elif type_ in unions: self._deps['unions'].add(unions[type_]) elif type_ in enums: self._deps['enums'].add(enums[type_]) elif type_ in functions: self._deps['functions'].add(functions[type_]) elif type_ in variables: self._deps['variables'].add(variables[type_]) elif type_ in namespaces: self._deps['namespaces'].add(namespaces[type_]) return self._deps def __str__(self): return self.template.format(name=self.name) class FILE(object): """ elements in Doxygen `index.xml`""" def __init__(self, _, refid, name, node, **__): global files if name.endswith('lv_types.h'): return if name in files: self.__dict__.update(files[name].__dict__) return files[name] = self self.refid = refid self.name = name self.header_file = os.path.splitext(name)[0] self.types_contained = set() enums_ = [] for member in node: if member.tag != 'member': continue cls_name = member.attrib['kind'].upper() self.types_contained.add(cls_name) cls = globals()[cls_name] if cls == ENUM: 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) else: member.attrib['name'] = member[0].text.strip() cls(self, **member.attrib) class ENUMVALUE(object): """ elements in Doxygen `index.xml`""" template = '''\ .. doxygenenumvalue:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): self.parent = parent self.refid = refid self.name = name self.description = None self.file_name = None self.line_no = None if parent is not None: if parent.file_name is not None: self.file_name = parent.file_name elif hasattr(parent, 'parent') and parent.parent is not None: if parent.parent.name is not None: self.file_name = parent.parent.name def __str__(self): return self.template.format(name=self.name) class ENUM(object): """ elements in Doxygen `index.xml`""" template = '''\ .. doxygenenum:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): global enums if name in enums: # This happens when `name` is `None`, for example. # This is true for unnamed enumerations. self.__dict__.update(enums[name].__dict__) else: enums[name] = self 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_etree(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 ( member.attrib['kind'] == 'enumvalue' and member.attrib['refid'].startswith(self.refid) ) 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'], name ) ) def __str__(self): template = [self.template.format(name=self.name)] template.extend(list(str(member) for member in self.members)) return '\n'.join(template) class DEFINE(object): """ elements in Doxygen `index.xml`""" template = '''\ .. doxygendefine:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): global defines if name in defines: self.__dict__.update(defines[name].__dict__) else: 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 if parent is not None: root = load_xml_etree(parent.refid) memberdef = [] 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) class TYPEDEF(object): template = '''\ .. doxygentypedef:: {name} :project: lvgl ''' def __init__(self, parent, refid, name, **_): global typedefs if name in typedefs: self.__dict__.update(typedefs[name].__dict__) else: typedefs[name] = self 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_etree(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'] != 'typedef': 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 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 def deps(self): if self._deps is None: self._deps = dict( typedefs=set(), functions=set(), enums=set(), structures=set(), unions=set(), namespaces=set(), variables=set(), ) if self.type is not None: type_ = self.type if type_ in typedefs: self._deps['typedefs'].add(typedefs[type_]) elif type_ in structures: self._deps['structures'].add(structures[type_]) elif type_ in unions: self._deps['unions'].add(unions[type_]) elif type_ in enums: self._deps['enums'].add(enums[type_]) elif type_ in functions: self._deps['functions'].add(functions[type_]) elif type_ in variables: self._deps['variables'].add(variables[type_]) elif type_ in namespaces: self._deps['namespaces'].add(namespaces[type_]) return self._deps def __str__(self): return self.template.format(name=self.name) class CLASS(object): """ elements in Doxygen `index.xml`""" def __init__(self, _, refid, name, node, **__): global classes if name in classes: self.__dict__.update(classes[name].__dict__) return classes[name] = self self.refid = refid self.name = name enums_ = [] for member in node: if member.tag != 'member': continue cls = globals()[member.attrib['kind'].upper()] if cls == ENUM: member.attrib['name'] = member[0].text.strip() enums_.append(cls(self, **member.attrib)) elif cls == ENUMVALUE: if enums_[-1].is_member(member): enums_[-1].add_member(member) else: member.attrib['name'] = member[0].text.strip() cls(self, **member.attrib) class DoxygenXml(object): """Opens, parses and loads a Doxygen-generated `index.xml` file and makes it available as a set of dictionary attributes of this module, documented at the top of this file and named below in `global` statements. """ def __init__(self, lvgl_src_dir: str, intermediate_dir: str, doxyfile_src_file: str, silent_mode=False, doxy_tagfile=''): """ - Prepare and run Doxygen, generating XML output. - Load that XML output, and use it to populate :param lvgl_src_dir: :param intermediate_dir: :param doxyfile_src_file: :param silent_mode: """ # Dictionaries to Be Populated: global defines global enums global variables global namespaces global structures global typedefs global functions global groups global files global classes global unions global xml_path announce_set_silent_mode(silent_mode) base_dir = os.path.abspath(os.path.dirname(__file__)) doxyfile_filename = str(os.path.split(doxyfile_src_file)[1]) doxyfile_dst_file = str(os.path.join(intermediate_dir, doxyfile_filename)) lv_conf_file = os.path.join(intermediate_dir, 'lv_conf.h') xml_path = os.path.join(intermediate_dir, 'xml') # In case DoxygenXml() is ever instantiated twice in 1 session, # clear these dictionaries before they are (re-)populated below. defines.clear() enums.clear() variables.clear() namespaces.clear() structures.clear() typedefs.clear() functions.clear() groups.clear() files.clear() classes.clear() unions.clear() # ----------------------------------------------------------------- # Prep and run Doxygen # ----------------------------------------------------------------- # Generate Doxyfile into `intermediate_dir` replacing certain # config options for this run. # 1. Load from Doxyfile cfg = doxygen_config.DoxygenConfig() cfg.load(doxyfile_src_file) # 2. Update cfg. cfg.set('OUTPUT_DIRECTORY', '.') cfg.set('XML_OUTPUT', "xml") cfg.set('HTML_OUTPUT', 'doxygen_html') cfg.set('INPUT', lvgl_src_dir) cfg.set('PREDEFINED', f'DOXYGEN LV_CONF_PATH="{lv_conf_file}"') cfg.set('QUIET', 'YES') cfg.set('GENERATE_HTML', 'NO') cfg.set('GENERATE_DOCSET', 'NO') cfg.set('GENERATE_HTMLHELP', 'NO') cfg.set('GENERATE_CHI', 'NO') cfg.set('GENERATE_QHP', 'NO') cfg.set('GENERATE_ECLIPSEHELP', 'NO') cfg.set('GENERATE_LATEX', 'NO') cfg.set('GENERATE_RTF', 'NO') cfg.set('GENERATE_MAN', 'NO') cfg.set('GENERATE_XML', 'YES') cfg.set('GENERATE_DOCBOOK', 'NO') cfg.set('GENERATE_PERLMOD', 'NO') # Include TAGFILES if requested. if doxy_tagfile: cfg.set('GENERATE_TAGFILE', doxy_tagfile) # 3. Store it for use by Doxygen in intermediate directory. cfg.save(doxyfile_dst_file) # Run Doxygen in intermediate directory. run_ext_cmd('doxygen Doxyfile', intermediate_dir, quiet=silent_mode) # ----------------------------------------------------------------- # Load root of Doxygen output (index.xml) as an `xml.etree.ElementTree`. # ----------------------------------------------------------------- index_xml_etree = load_xml_etree('index') # Populate these dictionaries. # Keys : C-code-element names (str) found by Doxygen. # Values: The XML-node created by `xml.etree::ElementTree` in `load_xml()` above. # # defines, enums, variables, # namespaces, structures, typedefs, # functions, unions, groups, # files, classes. announce_start(__file__, "Building source-code symbol dictionaries...") module_namespace = globals() for compound in index_xml_etree: # Here we will encounter these "kind" in the index.xml # elements: dir, file, page, struct, union. compound.attrib['name'] = compound[0].text.strip() # Filter out dir, page, example. if compound.attrib['kind'] not in ('example', 'page', 'dir'): class_name = compound.attrib['kind'].upper() class_obj = module_namespace[class_name] _ = class_obj(None, node=compound, **compound.attrib) announce_finish() # Additional data: the above instantiates the class, but doesn't # store the resulting object anywhere, since each class' __init__() # function adds the new object to the appropriate dictionary, # and uses the arguments it gets to "build out" additional # structure and/or populate additional dictionaries based on # content. Each class __init__() function specifies args it # in its parameter list. They match needed key values in the # `**compound.attrib` dictionary. The remaining (unused) # keys in that dictionary are accepted in the `**_` parameter # at the end of the parameter list. # # FILE class populates `files` dict plus: # - `defines` dictionary # - `enums` dictionary with children: # - ENUMVALUEs # - `variables` dictionary # - `typedefs` dictionary # - `functions` dictionary with children: # - FUNC_ARGs # STRUCT class populates the `structures` dictionary with children: # - STRUCT_FIELDs # # and possibly `namespaces`, `groups` and `classes` if # they are present in `index.xml`. def get_macros(self): global defines return list(defines.values()) def get_enum_item(self, e_name): global enums 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): global enums return enums.get(e_name, None) def get_function(self, f_name): global functions return functions.get(f_name, None) def get_variable(self, v_name): global variables return variables.get(v_name, None) def get_union(self, u_name): global unions return unions.get(u_name, None) def get_structure(self, s_name): global structures return structures.get(s_name, None) def get_typedef(self, t_name): global typedefs return typedefs.get(t_name, None) def get_macro(self, m_name): global defines return defines.get(m_name, None)