1""" 2This file opens the XML `index.xml` file generated by Doxygen 3which has the following structure: 4 5 <doxygenindex ...> 6 <compound ... > 7 </compound> 8 ... 9 </doxygenindex> 10 11Each <compound> element has a 'kind' attribute that is one of the following: 12 13 - define 14 - dir 15 - enum 16 - enumvalue 17 - example 18 - file 19 - function 20 - page 21 - struct 22 - typedef 23 - union 24 - variable 25 26Most <compound> elements have child <member> elements with their own contents 27depending on the 'kind' of <compound> element they are in. 28 29This file defines classes for each of these except for 30 31 - dir 32 - page 33 - example 34 35The remaining 'kind' values are: 36 37 - define 38 - enum 39 - enumvalue 40 - file 41 - function 42 - struct 43 - typedef 44 - union 45 - variable 46 47The list of classes is: 48 49 Class | Adds Self to Dictionary in __init__() 50 --------- | -------------------------------------- 51 DEFINE => `defines` 52 ENUM => `enums` 53 VARIABLE => `variables` 54 NAMESPACE => `namespaces` 55 STRUCT => `structures` 56 UNION appears to have a different purpose 57 TYPEDEF => `typedefs` 58 FUNCTION => `functions` 59 GROUP => `groups` 60 FILE => `files` 61 CLASS => `classes` 62 63Additional classes: 64 65 - NAMESPACE(object): 66 - FUNC_ARG(object): (becomes members of FUNCTION objects) 67 - STRUCT_FIELD(object): (becomes members of STRUCT objects) 68 - GROUP(object): 69 - CLASS(object): 70 - XMLSearch(object): 71 72Each of the above Dictionary variables has entries with 73 74 - keys = actual name of the code elements Doxygen found in the .H files. 75 - values = XML node generated by `xml.etree::ElementTree` 76 77Samples: 78 79 'defines': {'ZERO_MEM_SENTINEL': <doc_builder.DEFINE object at 0x000001FB5D866420>, 80 'LV_GLOBAL_DEFAULT': <doc_builder.DEFINE object at 0x000001FB5D866210>, 81 'LV_ASSERT_OBJ': <doc_builder.DEFINE object at 0x000001FB5D1EC080>, 82 'LV_TRACE_OBJ_CREATE': <doc_builder.DEFINE object at 0x000001FB5D8660F0>, 83 84 'enums': {'lv_key_t': <doc_builder.ENUM object at 0x000001FB5D1EEB40>, 85 'lv_group_refocus_policy_t': <doc_builder.ENUM object at 0x000001FB5D1E3DA0>, 86 'lv_obj_flag_t': <doc_builder.ENUM object at 0x000001FB5D29F830>, 87 'lv_obj_class_editable_t': <doc_builder.ENUM object at 0x000001FB5D29E300>, 88 89 'variables': {'lv_global': <doc_builder.VARIABLE object at 0x000001FB5D1E3FE0>, 90 'lv_obj_class': <doc_builder.VARIABLE object at 0x000001FB5D1EE1E0>, 91 'lv_font_montserrat_8': <doc_builder.VARIABLE object at 0x000001FB5DAB41A0>, 92 'lv_font_montserrat_10': <doc_builder.VARIABLE object at 0x000001FB5D99D040>, 93 94 'namespaces': {}, 95 96 'structures': {'_lv_anim_t::_lv_anim_path_para_t': <doc_builder.UNION object at 0x000001FB5C4240E0>, 97 '_lv_anim_t': <doc_builder.STRUCT object at 0x000001FB5C45F680>, 98 '_lv_animimg_t': <doc_builder.STRUCT object at 0x000001FB5C4FE390>, 99 '_lv_arc_t': <doc_builder.STRUCT object at 0x000001FB59D350A0>, 100 101 'unions': {}, 102 103 'typedefs': {'lv_global_t': <doc_builder.TYPEDEF object at 0x000001FB5D1EFFE0>, 104 'lv_group_focus_cb_t': <doc_builder.TYPEDEF object at 0x000001FB5D1F1CA0>, 105 'lv_group_edge_cb_t': <doc_builder.TYPEDEF object at 0x000001FB5D1EE7E0>, 106 107 'functions': {'lv_group_create': <doc_builder.FUNCTION object at 0x000001FB5D1E0470>, 108 'lv_group_delete': <doc_builder.FUNCTION object at 0x000001FB5D1F3800>, 109 'lv_group_set_default': <doc_builder.FUNCTION object at 0x000001FB5D1ECAA0>, 110 111Additional dictionaries: 112 'files': {'lv_global.h': <doc_builder.FILE object at 0x000001FB5D864E00>, 113 'lv_group.h': <doc_builder.FILE object at 0x000001FB5D1EFD40>, 114 'lv_group_private.h': <doc_builder.FILE object at 0x000001FB5D0D7DD0>, 115 116 'html_files': {'lvgl': 'lvgl.html', 117 'lv_api_map_v8': 'lv_api_map_v8.html', 118 'lv_api_map_v9_0': 'lv_api_map_v9_0.html', 119 'lv_api_map_v9_1': 'lv_api_map_v9_1.html', 120 121""" 122import os 123import sys 124from xml.etree import ElementTree as ET 125 126base_path = '' 127xml_path = '' 128 129EMIT_WARNINGS = True 130DOXYGEN_OUTPUT = True 131 132MISSING_FUNC = 'MissingFunctionDoc' 133MISSING_FUNC_ARG = 'MissingFunctionArgDoc' 134MISSING_FUNC_RETURN = 'MissingFunctionReturnDoc' 135MISSING_FUNC_ARG_MISMATCH = 'FunctionArgMissing' 136MISSING_STRUCT = 'MissingStructureDoc' 137MISSING_STRUCT_FIELD = 'MissingStructureFieldDoc' 138MISSING_UNION = 'MissingUnionDoc' 139MISSING_UNION_FIELD = 'MissingUnionFieldDoc' 140MISSING_ENUM = 'MissingEnumDoc' 141MISSING_ENUM_ITEM = 'MissingEnumItemDoc' 142MISSING_TYPEDEF = 'MissingTypedefDoc' 143MISSING_VARIABLE = 'MissingVariableDoc' 144MISSING_MACRO = 'MissingMacroDoc' 145 146 147def warn(warning_type, *args): 148 if EMIT_WARNINGS: 149 args = ' '.join(str(arg) for arg in args) 150 151 if warning_type is None: 152 output = f'\033[31;1m {args}\033[0m\n' 153 else: 154 output = f'\033[31;1m{warning_type}: {args}\033[0m\n' 155 156 sys.stdout.write(output) 157 sys.stdout.flush() 158 159 160def build_docstring(element): 161 docstring = None 162 if element.tag == 'parameterlist': 163 return None 164 165 if element.text: 166 docstring = element.text.strip() 167 168 for item in element: 169 ds = build_docstring(item) 170 if ds: 171 if docstring: 172 docstring += ' ' + ds 173 else: 174 docstring = ds.strip() 175 176 if element.tag == 'para': 177 if docstring: 178 docstring = '\n\n' + docstring 179 180 if element.tag == 'ref': 181 docstring = f':ref:`{docstring}`' 182 183 if element.tail: 184 if docstring: 185 docstring += ' ' + element.tail.strip() 186 else: 187 docstring = element.tail.strip() 188 189 return docstring 190 191 192def read_as_xml(d): 193 try: 194 return ET.fromstring(d) 195 except: # NOQA 196 return None 197 198 199def load_xml(fle): 200 fle = os.path.join(xml_path, fle + '.xml') 201 202 with open(fle, 'rb') as f: 203 d = f.read().decode('utf-8') 204 205 # This code is to correct a bug in Doxygen. That bug incorrectly parses 206 # a typedef and it causes an error to occur building the docs. The Error 207 # doesn't stop the documentation from being generated, I just don't want 208 # to see the ugly red output. 209 # 210 # if 'typedef void() lv_lru_free_t(void *v)' in d: 211 # d = d.replace( 212 # '<type>void()</type>\n ' 213 # '<definition>typedef void() lv_lru_free_t(void *v)</definition>', 214 # '<type>void</type>\n ' 215 # '<definition>typedef void(lv_lru_free_t)(void *v)</definition>' 216 # ) 217 # with open(fle, 'wb') as f: 218 # f.write(d.encode('utf-8')) 219 220 return ET.fromstring(d) 221 222 223structures = {} 224functions = {} 225enums = {} 226typedefs = {} 227variables = {} 228unions = {} 229namespaces = {} 230files = {} 231 232 233# things to remove from description 234# <para> </para> 235 236 237class STRUCT_FIELD(object): 238 239 def __init__(self, name, type, description, file_name, line_no): 240 self.name = name 241 self.type = type 242 self.description = description 243 self.file_name = file_name 244 self.line_no = line_no 245 246 247class STRUCT(object): 248 _missing = MISSING_STRUCT 249 _missing_field = MISSING_STRUCT_FIELD 250 251 template = '''\ 252.. doxygenstruct:: {name} 253 :project: lvgl 254 :members: 255 :protected-members: 256 :private-members: 257 :undoc-members: 258''' 259 260 def __init__(self, parent, refid, name, **_): 261 if name in structures: 262 self.__dict__.update(structures[name].__dict__) 263 else: 264 structures[name] = self 265 self.parent = parent 266 self.refid = refid 267 self.name = name 268 self.types = set() 269 self._deps = None 270 self.header_file = '' 271 self.description = None 272 self.fields = [] 273 self.file_name = None 274 self.line_no = None 275 276 if parent and refid: 277 root = load_xml(refid) 278 279 for compounddef in root: 280 if compounddef.attrib['id'] != self.refid: 281 continue 282 283 for child in compounddef: 284 if child.tag == 'includes': 285 self.header_file = os.path.splitext(child.text)[0] 286 continue 287 288 elif child.tag == 'location': 289 self.file_name = child.attrib['file'] 290 self.line_no = child.attrib['line'] 291 292 elif child.tag == 'detaileddescription': 293 self.description = build_docstring(child) 294 295 elif child.tag == 'sectiondef': 296 for memberdef in child: 297 t = get_type(memberdef) 298 description = None 299 name = '' 300 file_name = None 301 line_no = None 302 303 for element in memberdef: 304 if element.tag == 'location': 305 file_name = element.attrib['file'] 306 line_no = element.attrib['line'] 307 308 elif element.tag == 'name': 309 name = element.text 310 311 elif element.tag == 'detaileddescription': 312 description = build_docstring(element) 313 314 field = STRUCT_FIELD(name, t, description, file_name, line_no) 315 self.fields.append(field) 316 317 if t is None: 318 continue 319 320 self.types.add(t) 321 322 if not self.description: 323 warn(self._missing, self.name) 324 warn(None, 'FILE:', self.file_name) 325 warn(None, 'LINE:', self.line_no) 326 warn(None) 327 328 for field in self.fields: 329 if not field.description: 330 warn(self._missing_field, self.name) 331 warn(None, 'FIELD:', field.name) 332 warn(None, 'FILE:', field.file_name) 333 warn(None, 'LINE:', field.line_no) 334 warn(None) 335 336 def get_field(self, name): 337 for field in self.fields: 338 if field.name == name: 339 return field 340 341 @property 342 def deps(self): 343 if self._deps is None: 344 self._deps = dict( 345 typedefs=set(), 346 functions=set(), 347 enums=set(), 348 structures=set(), 349 unions=set(), 350 namespaces=set(), 351 variables=set(), 352 ) 353 for type_ in self.types: 354 if type_ in typedefs: 355 self._deps['typedefs'].add(typedefs[type_]) 356 elif type_ in structures: 357 self._deps['structures'].add(structures[type_]) 358 elif type_ in unions: 359 self._deps['unions'].add(unions[type_]) 360 elif type_ in enums: 361 self._deps['enums'].add(enums[type_]) 362 elif type_ in functions: 363 self._deps['functions'].add(functions[type_]) 364 elif type_ in variables: 365 self._deps['variables'].add(variables[type_]) 366 elif type_ in namespaces: 367 self._deps['namespaces'].add(namespaces[type_]) 368 return self._deps 369 370 def __str__(self): 371 return self.template.format(name=self.name) 372 373 374class UNION(STRUCT): 375 _missing = MISSING_UNION 376 _missing_field = MISSING_UNION_FIELD 377 378 template = '''\ 379.. doxygenunion:: {name} 380 :project: lvgl 381''' 382 383 384def get_type(node): 385 def gt(n): 386 for c in n: 387 if c.tag == 'ref': 388 t = c.text.strip() 389 break 390 else: 391 t = node.text.strip() 392 393 return t.replace('*', '').replace('(', '').replace(')', '').strip() 394 395 for child in node: 396 if child.tag == 'type': 397 return gt(child) 398 399 400class VARIABLE(object): 401 template = '''\ 402.. doxygenvariable:: {name} 403 :project: lvgl 404''' 405 406 def __init__(self, parent, refid, name, **_): 407 if name in variables: 408 self.__dict__.update(variables[name].__dict__) 409 else: 410 variables[name] = self 411 self.parent = parent 412 self.refid = refid 413 self.name = name 414 self.description = None 415 self.type = '' 416 self.file_name = None 417 self.line_no = None 418 419 if parent is not None: 420 root = load_xml(parent.refid) 421 422 for compounddef in root: 423 if compounddef.attrib['id'] != parent.refid: 424 continue 425 426 for child in compounddef: 427 if ( 428 child.tag == 'sectiondef' and 429 child.attrib['kind'] == 'var' 430 ): 431 for memberdef in child: 432 if memberdef.attrib['id'] == refid: 433 break 434 else: 435 continue 436 437 self.type = get_type(memberdef) 438 439 for element in memberdef: 440 if element.tag == 'location': 441 self.file_name = element.attrib['file'] 442 self.line_no = element.attrib['line'] 443 elif element.tag == 'detaileddescription': 444 self.description = build_docstring(element) 445 446 if not self.description: 447 warn(MISSING_VARIABLE, self.name) 448 warn(None, 'FILE:', self.file_name) 449 warn(None, 'LINE:', self.line_no) 450 warn(None) 451 452 def __str__(self): 453 return self.template.format(name=self.name) 454 455 456class NAMESPACE(object): 457 template = '''\ 458.. doxygennamespace:: {name} 459 :project: lvgl 460 :members: 461 :protected-members: 462 :private-members: 463 :undoc-members: 464''' 465 466 def __init__(self, parent, refid, name, **_): 467 if name in namespaces: 468 self.__dict__.update(namespaces[name].__dict__) 469 else: 470 namespaces[name] = self 471 self.parent = parent 472 self.refid = refid 473 self.name = name 474 self.description = None 475 self.line_no = None 476 self.file_name = None 477 self.enums = [] 478 self.funcs = [] 479 self.vars = [] 480 self.typedefs = [] 481 self.structs = [] 482 self.unions = [] 483 self.classes = [] 484 485 # root = load_xml(refid) 486 # 487 # for compounddef in root: 488 # if compounddef.attrib['id'] != refid: 489 # continue 490 # 491 # for sectiondef in compounddef: 492 # if sectiondef.tag != 'sectiondef': 493 # continue 494 # 495 # enum 496 # typedef 497 # func 498 # struct 499 # union 500 # 501 # 502 # cls = globals()[sectiondef.attrib['kind'].upper()] 503 # if cls == ENUM: 504 # if sectiondef[0].text: 505 # sectiondef.attrib['name'] = sectiondef[0].text.strip() 506 # enums_.append(cls(self, **sectiondef.attrib)) 507 # else: 508 # sectiondef.attrib['name'] = None 509 # enums_.append(cls(self, **sectiondef.attrib)) 510 # 511 # elif cls == ENUMVALUE: 512 # if enums_[-1].is_member(sectiondef): 513 # enums_[-1].add_member(sectiondef) 514 # 515 # else: 516 # sectiondef.attrib['name'] = sectiondef[0].text.strip() 517 # cls(self, **sectiondef.attrib) 518 519 def __str__(self): 520 return self.template.format(name=self.name) 521 522 523class FUNC_ARG(object): 524 525 def __init__(self, name, type): 526 self.name = name 527 self.type = type 528 self.description = None 529 530 531groups = {} 532 533 534class GROUP(object): 535 template = '''\ 536.. doxygengroup:: {name} 537 :project: lvgl 538''' 539 540 def __init__(self, parent, refid, name, **_): 541 if name in groups: 542 self.__dict__.update(functions[name].__dict__) 543 else: 544 functions[name] = self 545 self.parent = parent 546 self.refid = refid 547 self.name = name 548 self.description = None 549 550 def __str__(self): 551 return self.template.format(name=self.name) 552 553 554 555class FUNCTION(object): 556 template = '''\ 557.. doxygenfunction:: {name} 558 :project: lvgl 559''' 560 561 def __init__(self, parent, refid, name, **_): 562 if name in functions: 563 self.__dict__.update(functions[name].__dict__) 564 else: 565 functions[name] = self 566 self.parent = parent 567 self.refid = refid 568 self.name = name 569 self.types = set() 570 self.restype = None 571 self.args = [] 572 self._deps = None 573 self.description = None 574 self.res_description = None 575 self.file_name = None 576 self.line_no = None 577 self.void_return = False 578 579 if parent is not None: 580 root = load_xml(parent.refid) 581 582 for compounddef in root: 583 if compounddef.attrib['id'] != parent.refid: 584 continue 585 586 for child in compounddef: 587 if child.tag != 'sectiondef': 588 continue 589 590 if child.attrib['kind'] != 'func': 591 continue 592 593 for memberdef in child: 594 if 'id' not in memberdef.attrib: 595 continue 596 597 if memberdef.attrib['id'] == refid: 598 break 599 else: 600 continue 601 602 break 603 else: 604 continue 605 606 break 607 else: 608 return 609 610 self.restype = get_type(memberdef) 611 612 for child in memberdef: 613 if child.tag == 'type': 614 if child.text and child.text.strip() == 'void': 615 self.void_return = True 616 617 if child.tag == 'param': 618 t = get_type(child) 619 if t is not None: 620 self.types.add(t) 621 622 for element in child: 623 if element.tag == 'declname': 624 arg = FUNC_ARG(element.text, t) 625 self.args.append(arg) 626 627 for child in memberdef: 628 if child.tag == 'location': 629 self.file_name = child.attrib['file'] 630 self.line_no = child.attrib['line'] 631 632 elif child.tag == 'detaileddescription': 633 self.description = build_docstring(child) 634 for element in child: 635 if element.tag != 'para': 636 continue 637 638 for desc_element in element: 639 if desc_element.tag == 'simplesect' and desc_element.attrib['kind'] == 'return': 640 self.res_description = build_docstring(desc_element) 641 642 if desc_element.tag != 'parameterlist': 643 continue 644 645 for parameter_item in desc_element: 646 parameternamelist = parameter_item[0] 647 if parameternamelist.tag != 'parameternamelist': 648 continue 649 650 parameter_name = parameternamelist[0].text 651 652 try: 653 parameterdescription = parameter_item[1] 654 if parameterdescription.tag == 'parameterdescription': 655 parameter_description = build_docstring(parameterdescription) 656 else: 657 parameter_description = None 658 except IndexError: 659 parameter_description = None 660 661 if parameter_name is not None: 662 for arg in self.args: 663 if arg.name != parameter_name: 664 continue 665 666 arg.description = parameter_description 667 break 668 else: 669 warn(MISSING_FUNC_ARG_MISMATCH, self.name) 670 warn(None, 'ARG:', parameter_name) 671 warn(None, 'FILE:', self.file_name) 672 warn(None, 'LINE:', self.line_no) 673 warn(None) 674 675 if not self.description: 676 warn(MISSING_FUNC, self.name) 677 warn(None, 'FILE:', self.file_name) 678 warn(None, 'LINE:', self.line_no) 679 warn(None) 680 else: 681 for arg in self.args: 682 if not arg.description: 683 warn(MISSING_FUNC_ARG, self.name) 684 warn(None, 'ARG:', arg.name) 685 warn(None, 'FILE:', self.file_name) 686 warn(None, 'LINE:', self.line_no) 687 warn(None) 688 689 if not self.res_description and not self.void_return: 690 warn(MISSING_FUNC_RETURN, self.name) 691 warn(None, 'FILE:', self.file_name) 692 warn(None, 'LINE:', self.line_no) 693 warn(None) 694 695 if self.restype in self.types: 696 self.restype = None 697 698 @property 699 def deps(self): 700 if self._deps is None: 701 self._deps = dict( 702 typedefs=set(), 703 functions=set(), 704 enums=set(), 705 structures=set(), 706 unions=set(), 707 namespaces=set(), 708 variables=set(), 709 ) 710 if self.restype is not None: 711 self.types.add(self.restype) 712 713 for type_ in self.types: 714 if type_ in typedefs: 715 self._deps['typedefs'].add(typedefs[type_]) 716 elif type_ in structures: 717 self._deps['structures'].add(structures[type_]) 718 elif type_ in unions: 719 self._deps['unions'].add(unions[type_]) 720 elif type_ in enums: 721 self._deps['enums'].add(enums[type_]) 722 elif type_ in functions: 723 self._deps['functions'].add(functions[type_]) 724 elif type_ in variables: 725 self._deps['variables'].add(variables[type_]) 726 elif type_ in namespaces: 727 self._deps['namespaces'].add(namespaces[type_]) 728 return self._deps 729 730 def __str__(self): 731 return self.template.format(name=self.name) 732 733 734class FILE(object): 735 736 def __init__(self, _, refid, name, node, **__): 737 if name in files: 738 self.__dict__.update(files[name].__dict__) 739 return 740 741 files[name] = self 742 743 self.refid = refid 744 self.name = name 745 self.header_file = os.path.splitext(name)[0] 746 747 enums_ = [] 748 749 for member in node: 750 if member.tag != 'member': 751 continue 752 753 cls = globals()[member.attrib['kind'].upper()] 754 if cls == ENUM: 755 if member[0].text: 756 member.attrib['name'] = member[0].text.strip() 757 enums_.append(cls(self, **member.attrib)) 758 else: 759 member.attrib['name'] = None 760 enums_.append(cls(self, **member.attrib)) 761 762 elif cls == ENUMVALUE: 763 if enums_[-1].is_member(member): 764 enums_[-1].add_member(member) 765 766 else: 767 member.attrib['name'] = member[0].text.strip() 768 cls(self, **member.attrib) 769 770 771class ENUM(object): 772 template = '''\ 773.. doxygenenum:: {name} 774 :project: lvgl 775''' 776 777 def __init__(self, parent, refid, name, **_): 778 if name in enums: 779 self.__dict__.update(enums[name].__dict__) 780 else: 781 782 enums[name] = self 783 784 self.parent = parent 785 self.refid = refid 786 self.name = name 787 self.members = [] 788 self.description = None 789 self.file_name = None 790 self.line_no = None 791 792 if parent is not None: 793 root = load_xml(parent.refid) 794 795 for compounddef in root: 796 if compounddef.attrib['id'] != parent.refid: 797 continue 798 799 for child in compounddef: 800 if child.tag != 'sectiondef': 801 continue 802 803 if child.attrib['kind'] != 'enum': 804 continue 805 806 for memberdef in child: 807 if 'id' not in memberdef.attrib: 808 continue 809 810 if memberdef.attrib['id'] == refid: 811 break 812 else: 813 continue 814 815 break 816 else: 817 continue 818 819 break 820 else: 821 return 822 # raise RuntimeError(f'not able to locate enum {name} ({refid})') 823 824 for element in memberdef: 825 if element.tag == 'location': 826 self.file_name = element.attrib['file'] 827 self.line_no = element.attrib['line'] 828 829 if element.tag == 'detaileddescription': 830 self.description = build_docstring(element) 831 elif element.tag == 'enumvalue': 832 item_name = None 833 item_description = None 834 item_file_name = None 835 item_line_no = None 836 837 for s_element in element: 838 if s_element.tag == 'name': 839 item_name = s_element.text 840 elif s_element.tag == 'detaileddescription': 841 item_description = build_docstring(s_element) 842 843 elif s_element.tag == 'location': 844 item_file_name = child.attrib['file'] 845 item_line_no = child.attrib['line'] 846 847 if item_name is not None: 848 for ev in self.members: 849 if ev.name != item_name: 850 continue 851 break 852 else: 853 ev = ENUMVALUE( 854 self, 855 element.attrib['id'], 856 item_name 857 ) 858 859 self.members.append(ev) 860 861 ev.description = item_description 862 863 if not self.description: 864 warn(MISSING_ENUM, self.name) 865 warn(None, 'FILE:', self.file_name) 866 warn(None, 'LINE:', self.line_no) 867 warn(None) 868 869 for member in self.members: 870 if not member.description: 871 warn(MISSING_ENUM_ITEM, self.name) 872 warn(None, 'MEMBER:', member.name) 873 warn(None, 'FILE:', self.file_name) 874 warn(None, 'LINE:', self.line_no) 875 warn(None) 876 877 def is_member(self, member): 878 return ( 879 member.attrib['kind'] == 'enumvalue' and 880 member.attrib['refid'].startswith(self.refid) 881 ) 882 883 def add_member(self, member): 884 name = member[0].text.strip() 885 for ev in self.members: 886 if ev.name == name: 887 return 888 889 self.members.append( 890 ENUMVALUE( 891 self, 892 member.attrib['refid'], 893 name 894 ) 895 ) 896 897 def __str__(self): 898 template = [self.template.format(name=self.name)] 899 template.extend(list(str(member) for member in self.members)) 900 901 return '\n'.join(template) 902 903 904defines = {} 905 906 907def build_define(element): 908 define = None 909 910 if element.text: 911 define = element.text.strip() 912 913 for item in element: 914 ds = build_define(item) 915 if ds: 916 if define: 917 define += ' ' + ds 918 else: 919 define = ds.strip() 920 921 if element.tail: 922 if define: 923 define += ' ' + element.tail.strip() 924 else: 925 define = element.tail.strip() 926 927 return define 928 929 930class DEFINE(object): 931 template = '''\ 932.. doxygendefine:: {name} 933 :project: lvgl 934''' 935 936 def __init__(self, parent, refid, name, **_): 937 if name in defines: 938 self.__dict__.update(defines[name].__dict__) 939 else: 940 defines[name] = self 941 942 self.parent = parent 943 self.refid = refid 944 self.name = name 945 self.description = None 946 self.file_name = None 947 self.line_no = None 948 self.params = None 949 self.initializer = None 950 951 if parent is not None: 952 root = load_xml(parent.refid) 953 954 for compounddef in root: 955 if compounddef.attrib['id'] != parent.refid: 956 continue 957 958 for child in compounddef: 959 if child.tag != 'sectiondef': 960 continue 961 962 if child.attrib['kind'] != 'define': 963 continue 964 965 for memberdef in child: 966 if memberdef.attrib['id'] == refid: 967 break 968 else: 969 continue 970 971 break 972 else: 973 continue 974 975 break 976 else: 977 return 978 979 for element in memberdef: 980 if element.tag == 'location': 981 self.file_name = element.attrib['file'] 982 self.line_no = element.attrib['line'] 983 984 elif element.tag == 'detaileddescription': 985 self.description = build_docstring(element) 986 987 elif element.tag == 'param': 988 for child in element: 989 if child.tag == 'defname': 990 if self.params is None: 991 self.params = [] 992 993 if child.text: 994 self.params.append(child.text) 995 996 elif element.tag == 'initializer': 997 initializer = build_define(element) 998 if initializer is None: 999 self.initializer = '' 1000 else: 1001 self.initializer = initializer 1002 1003 if not self.description: 1004 warn(MISSING_MACRO, self.name) 1005 warn(None, 'FILE:', self.file_name) 1006 warn(None, 'LINE:', self.line_no) 1007 warn(None) 1008 1009 def __str__(self): 1010 return self.template.format(name=self.name) 1011 1012 1013class ENUMVALUE(object): 1014 template = '''\ 1015.. doxygenenumvalue:: {name} 1016 :project: lvgl 1017''' 1018 1019 def __init__(self, parent, refid, name, **_): 1020 self.parent = parent 1021 self.refid = refid 1022 self.name = name 1023 self.description = None 1024 self.file_name = None 1025 self.line_no = None 1026 1027 def __str__(self): 1028 return self.template.format(name=self.name) 1029 1030 1031class TYPEDEF(object): 1032 template = '''\ 1033.. doxygentypedef:: {name} 1034 :project: lvgl 1035''' 1036 1037 def __init__(self, parent, refid, name, **_): 1038 if name in typedefs: 1039 self.__dict__.update(typedefs[name].__dict__) 1040 else: 1041 typedefs[name] = self 1042 1043 self.parent = parent 1044 self.refid = refid 1045 self.name = name 1046 self.type = None 1047 self._deps = None 1048 self.description = None 1049 self.file_name = None 1050 self.line_no = None 1051 1052 if parent is not None: 1053 root = load_xml(parent.refid) 1054 1055 for compounddef in root: 1056 if compounddef.attrib['id'] != parent.refid: 1057 continue 1058 1059 for child in compounddef: 1060 if child.tag != 'sectiondef': 1061 continue 1062 if child.attrib['kind'] != 'typedef': 1063 continue 1064 1065 for memberdef in child: 1066 if 'id' not in memberdef.attrib: 1067 continue 1068 1069 if memberdef.attrib['id'] == refid: 1070 break 1071 else: 1072 continue 1073 1074 break 1075 else: 1076 continue 1077 1078 break 1079 else: 1080 return 1081 1082 for element in memberdef: 1083 if element.tag == 'location': 1084 self.file_name = element.attrib['file'] 1085 self.line_no = element.attrib['line'] 1086 1087 if element.tag == 'detaileddescription': 1088 self.description = build_docstring(element) 1089 1090 if not self.description: 1091 warn(MISSING_TYPEDEF, self.name) 1092 warn(None, 'FILE:', self.file_name) 1093 warn(None, 'LINE:', self.line_no) 1094 warn(None) 1095 1096 self.type = get_type(memberdef) 1097 1098 @property 1099 def deps(self): 1100 if self._deps is None: 1101 self._deps = dict( 1102 typedefs=set(), 1103 functions=set(), 1104 enums=set(), 1105 structures=set(), 1106 unions=set(), 1107 namespaces=set(), 1108 variables=set(), 1109 ) 1110 if self.type is not None: 1111 type_ = self.type 1112 1113 if type_ in typedefs: 1114 self._deps['typedefs'].add(typedefs[type_]) 1115 elif type_ in structures: 1116 self._deps['structures'].add(structures[type_]) 1117 elif type_ in unions: 1118 self._deps['unions'].add(unions[type_]) 1119 elif type_ in enums: 1120 self._deps['enums'].add(enums[type_]) 1121 elif type_ in functions: 1122 self._deps['functions'].add(functions[type_]) 1123 elif type_ in variables: 1124 self._deps['variables'].add(variables[type_]) 1125 elif type_ in namespaces: 1126 self._deps['namespaces'].add(namespaces[type_]) 1127 1128 return self._deps 1129 1130 def __str__(self): 1131 return self.template.format(name=self.name) 1132 1133 1134classes = {} 1135 1136 1137class CLASS(object): 1138 1139 def __init__(self, _, refid, name, node, **__): 1140 if name in classes: 1141 self.__dict__.update(classes[name].__dict__) 1142 return 1143 1144 classes[name] = self 1145 1146 self.refid = refid 1147 self.name = name 1148 1149 enums_ = [] 1150 1151 for member in node: 1152 if member.tag != 'member': 1153 continue 1154 1155 cls = globals()[member.attrib['kind'].upper()] 1156 if cls == ENUM: 1157 member.attrib['name'] = member[0].text.strip() 1158 enums_.append(cls(self, **member.attrib)) 1159 elif cls == ENUMVALUE: 1160 if enums_[-1].is_member(member): 1161 enums_[-1].add_member(member) 1162 1163 else: 1164 member.attrib['name'] = member[0].text.strip() 1165 cls(self, **member.attrib) 1166 1167 1168lvgl_src_path = '' 1169api_path = '' 1170html_files = {} 1171 1172 1173def iter_src(n, p): 1174 if p: 1175 out_path = os.path.join(api_path, p) 1176 else: 1177 out_path = api_path 1178 1179 index_file = None 1180 1181 if p: 1182 src_path = os.path.join(lvgl_src_path, p) 1183 else: 1184 src_path = lvgl_src_path 1185 1186 folders = [] 1187 1188 for file in os.listdir(src_path): 1189 if 'private' in file: 1190 continue 1191 1192 if os.path.isdir(os.path.join(src_path, file)): 1193 folders.append((file, os.path.join(p, file))) 1194 continue 1195 1196 if not file.endswith('.h'): 1197 continue 1198 1199 if not os.path.exists(out_path): 1200 os.makedirs(out_path) 1201 1202 if index_file is None: 1203 index_file = open(os.path.join(out_path, 'index.rst'), 'w') 1204 if n: 1205 index_file.write('=' * len(n)) 1206 index_file.write('\n' + n + '\n') 1207 index_file.write('=' * len(n)) 1208 index_file.write('\n\n\n') 1209 1210 index_file.write('.. toctree::\n :maxdepth: 2\n\n') 1211 1212 name = os.path.splitext(file)[0] 1213 index_file.write(' ' + name + '\n') 1214 1215 rst_file = os.path.join(out_path, name + '.rst') 1216 html_file = os.path.join(p, name + '.html') 1217 html_files[name] = html_file 1218 1219 with open(rst_file, 'w') as f: 1220 f.write('.. _{0}_h:'.format(name)) 1221 f.write('\n\n') 1222 f.write('=' * len(file)) 1223 f.write('\n') 1224 f.write(file) 1225 f.write('\n') 1226 f.write('=' * len(file)) 1227 f.write('\n\n\n') 1228 1229 f.write('.. doxygenfile:: ' + file) 1230 f.write('\n') 1231 f.write(' :project: lvgl') 1232 f.write('\n\n') 1233 1234 for name, folder in folders: 1235 if iter_src(name, folder): 1236 if index_file is None: 1237 index_file = open(os.path.join(out_path, 'index.rst'), 'w') 1238 1239 if n: 1240 index_file.write('=' * len(n)) 1241 index_file.write('\n' + n + '\n') 1242 index_file.write('=' * len(n)) 1243 index_file.write('\n\n\n') 1244 1245 index_file.write('.. toctree::\n :maxdepth: 2\n\n') 1246 1247 index_file.write(' ' + os.path.split(folder)[-1] + '/index\n') 1248 1249 if index_file is not None: 1250 index_file.write('\n') 1251 index_file.close() 1252 return True 1253 1254 return False 1255 1256 1257def clean_name(nme): 1258 # Handle error: 1259 # AttributeError: 'NoneType' object has no attribute 'startswith' 1260 if nme is None: 1261 return nme 1262 1263 if nme.startswith('_lv_'): 1264 nme = nme[4:] 1265 elif nme.startswith('lv_'): 1266 nme = nme[3:] 1267 1268 if nme.endswith('_t'): 1269 nme = nme[:-2] 1270 1271 return nme 1272 1273 1274# Definitions: 1275# - "section" => The name "abc_def" has 2 sections. 1276# - N = number of sections in `item_name`. 1277# After removing leading '_lv_', 'lv_' and trailing '_t' from `obj_name`, 1278# do the remaining first N "sections" of `obj_name` match `item_name` 1279# (case sensitive)? 1280def is_name_match(item_name, obj_name): 1281 # Handle error: 1282 # AttributeError: 'NoneType' object has no attribute 'split' 1283 if obj_name is None: 1284 return False 1285 1286 u_num = item_name.count('_') + 1 1287 1288 obj_name = obj_name.split('_') 1289 1290 # Reject (False) if `obj_name` doesn't have as many sections as `item_name`. 1291 if len(obj_name) < u_num: 1292 return False 1293 1294 obj_name = '_'.join(obj_name[:u_num]) 1295 1296 return item_name == obj_name 1297 1298 1299def get_includes(name1, name2, obj, includes): 1300 name2 = clean_name(name2) 1301 1302 if not is_name_match(name1, name2): 1303 return 1304 1305 if obj.parent is not None and hasattr(obj.parent, 'header_file'): 1306 header_file = obj.parent.header_file 1307 elif hasattr(obj, 'header_file'): 1308 header_file = obj.header_file 1309 else: 1310 return 1311 1312 if not header_file: 1313 return 1314 1315 if header_file not in html_files: 1316 return 1317 1318 includes.add((header_file, html_files[header_file])) 1319 1320 1321class XMLSearch(object): 1322 1323 def __init__(self, temp_directory): 1324 global xml_path 1325 import subprocess 1326 import re 1327 import sys 1328 1329 bp = os.path.abspath(os.path.dirname(__file__)) 1330 1331 lvgl_path = os.path.join(temp_directory, 'lvgl') 1332 src_path = os.path.join(lvgl_path, 'src') 1333 1334 doxy_path = os.path.join(bp, 'Doxyfile') 1335 1336 with open(doxy_path, 'rb') as f: 1337 data = f.read().decode('utf-8') 1338 1339 data = data.replace( 1340 '#*#*LV_CONF_PATH*#*#', 1341 os.path.join(temp_directory, 'lv_conf.h') 1342 ) 1343 data = data.replace('*#*#SRC#*#*', '"{0}"'.format(src_path)) 1344 1345 with open(os.path.join(temp_directory, 'Doxyfile'), 'wb') as f: 1346 f.write(data.encode('utf-8')) 1347 1348 # ----------------------------------------------------------------- 1349 # Populate LVGL_URLPATH and LVGL_GITCOMMIT environment variables: 1350 # - LVGL_URLPATH <= 'master' or '8.4' '9.2' etc. 1351 # - LVGL_GITCOMMIT <= commit hash of HEAD. 1352 # The previous version of this was populating LVGL_URLPATH with 1353 # the multi-line list of all existing branches in the repository, 1354 # which was not what was intended. 1355 # ----------------------------------------------------------------- 1356 status, branch = subprocess.getstatusoutput("git branch --show-current") 1357 _, gitcommit = subprocess.getstatusoutput("git rev-parse HEAD") 1358 1359 # If above failed (i.e. `branch` not valid), default to 'master'. 1360 if status != 0: 1361 branch = 'master' 1362 elif branch == 'master': 1363 # Expected in most cases. Nothing to change. 1364 pass 1365 else: 1366 # `branch` is valid. Capture release version if in a 'release/' branch. 1367 if branch.startswith('release/'): 1368 branch = branch[8:] 1369 else: 1370 # Default to 'master'. 1371 branch = 'master' 1372 1373 os.environ['LVGL_URLPATH'] = branch 1374 os.environ['LVGL_GITCOMMIT'] = gitcommit 1375 1376 # --------------------------------------------------------------------- 1377 # Provide a way to run an external command and abort build on error. 1378 # 1379 # This is necessary because when tempdir created by tempfile.mkdtemp()` 1380 # is on a different drive, the "cd tmpdir && doxygen Doxyfile" syntax 1381 # fails because of the different semantics of the `cd` command on 1382 # Windows: it doesn't change the default DRIVE if `cd` is executed 1383 # from a different drive. The result, when this is the case, is that 1384 # Doxygen runs in the current working directory instead of in the 1385 # temporary directory as was intended. 1386 # --------------------------------------------------------------------- 1387 def cmd(cmd_str, start_dir=None): 1388 if start_dir is None: 1389 start_dir = os.getcwd() 1390 1391 saved_dir = os.getcwd() 1392 os.chdir(start_dir) 1393 1394 # This method of running Doxygen is used because if it 1395 # succeeds, we do not want anything going to STDOUT. 1396 # Running it via `os.system()` would send its output 1397 # to STDOUT. 1398 p = subprocess.Popen( 1399 cmd_str, 1400 stdout=subprocess.PIPE, 1401 stderr=subprocess.PIPE, 1402 shell=True 1403 ) 1404 1405 out, err = p.communicate() 1406 if p.returncode: 1407 if out: 1408 # Note the `.decode("utf-8")` is required here 1409 # because `sys.stdout.write()` requires a string, 1410 # and `out` by itself is a byte array -- it causes 1411 # it to generate an exception and abort the script. 1412 sys.stdout.write(out.decode("utf-8")) 1413 sys.stdout.flush() 1414 if err: 1415 sys.stderr.write(err.decode("utf-8")) 1416 sys.stdout.flush() 1417 1418 sys.exit(p.returncode) 1419 1420 # If execution arrived here, Doxygen exited with code 0. 1421 os.chdir(saved_dir) 1422 1423 # ----------------------------------------------------------------- 1424 # Run Doxygen in temporary directory. 1425 # ----------------------------------------------------------------- 1426 cmd('doxygen Doxyfile', temp_directory) 1427 1428 xml_path = os.path.join(temp_directory, 'xml') 1429 1430 self.index = load_xml('index') 1431 1432 for compound in self.index: 1433 compound.attrib['name'] = compound[0].text.strip() 1434 if compound.attrib['kind'] in ('example', 'page', 'dir'): 1435 continue 1436 1437 globals()[compound.attrib['kind'].upper()]( 1438 None, 1439 node=compound, 1440 **compound.attrib 1441 ) 1442 1443 def get_macros(self): 1444 return list(defines.values()) 1445 1446 def get_enum_item(self, e_name): 1447 for enum, obj in enums.items(): 1448 for enum_item in obj.members: 1449 if enum_item.name == e_name: 1450 return enum_item 1451 1452 def get_enum(self, e_name): 1453 return enums.get(e_name, None) 1454 1455 def get_function(self, f_name): 1456 return functions.get(f_name, None) 1457 1458 def get_variable(self, v_name): 1459 return variables.get(v_name, None) 1460 1461 def get_union(self, u_name): 1462 return unions.get(u_name, None) 1463 1464 def get_structure(self, s_name): 1465 return structures.get(s_name, None) 1466 1467 def get_typedef(self, t_name): 1468 return typedefs.get(t_name, None) 1469 1470 def get_macro(self, m_name): 1471 return defines.get(m_name, None) 1472 1473 1474def announce(*args): 1475 args = ' '.join(repr(arg) for arg in args) 1476 print(f'{os.path.basename(__file__)}: ', args) 1477 1478 1479def run(project_path, temp_directory, *doc_paths): 1480 """ 1481 This function does 2 things: 1482 1. Generates .RST files for the LVGL header files that will have API 1483 pages generated for them. It places these in <tmp_dir>/API/... 1484 following the <project_path>/src/ directory structure. 1485 2. Add Sphinx hyperlinks to the end of source .RST files found 1486 in the `doc_paths` array directories, whose file-name stems 1487 match code-element names found by Doxygen. 1488 1489 :param project_path: platform-appropriate path to LVGL root directory 1490 :param temp_directory: platform-appropriate path to temp dir being operated on 1491 :param doc_paths: list of platform-appropriate paths to find source .RST files. 1492 :return: n/a 1493 """ 1494 global base_path 1495 global xml_path 1496 global lvgl_src_path 1497 global api_path 1498 1499 base_path = temp_directory 1500 xml_path = os.path.join(base_path, 'xml') 1501 api_path = os.path.join(base_path, 'API') 1502 lvgl_src_path = os.path.join(project_path, 'src') 1503 1504 announce("Generating API documentation .RST files...") 1505 1506 if not os.path.exists(api_path): 1507 os.makedirs(api_path) 1508 1509 # Generate .RST files for API pages. 1510 iter_src('API', '') 1511 # Load index.xml -- core of what was generated by Doxygen. 1512 index = load_xml('index') 1513 1514 # Populate these dictionaries. 1515 # Keys : C-code-element names (str) found by Doxygen. 1516 # Values: The <compound> XML-node created by `xml.etree::ElementTree` in `load_xml()` above. 1517 # 1518 # - defines, 1519 # - enums, 1520 # - variables, 1521 # - namespaces, 1522 # - structures, 1523 # - unions, 1524 # - typedefs, 1525 # - functions. 1526 announce("Building source-code symbol tables...") 1527 1528 for compound in index: 1529 compound.attrib['name'] = compound[0].text.strip() 1530 if compound.attrib['kind'] in ('example', 'page', 'dir'): 1531 continue 1532 1533 # This below highly-compressed command effectively does this: 1534 # 1535 # namespace_dict = globals() 1536 # compound_elem_kind_upper = compound.attrib['kind'].upper() 1537 # e.g. 'FUNCTION' 1538 # class_obj = namespace_dict['FUNCTION'] 1539 # # In each case of `class_obj`, the __init__ args are: 1540 # # (self, parent, refid, name, **_) 1541 # # So we get... 1542 # attrib_keyword_args = **compound.attrib 1543 # # Passing (**compound.attrib) as an argument creates and 1544 # # passes a set of keyword arguments produced from the 1545 # # dictionary `compound.attrib`. 1546 # new_obj = class_obj(None, node=compound, attrib_keyword_args) 1547 # 1548 # Note carefully that `new_obj` gets thrown away, but the new object created 1549 # doesn't go away because during its execution of __init__(), the new object 1550 # adds itself to the global dictionary matching its "kind": 1551 # 1552 # Class Dictionary New Object Adds Itself To 1553 # ------------ | ------------------------------------ 1554 # - DEFINE => `defines` 1555 # - ENUM => `enums` 1556 # - VARIABLE => `variables` 1557 # - NAMESPACE => `namespaces` 1558 # - STRUCT => `structures` 1559 # - UNION appears to have a different purpose 1560 # - TYPEDEF => `typedefs` 1561 # - FUNCTION => `functions` 1562 # - GROUP => `groups` 1563 # - FILE => `files` 1564 # - CLASS => `classes` 1565 # 1566 # Populating these dictionaries takes quite a while: 1567 # ~18-seconds on a medium-speed system. 1568 globals()[compound.attrib['kind'].upper()]( 1569 None, 1570 node=compound, 1571 **compound.attrib 1572 ) 1573 1574 # For each directory entry in `doc_paths` array... 1575 announce("Adding API-page hyperlinks to source docs...") 1576 1577 for folder in doc_paths: 1578 # Fetch a list of '.rst' files excluding 'index.rst'. 1579 rst_files = list( 1580 (os.path.splitext(item)[0], os.path.join(folder, item)) 1581 for item in os.listdir(folder) 1582 if item.endswith('.rst') and 'index.rst' not in item 1583 ) 1584 1585 # For each .RST file in that directory... 1586 for stem, path in rst_files: 1587 # Start with an empty set. 1588 html_includes = set() 1589 1590 # Build `html_includes` set as a list of tuples containing 1591 # (name, html_file). Example: "draw.rst" has `stem` == 'draw', 1592 # and generates a list of tuples from .H files where matching 1593 # C-code-element names were found. Example: 1594 # {('lv_draw_line', 'draw\\lv_draw_line.html'), 1595 # ('lv_draw_sdl', 'draw\\sdl\\lv_draw_sdl.html'), 1596 # ('lv_draw_sw_blend_to_i1', 'draw\\sw\\blend\\lv_draw_sw_blend_to_i1.html'), 1597 # etc.} 1598 for container in ( 1599 defines, 1600 enums, 1601 variables, 1602 namespaces, 1603 structures, 1604 unions, 1605 typedefs, 1606 functions 1607 ): 1608 for n, o in container.items(): 1609 get_includes(stem, n, o, html_includes) 1610 1611 if html_includes: 1612 # Convert `html_includes` set to a list of strings containing the 1613 # Sphinx hyperlink syntax "link references". Example from above: 1614 # [':ref:`lv_draw_line_h`\n', 1615 # ':ref:`lv_draw_sdl_h`\n', 1616 # ':ref:`lv_draw_sw_blend_to_i1_h`\n', 1617 # etc.] 1618 html_includes = list( 1619 ':ref:`{0}_h`\n'.format(inc) 1620 for inc, _ in html_includes 1621 ) 1622 1623 # Convert that list to a single string of Sphinx hyperlink 1624 # references with blank lines between them. 1625 # :ref:`lv_draw_line_h` 1626 # 1627 # :ref:`lv_draw_sdl_h` 1628 # 1629 # :ref:`lv_draw_sw_blend_to_i1_h` 1630 # 1631 # etc. 1632 output = ('\n'.join(html_includes)) + '\n' 1633 1634 # Append that string to the source .RST file being processed. 1635 with open(path, 'rb') as f: 1636 try: 1637 data = f.read().decode('utf-8') 1638 except UnicodeDecodeError: 1639 print(path) 1640 raise 1641 1642 data = data.split('.. Autogenerated', 1)[0] 1643 1644 data += '.. Autogenerated\n\n' 1645 data += output 1646 1647 with open(path, 'wb') as f: 1648 f.write(data.encode('utf-8')) 1649