|
| 1 | +""" |
| 2 | +The aim of this directive is to document inline which classes of the LKQL |
| 3 | +hierarchy are already documented, in reference_manual.rst, and get a report of |
| 4 | +which classes remain to be documented. |
| 5 | +""" |
| 6 | + |
| 7 | + |
| 8 | +from collections import defaultdict |
| 9 | +from docutils import nodes |
| 10 | +from functools import lru_cache |
| 11 | + |
| 12 | +from sphinx.util.docutils import SphinxDirective |
| 13 | +from sphinx.errors import ExtensionError |
| 14 | + |
| 15 | +import liblkqllang |
| 16 | +import traceback |
| 17 | + |
| 18 | +lkql_classes = [ |
| 19 | + v for _, v in liblkqllang.__dict__.items() |
| 20 | + if (type(v) == type |
| 21 | + and issubclass(v, liblkqllang.LKQLNode)) |
| 22 | +] |
| 23 | + |
| 24 | + |
| 25 | +@lru_cache |
| 26 | +def lkql_cls_subclasses(): |
| 27 | + """ |
| 28 | + Return a dict of class to direct subclasses. |
| 29 | + """ |
| 30 | + res = defaultdict(list) |
| 31 | + for cls in lkql_classes: |
| 32 | + if cls != liblkqllang.LKQLNode: |
| 33 | + res[cls.__base__].append(cls) |
| 34 | + return res |
| 35 | + |
| 36 | + |
| 37 | +@lru_cache(maxsize=None) |
| 38 | +def is_class_documented(lkql_class): |
| 39 | + """ |
| 40 | + Helper function: whether a class is documented, taking inheritance into |
| 41 | + account (if all subclasses of a class are documented, then the class is |
| 42 | + documented). |
| 43 | + """ |
| 44 | + subclasses = lkql_cls_subclasses()[lkql_class] |
| 45 | + return ( |
| 46 | + getattr(lkql_class, "documented", False) |
| 47 | + or (len(subclasses) > 0 |
| 48 | + and all(is_class_documented(subcls) |
| 49 | + for subcls in lkql_cls_subclasses()[lkql_class])) |
| 50 | + ) |
| 51 | + |
| 52 | + |
| 53 | +class LKQLDocClassDirective(SphinxDirective): |
| 54 | + """ |
| 55 | + Directive to be used to annotate documentation of an LKQL node. |
| 56 | + """ |
| 57 | + |
| 58 | + has_content = False |
| 59 | + required_arguments = 1 |
| 60 | + optional_arguments = 0 |
| 61 | + |
| 62 | + def run(self): |
| 63 | + targetid = 'lkqldocclass-%d' % self.env.new_serialno('lkqldocclass') |
| 64 | + targetnode = nodes.target('', '', ids=[targetid]) |
| 65 | + |
| 66 | + cls_name = self.arguments[0] |
| 67 | + |
| 68 | + if not hasattr(self.env, 'documented_classes'): |
| 69 | + self.env.documented_classes = [] |
| 70 | + |
| 71 | + try: |
| 72 | + lkql_class = getattr(liblkqllang, cls_name) |
| 73 | + self.env.documented_classes.append(lkql_class) |
| 74 | + lkql_class.documented = True |
| 75 | + except AttributeError as e: |
| 76 | + raise ExtensionError(f"LKQL class not found: {cls_name}", e) |
| 77 | + |
| 78 | + return [] |
| 79 | + |
| 80 | + |
| 81 | +def process_lkql_classes_coverage(app, doctree, fromdocname): |
| 82 | + """ |
| 83 | + Process the coverage of lkql classes in documentation. This will print |
| 84 | + warnings for every non-documented class. |
| 85 | + """ |
| 86 | + try: |
| 87 | + for cls in lkql_classes: |
| 88 | + if issubclass(cls, liblkqllang.LKQLNodeBaseList): |
| 89 | + continue |
| 90 | + if not is_class_documented(cls): |
| 91 | + print(f"Class not documented: {cls}") |
| 92 | + except Exception as e: |
| 93 | + traceback.print_exception(type(e), e, e.__traceback__) |
| 94 | + |
| 95 | + |
| 96 | +def setup(app): |
| 97 | + app.add_directive('lkql_doc_class', LKQLDocClassDirective) |
| 98 | + app.connect('doctree-resolved', process_lkql_classes_coverage) |
| 99 | + |
| 100 | + return { |
| 101 | + 'version': '0.1', |
| 102 | + 'parallel_read_safe': True, |
| 103 | + 'parallel_write_safe': True, |
| 104 | + } |
0 commit comments