]> git.ipfire.org Git - thirdparty/systemd.git/blame - tools/update-dbus-docs.py
tools: make update-dbus-docs compatible with Python 3.7
[thirdparty/systemd.git] / tools / update-dbus-docs.py
CommitLineData
e5dd26cc 1#!/usr/bin/env python3
db9ecf05 2# SPDX-License-Identifier: LGPL-2.1-or-later
e5dd26cc 3
0f5cea02 4import argparse
e5dd26cc
ZJS
5import collections
6import sys
c351d568 7import os
e5dd26cc
ZJS
8import subprocess
9import io
e5dd26cc 10
8aaf611b
ZJS
11try:
12 from lxml import etree
13except ModuleNotFoundError as e:
14 etree = e
e5dd26cc 15
198fda4f
ZJS
16try:
17 from shlex import join as shlex_join
18except ImportError as e:
19 shlex_join = e
20
668b3a42
LB
21try:
22 from shlex import quote as shlex_quote
23except ImportError as e:
24 shlex_quote = e
25
e5dd26cc
ZJS
26class NoCommand(Exception):
27 pass
28
e5dd26cc
ZJS
29BORING_INTERFACES = [
30 'org.freedesktop.DBus.Peer',
31 'org.freedesktop.DBus.Introspectable',
32 'org.freedesktop.DBus.Properties',
33]
34
8aaf611b
ZJS
35def xml_parser():
36 return etree.XMLParser(no_network=True,
37 remove_comments=False,
38 strip_cdata=False,
39 resolve_entities=False)
40
e5dd26cc
ZJS
41def print_method(declarations, elem, *, prefix, file, is_signal=False):
42 name = elem.get('name')
43 klass = 'signal' if is_signal else 'method'
44 declarations[klass].append(name)
45
46 print(f'''{prefix}{name}(''', file=file, end='')
47 lead = ',\n' + prefix + ' ' * len(name) + ' '
48
49 for num, arg in enumerate(elem.findall('./arg')):
50 argname = arg.get('name')
51
52 if argname is None:
04aa6fa8 53 if opts.print_errors:
e5dd26cc
ZJS
54 print(f'method {name}: argument {num+1} has no name', file=sys.stderr)
55 argname = 'UNNAMED'
56
57 type = arg.get('type')
58 if not is_signal:
59 direction = arg.get('direction')
60 print(f'''{lead if num > 0 else ''}{direction:3} {type} {argname}''', file=file, end='')
61 else:
62 print(f'''{lead if num > 0 else ''}{type} {argname}''', file=file, end='')
63
64 print(f');', file=file)
65
66ACCESS_MAP = {
67 'read' : 'readonly',
68 'write' : 'readwrite',
69}
70
71def value_ellipsis(type):
72 if type == 's':
73 return "'...'";
74 if type[0] == 'a':
75 inner = value_ellipsis(type[1:])
76 return f"[{inner}{', ...' if inner != '...' else ''}]";
77 return '...'
78
79def print_property(declarations, elem, *, prefix, file):
80 name = elem.get('name')
81 type = elem.get('type')
82 access = elem.get('access')
83
84 declarations['property'].append(name)
85
86 # @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
87 # @org.freedesktop.systemd1.Privileged("true")
88 # readwrite b EnableWallMessages = false;
89
90 for anno in elem.findall('./annotation'):
91 anno_name = anno.get('name')
92 anno_value = anno.get('value')
93 print(f'''{prefix}@{anno_name}("{anno_value}")''', file=file)
94
95 access = ACCESS_MAP.get(access, access)
96 print(f'''{prefix}{access} {type} {name} = {value_ellipsis(type)};''', file=file)
97
08fe1b6c 98def print_interface(iface, *, prefix, file, print_boring, only_interface, declarations):
e5dd26cc
ZJS
99 name = iface.get('name')
100
08fe1b6c
ZJS
101 is_boring = (name in BORING_INTERFACES or
102 only_interface is not None and name != only_interface)
103
e5dd26cc
ZJS
104 if is_boring and print_boring:
105 print(f'''{prefix}interface {name} {{ ... }};''', file=file)
08fe1b6c 106
e5dd26cc
ZJS
107 elif not is_boring and not print_boring:
108 print(f'''{prefix}interface {name} {{''', file=file)
109 prefix2 = prefix + ' '
110
111 for num, elem in enumerate(iface.findall('./method')):
112 if num == 0:
113 print(f'''{prefix2}methods:''', file=file)
114 print_method(declarations, elem, prefix=prefix2 + ' ', file=file)
115
116 for num, elem in enumerate(iface.findall('./signal')):
117 if num == 0:
118 print(f'''{prefix2}signals:''', file=file)
119 print_method(declarations, elem, prefix=prefix2 + ' ', file=file, is_signal=True)
120
121 for num, elem in enumerate(iface.findall('./property')):
122 if num == 0:
123 print(f'''{prefix2}properties:''', file=file)
124 print_property(declarations, elem, prefix=prefix2 + ' ', file=file)
125
126 print(f'''{prefix}}};''', file=file)
127
128def document_has_elem_with_text(document, elem, item_repr):
129 predicate = f".//{elem}" # [text() = 'foo'] doesn't seem supported :(
130 for loc in document.findall(predicate):
131 if loc.text == item_repr:
132 return True
45752a24 133 return False
e5dd26cc 134
af4c7dc2 135def check_documented(document, declarations, stats):
e5dd26cc
ZJS
136 missing = []
137 for klass, items in declarations.items():
af4c7dc2
ZJS
138 stats['total'] += len(items)
139
e5dd26cc
ZJS
140 for item in items:
141 if klass == 'method':
142 elem = 'function'
143 item_repr = f'{item}()'
144 elif klass == 'signal':
145 elem = 'function'
146 item_repr = item
147 elif klass == 'property':
148 elem = 'varname'
149 item_repr = item
150 else:
151 assert False, (klass, item)
152
153 if not document_has_elem_with_text(document, elem, item_repr):
04aa6fa8 154 if opts.print_errors:
e5dd26cc
ZJS
155 print(f'{klass} {item} is not documented :(')
156 missing.append((klass, item))
157
af4c7dc2
ZJS
158 stats['missing'] += len(missing)
159
e5dd26cc
ZJS
160 return missing
161
08fe1b6c 162def xml_to_text(destination, xml, *, only_interface=None):
e5dd26cc
ZJS
163 file = io.StringIO()
164
165 declarations = collections.defaultdict(list)
f92c8d1c 166 interfaces = []
e5dd26cc
ZJS
167
168 print(f'''node {destination} {{''', file=file)
169
170 for print_boring in [False, True]:
171 for iface in xml.findall('./interface'):
172 print_interface(iface, prefix=' ', file=file,
173 print_boring=print_boring,
08fe1b6c 174 only_interface=only_interface,
e5dd26cc 175 declarations=declarations)
f92c8d1c
JR
176 name = iface.get('name')
177 if not name in BORING_INTERFACES:
178 interfaces.append(name)
e5dd26cc
ZJS
179
180 print(f'''}};''', file=file)
181
f92c8d1c 182 return file.getvalue(), declarations, interfaces
e5dd26cc 183
af4c7dc2 184def subst_output(document, programlisting, stats):
c351d568
ZJS
185 executable = programlisting.get('executable', None)
186 if executable is None:
187 # Not our thing
e5dd26cc 188 return
c351d568
ZJS
189 executable = programlisting.get('executable')
190 node = programlisting.get('node')
191 interface = programlisting.get('interface')
e5dd26cc 192
0f5cea02 193 argv = [f'{opts.build_dir}/{executable}', f'--bus-introspect={interface}']
668b3a42
LB
194 if isinstance(shlex_join, Exception):
195 print(f'COMMAND: {" ".join(shlex_quote(arg) for arg in argv)}')
196 else:
197 print(f'COMMAND: {shlex_join(argv)}')
e5dd26cc 198
e5dd26cc
ZJS
199 try:
200 out = subprocess.check_output(argv, text=True)
c351d568
ZJS
201 except FileNotFoundError:
202 print(f'{executable} not found, ignoring', file=sys.stderr)
e5dd26cc
ZJS
203 return
204
8aaf611b 205 xml = etree.fromstring(out, parser=xml_parser())
e5dd26cc 206
c351d568
ZJS
207 new_text, declarations, interfaces = xml_to_text(node, xml, only_interface=interface)
208 programlisting.text = '\n' + new_text + ' '
e5dd26cc
ZJS
209
210 if declarations:
af4c7dc2 211 missing = check_documented(document, declarations, stats)
e5dd26cc
ZJS
212 parent = programlisting.getparent()
213
214 # delete old comments
215 for child in parent:
f92c8d1c
JR
216 if (child.tag == etree.Comment
217 and 'Autogenerated' in child.text):
218 parent.remove(child)
e5dd26cc
ZJS
219 if (child.tag == etree.Comment
220 and 'not documented' in child.text):
221 parent.remove(child)
f92c8d1c
JR
222 if (child.tag == "variablelist"
223 and child.attrib.get("generated",False) == "True"):
224 parent.remove(child)
225
226 # insert pointer for systemd-directives generation
227 the_tail = programlisting.tail #tail is erased by addnext, so save it here.
228 prev_element = etree.Comment("Autogenerated cross-references for systemd.directives, do not edit")
229 programlisting.addnext(prev_element)
230 programlisting.tail = the_tail
231
232 for interface in interfaces:
233 variablelist = etree.Element("variablelist")
234 variablelist.attrib['class'] = 'dbus-interface'
235 variablelist.attrib['generated'] = 'True'
236 variablelist.attrib['extra-ref'] = interface
237
238 prev_element.addnext(variablelist)
239 prev_element.tail = the_tail
240 prev_element = variablelist
241
242 for decl_type,decl_list in declarations.items():
243 for declaration in decl_list:
244 variablelist = etree.Element("variablelist")
245 variablelist.attrib['class'] = 'dbus-'+decl_type
246 variablelist.attrib['generated'] = 'True'
247 if decl_type == 'method' :
248 variablelist.attrib['extra-ref'] = declaration + '()'
249 else:
250 variablelist.attrib['extra-ref'] = declaration
251
252 prev_element.addnext(variablelist)
253 prev_element.tail = the_tail
254 prev_element = variablelist
255
256 last_element = etree.Comment("End of Autogenerated section")
257 prev_element.addnext(last_element)
258 prev_element.tail = the_tail
259 last_element.tail = the_tail
e5dd26cc
ZJS
260
261 # insert comments for undocumented items
262 for item in reversed(missing):
263 comment = etree.Comment(f'{item[0]} {item[1]} is not documented!')
264 comment.tail = programlisting.tail
265 parent.insert(parent.index(programlisting) + 1, comment)
266
267def process(page):
268 src = open(page).read()
8aaf611b 269 xml = etree.fromstring(src, parser=xml_parser())
e5dd26cc
ZJS
270
271 # print('parsing {}'.format(name), file=sys.stderr)
272 if xml.tag != 'refentry':
273 return
274
af4c7dc2
ZJS
275 stats = collections.Counter()
276
e5dd26cc
ZJS
277 pls = xml.findall('.//programlisting')
278 for pl in pls:
af4c7dc2 279 subst_output(xml, pl, stats)
e5dd26cc
ZJS
280
281 out_text = etree.tostring(xml, encoding='unicode')
86b52a39 282 # massage format to avoid some lxml whitespace handling idiosyncrasies
e5dd26cc
ZJS
283 # https://bugs.launchpad.net/lxml/+bug/526799
284 out_text = (src[:src.find('<refentryinfo')] +
285 out_text[out_text.find('<refentryinfo'):] +
286 '\n')
287
1b584f38
ZJS
288 if not opts.test:
289 with open(page, 'w') as out:
290 out.write(out_text)
e5dd26cc 291
1b584f38 292 return dict(stats=stats, outdated=(out_text != src))
af4c7dc2 293
0f5cea02
ZJS
294def parse_args():
295 p = argparse.ArgumentParser()
1b584f38
ZJS
296 p.add_argument('--test', action='store_true',
297 help='only verify that everything is up2date')
0f5cea02
ZJS
298 p.add_argument('--build-dir', default='build')
299 p.add_argument('pages', nargs='+')
04aa6fa8
ZJS
300 opts = p.parse_args()
301 opts.print_errors = not opts.test
302 return opts
e5dd26cc 303
0f5cea02
ZJS
304if __name__ == '__main__':
305 opts = parse_args()
c351d568 306
668b3a42 307 for item in (etree, shlex_quote):
198fda4f
ZJS
308 if isinstance(item, Exception):
309 print(item, file=sys.stderr)
310 exit(77 if opts.test else 1)
8aaf611b 311
0f5cea02
ZJS
312 if not os.path.exists(f'{opts.build_dir}/systemd'):
313 exit(f"{opts.build_dir}/systemd doesn't exist. Use --build-dir=.")
c351d568 314
0f5cea02 315 stats = {page.split('/')[-1] : process(page) for page in opts.pages}
af4c7dc2
ZJS
316
317 # Let's print all statistics at the end
318 mlen = max(len(page) for page in stats)
668b3a42 319 total = sum((item['stats'] for item in stats.values()), collections.Counter())
1b584f38
ZJS
320 total = 'total', dict(stats=total, outdated=False)
321 outdated = []
322 for page, info in sorted(stats.items()) + [total]:
323 m = info['stats']['missing']
324 t = info['stats']['total']
af4c7dc2 325 p = page + ':'
1b584f38
ZJS
326 c = 'OUTDATED' if info['outdated'] else ''
327 if c:
328 outdated.append(page)
329 print(f'{p:{mlen + 1}} {t - m}/{t} {c}')
330
331 if opts.test and outdated:
c91e3116 332 exit(f'Outdated pages: {", ".join(outdated)}\n'
4c890ad3 333 f'Hint: ninja -C {opts.build_dir} update-dbus-docs')