Skip to content

Commit 7084650

Browse files
committed
SB05-065: Add lkql_doc_class sphinx directive
1 parent cd02068 commit 7084650

File tree

1 file changed

+104
-0
lines changed

1 file changed

+104
-0
lines changed

user_manual/source/lkql_doc_class.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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

Comments
 (0)