]> Git Repo - qemu.git/blob - docs/sphinx/qapidoc.py
works with less than base ISA qemu-system-riscv32 -M virt -bios none -kernel output...
[qemu.git] / docs / sphinx / qapidoc.py
1 # coding=utf-8
2 #
3 # QEMU qapidoc QAPI file parsing extension
4 #
5 # Copyright (c) 2020 Linaro
6 #
7 # This work is licensed under the terms of the GNU GPLv2 or later.
8 # See the COPYING file in the top-level directory.
9
10 """
11 qapidoc is a Sphinx extension that implements the qapi-doc directive
12
13 The purpose of this extension is to read the documentation comments
14 in QAPI schema files, and insert them all into the current document.
15
16 It implements one new rST directive, "qapi-doc::".
17 Each qapi-doc:: directive takes one argument, which is the
18 pathname of the schema file to process, relative to the source tree.
19
20 The docs/conf.py file must set the qapidoc_srctree config value to
21 the root of the QEMU source tree.
22
23 The Sphinx documentation on writing extensions is at:
24 https://www.sphinx-doc.org/en/master/development/index.html
25 """
26
27 import os
28 import re
29
30 from docutils import nodes
31 from docutils.statemachine import ViewList
32 from docutils.parsers.rst import directives, Directive
33 from sphinx.errors import ExtensionError
34 from sphinx.util.nodes import nested_parse_with_titles
35 import sphinx
36 from qapi.gen import QAPISchemaVisitor
37 from qapi.error import QAPIError, QAPISemError
38 from qapi.schema import QAPISchema
39
40
41 # Sphinx up to 1.6 uses AutodocReporter; 1.7 and later
42 # use switch_source_input. Check borrowed from kerneldoc.py.
43 Use_SSI = sphinx.__version__[:3] >= '1.7'
44 if Use_SSI:
45     from sphinx.util.docutils import switch_source_input
46 else:
47     from sphinx.ext.autodoc import AutodocReporter
48
49
50 __version__ = '1.0'
51
52
53 # Function borrowed from pydash, which is under the MIT license
54 def intersperse(iterable, separator):
55     """Yield the members of *iterable* interspersed with *separator*."""
56     iterable = iter(iterable)
57     yield next(iterable)
58     for item in iterable:
59         yield separator
60         yield item
61
62
63 class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
64     """A QAPI schema visitor which generates docutils/Sphinx nodes
65
66     This class builds up a tree of docutils/Sphinx nodes corresponding
67     to documentation for the various QAPI objects. To use it, first
68     create a QAPISchemaGenRSTVisitor object, and call its
69     visit_begin() method.  Then you can call one of the two methods
70     'freeform' (to add documentation for a freeform documentation
71     chunk) or 'symbol' (to add documentation for a QAPI symbol). These
72     will cause the visitor to build up the tree of document
73     nodes. Once you've added all the documentation via 'freeform' and
74     'symbol' method calls, you can call 'get_document_nodes' to get
75     the final list of document nodes (in a form suitable for returning
76     from a Sphinx directive's 'run' method).
77     """
78     def __init__(self, sphinx_directive):
79         self._cur_doc = None
80         self._sphinx_directive = sphinx_directive
81         self._top_node = nodes.section()
82         self._active_headings = [self._top_node]
83
84     def _make_dlitem(self, term, defn):
85         """Return a dlitem node with the specified term and definition.
86
87         term should be a list of Text and literal nodes.
88         defn should be one of:
89         - a string, which will be handed to _parse_text_into_node
90         - a list of Text and literal nodes, which will be put into
91           a paragraph node
92         """
93         dlitem = nodes.definition_list_item()
94         dlterm = nodes.term('', '', *term)
95         dlitem += dlterm
96         if defn:
97             dldef = nodes.definition()
98             if isinstance(defn, list):
99                 dldef += nodes.paragraph('', '', *defn)
100             else:
101                 self._parse_text_into_node(defn, dldef)
102             dlitem += dldef
103         return dlitem
104
105     def _make_section(self, title):
106         """Return a section node with optional title"""
107         section = nodes.section(ids=[self._sphinx_directive.new_serialno()])
108         if title:
109             section += nodes.title(title, title)
110         return section
111
112     def _nodes_for_ifcond(self, ifcond, with_if=True):
113         """Return list of Text, literal nodes for the ifcond
114
115         Return a list which gives text like ' (If: condition)'.
116         If with_if is False, we don't return the "(If: " and ")".
117         """
118
119         doc = ifcond.docgen()
120         if not doc:
121             return []
122         doc = nodes.literal('', doc)
123         if not with_if:
124             return [doc]
125
126         nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')]
127         nodelist.append(doc)
128         nodelist.append(nodes.Text(')'))
129         return nodelist
130
131     def _nodes_for_one_member(self, member):
132         """Return list of Text, literal nodes for this member
133
134         Return a list of doctree nodes which give text like
135         'name: type (optional) (If: ...)' suitable for use as the
136         'term' part of a definition list item.
137         """
138         term = [nodes.literal('', member.name)]
139         if member.type.doc_type():
140             term.append(nodes.Text(': '))
141             term.append(nodes.literal('', member.type.doc_type()))
142         if member.optional:
143             term.append(nodes.Text(' (optional)'))
144         if member.ifcond.is_present():
145             term.extend(self._nodes_for_ifcond(member.ifcond))
146         return term
147
148     def _nodes_for_variant_when(self, variants, variant):
149         """Return list of Text, literal nodes for variant 'when' clause
150
151         Return a list of doctree nodes which give text like
152         'when tagname is variant (If: ...)' suitable for use in
153         the 'variants' part of a definition list.
154         """
155         term = [nodes.Text(' when '),
156                 nodes.literal('', variants.tag_member.name),
157                 nodes.Text(' is '),
158                 nodes.literal('', '"%s"' % variant.name)]
159         if variant.ifcond.is_present():
160             term.extend(self._nodes_for_ifcond(variant.ifcond))
161         return term
162
163     def _nodes_for_members(self, doc, what, base=None, variants=None):
164         """Return list of doctree nodes for the table of members"""
165         dlnode = nodes.definition_list()
166         for section in doc.args.values():
167             term = self._nodes_for_one_member(section.member)
168             # TODO drop fallbacks when undocumented members are outlawed
169             if section.text:
170                 defn = section.text
171             elif (variants and variants.tag_member == section.member
172                   and not section.member.type.doc_type()):
173                 values = section.member.type.member_names()
174                 defn = [nodes.Text('One of ')]
175                 defn.extend(intersperse([nodes.literal('', v) for v in values],
176                                         nodes.Text(', ')))
177             else:
178                 defn = [nodes.Text('Not documented')]
179
180             dlnode += self._make_dlitem(term, defn)
181
182         if base:
183             dlnode += self._make_dlitem([nodes.Text('The members of '),
184                                          nodes.literal('', base.doc_type())],
185                                         None)
186
187         if variants:
188             for v in variants.variants:
189                 if v.type.is_implicit():
190                     assert not v.type.base and not v.type.variants
191                     for m in v.type.local_members:
192                         term = self._nodes_for_one_member(m)
193                         term.extend(self._nodes_for_variant_when(variants, v))
194                         dlnode += self._make_dlitem(term, None)
195                 else:
196                     term = [nodes.Text('The members of '),
197                             nodes.literal('', v.type.doc_type())]
198                     term.extend(self._nodes_for_variant_when(variants, v))
199                     dlnode += self._make_dlitem(term, None)
200
201         if not dlnode.children:
202             return []
203
204         section = self._make_section(what)
205         section += dlnode
206         return [section]
207
208     def _nodes_for_enum_values(self, doc):
209         """Return list of doctree nodes for the table of enum values"""
210         seen_item = False
211         dlnode = nodes.definition_list()
212         for section in doc.args.values():
213             termtext = [nodes.literal('', section.member.name)]
214             if section.member.ifcond.is_present():
215                 termtext.extend(self._nodes_for_ifcond(section.member.ifcond))
216             # TODO drop fallbacks when undocumented members are outlawed
217             if section.text:
218                 defn = section.text
219             else:
220                 defn = [nodes.Text('Not documented')]
221
222             dlnode += self._make_dlitem(termtext, defn)
223             seen_item = True
224
225         if not seen_item:
226             return []
227
228         section = self._make_section('Values')
229         section += dlnode
230         return [section]
231
232     def _nodes_for_arguments(self, doc, boxed_arg_type):
233         """Return list of doctree nodes for the arguments section"""
234         if boxed_arg_type:
235             assert not doc.args
236             section = self._make_section('Arguments')
237             dlnode = nodes.definition_list()
238             dlnode += self._make_dlitem(
239                 [nodes.Text('The members of '),
240                  nodes.literal('', boxed_arg_type.name)],
241                 None)
242             section += dlnode
243             return [section]
244
245         return self._nodes_for_members(doc, 'Arguments')
246
247     def _nodes_for_features(self, doc):
248         """Return list of doctree nodes for the table of features"""
249         seen_item = False
250         dlnode = nodes.definition_list()
251         for section in doc.features.values():
252             dlnode += self._make_dlitem([nodes.literal('', section.name)],
253                                         section.text)
254             seen_item = True
255
256         if not seen_item:
257             return []
258
259         section = self._make_section('Features')
260         section += dlnode
261         return [section]
262
263     def _nodes_for_example(self, exampletext):
264         """Return list of doctree nodes for a code example snippet"""
265         return [nodes.literal_block(exampletext, exampletext)]
266
267     def _nodes_for_sections(self, doc):
268         """Return list of doctree nodes for additional sections"""
269         nodelist = []
270         for section in doc.sections:
271             snode = self._make_section(section.name)
272             if section.name and section.name.startswith('Example'):
273                 snode += self._nodes_for_example(section.text)
274             else:
275                 self._parse_text_into_node(section.text, snode)
276             nodelist.append(snode)
277         return nodelist
278
279     def _nodes_for_if_section(self, ifcond):
280         """Return list of doctree nodes for the "If" section"""
281         nodelist = []
282         if ifcond.is_present():
283             snode = self._make_section('If')
284             snode += nodes.paragraph(
285                 '', '', *self._nodes_for_ifcond(ifcond, with_if=False)
286             )
287             nodelist.append(snode)
288         return nodelist
289
290     def _add_doc(self, typ, sections):
291         """Add documentation for a command/object/enum...
292
293         We assume we're documenting the thing defined in self._cur_doc.
294         typ is the type of thing being added ("Command", "Object", etc)
295
296         sections is a list of nodes for sections to add to the definition.
297         """
298
299         doc = self._cur_doc
300         snode = nodes.section(ids=[self._sphinx_directive.new_serialno()])
301         snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol),
302                                        nodes.Text(' (' + typ + ')')])
303         self._parse_text_into_node(doc.body.text, snode)
304         for s in sections:
305             if s is not None:
306                 snode += s
307         self._add_node_to_current_heading(snode)
308
309     def visit_enum_type(self, name, info, ifcond, features, members, prefix):
310         doc = self._cur_doc
311         self._add_doc('Enum',
312                       self._nodes_for_enum_values(doc)
313                       + self._nodes_for_features(doc)
314                       + self._nodes_for_sections(doc)
315                       + self._nodes_for_if_section(ifcond))
316
317     def visit_object_type(self, name, info, ifcond, features,
318                           base, members, variants):
319         doc = self._cur_doc
320         if base and base.is_implicit():
321             base = None
322         self._add_doc('Object',
323                       self._nodes_for_members(doc, 'Members', base, variants)
324                       + self._nodes_for_features(doc)
325                       + self._nodes_for_sections(doc)
326                       + self._nodes_for_if_section(ifcond))
327
328     def visit_alternate_type(self, name, info, ifcond, features, variants):
329         doc = self._cur_doc
330         self._add_doc('Alternate',
331                       self._nodes_for_members(doc, 'Members')
332                       + self._nodes_for_features(doc)
333                       + self._nodes_for_sections(doc)
334                       + self._nodes_for_if_section(ifcond))
335
336     def visit_command(self, name, info, ifcond, features, arg_type,
337                       ret_type, gen, success_response, boxed, allow_oob,
338                       allow_preconfig, coroutine):
339         doc = self._cur_doc
340         self._add_doc('Command',
341                       self._nodes_for_arguments(doc,
342                                                 arg_type if boxed else None)
343                       + self._nodes_for_features(doc)
344                       + self._nodes_for_sections(doc)
345                       + self._nodes_for_if_section(ifcond))
346
347     def visit_event(self, name, info, ifcond, features, arg_type, boxed):
348         doc = self._cur_doc
349         self._add_doc('Event',
350                       self._nodes_for_arguments(doc,
351                                                 arg_type if boxed else None)
352                       + self._nodes_for_features(doc)
353                       + self._nodes_for_sections(doc)
354                       + self._nodes_for_if_section(ifcond))
355
356     def symbol(self, doc, entity):
357         """Add documentation for one symbol to the document tree
358
359         This is the main entry point which causes us to add documentation
360         nodes for a symbol (which could be a 'command', 'object', 'event',
361         etc). We do this by calling 'visit' on the schema entity, which
362         will then call back into one of our visit_* methods, depending
363         on what kind of thing this symbol is.
364         """
365         self._cur_doc = doc
366         entity.visit(self)
367         self._cur_doc = None
368
369     def _start_new_heading(self, heading, level):
370         """Start a new heading at the specified heading level
371
372         Create a new section whose title is 'heading' and which is placed
373         in the docutils node tree as a child of the most recent level-1
374         heading. Subsequent document sections (commands, freeform doc chunks,
375         etc) will be placed as children of this new heading section.
376         """
377         if len(self._active_headings) < level:
378             raise QAPISemError(self._cur_doc.info,
379                                'Level %d subheading found outside a '
380                                'level %d heading'
381                                % (level, level - 1))
382         snode = self._make_section(heading)
383         self._active_headings[level - 1] += snode
384         self._active_headings = self._active_headings[:level]
385         self._active_headings.append(snode)
386
387     def _add_node_to_current_heading(self, node):
388         """Add the node to whatever the current active heading is"""
389         self._active_headings[-1] += node
390
391     def freeform(self, doc):
392         """Add a piece of 'freeform' documentation to the document tree
393
394         A 'freeform' document chunk doesn't relate to any particular
395         symbol (for instance, it could be an introduction).
396
397         If the freeform document starts with a line of the form
398         '= Heading text', this is a section or subsection heading, with
399         the heading level indicated by the number of '=' signs.
400         """
401
402         # QAPIDoc documentation says free-form documentation blocks
403         # must have only a body section, nothing else.
404         assert not doc.sections
405         assert not doc.args
406         assert not doc.features
407         self._cur_doc = doc
408
409         text = doc.body.text
410         if re.match(r'=+ ', text):
411             # Section/subsection heading (if present, will always be
412             # the first line of the block)
413             (heading, _, text) = text.partition('\n')
414             (leader, _, heading) = heading.partition(' ')
415             self._start_new_heading(heading, len(leader))
416             if text == '':
417                 return
418
419         node = self._make_section(None)
420         self._parse_text_into_node(text, node)
421         self._add_node_to_current_heading(node)
422         self._cur_doc = None
423
424     def _parse_text_into_node(self, doctext, node):
425         """Parse a chunk of QAPI-doc-format text into the node
426
427         The doc comment can contain most inline rST markup, including
428         bulleted and enumerated lists.
429         As an extra permitted piece of markup, @var will be turned
430         into ``var``.
431         """
432
433         # Handle the "@var means ``var`` case
434         doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext)
435
436         rstlist = ViewList()
437         for line in doctext.splitlines():
438             # The reported line number will always be that of the start line
439             # of the doc comment, rather than the actual location of the error.
440             # Being more precise would require overhaul of the QAPIDoc class
441             # to track lines more exactly within all the sub-parts of the doc
442             # comment, as well as counting lines here.
443             rstlist.append(line, self._cur_doc.info.fname,
444                            self._cur_doc.info.line)
445         # Append a blank line -- in some cases rST syntax errors get
446         # attributed to the line after one with actual text, and if there
447         # isn't anything in the ViewList corresponding to that then Sphinx
448         # 1.6's AutodocReporter will then misidentify the source/line location
449         # in the error message (usually attributing it to the top-level
450         # .rst file rather than the offending .json file). The extra blank
451         # line won't affect the rendered output.
452         rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
453         self._sphinx_directive.do_parse(rstlist, node)
454
455     def get_document_nodes(self):
456         """Return the list of docutils nodes which make up the document"""
457         return self._top_node.children
458
459
460 class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
461     """A QAPI schema visitor which adds Sphinx dependencies each module
462
463     This class calls the Sphinx note_dependency() function to tell Sphinx
464     that the generated documentation output depends on the input
465     schema file associated with each module in the QAPI input.
466     """
467     def __init__(self, env, qapidir):
468         self._env = env
469         self._qapidir = qapidir
470
471     def visit_module(self, name):
472         if name != "./builtin":
473             qapifile = self._qapidir + '/' + name
474             self._env.note_dependency(os.path.abspath(qapifile))
475         super().visit_module(name)
476
477
478 class QAPIDocDirective(Directive):
479     """Extract documentation from the specified QAPI .json file"""
480     required_argument = 1
481     optional_arguments = 1
482     option_spec = {
483         'qapifile': directives.unchanged_required
484     }
485     has_content = False
486
487     def new_serialno(self):
488         """Return a unique new ID string suitable for use as a node's ID"""
489         env = self.state.document.settings.env
490         return 'qapidoc-%d' % env.new_serialno('qapidoc')
491
492     def run(self):
493         env = self.state.document.settings.env
494         qapifile = env.config.qapidoc_srctree + '/' + self.arguments[0]
495         qapidir = os.path.dirname(qapifile)
496
497         try:
498             schema = QAPISchema(qapifile)
499
500             # First tell Sphinx about all the schema files that the
501             # output documentation depends on (including 'qapifile' itself)
502             schema.visit(QAPISchemaGenDepVisitor(env, qapidir))
503
504             vis = QAPISchemaGenRSTVisitor(self)
505             vis.visit_begin(schema)
506             for doc in schema.docs:
507                 if doc.symbol:
508                     vis.symbol(doc, schema.lookup_entity(doc.symbol))
509                 else:
510                     vis.freeform(doc)
511             return vis.get_document_nodes()
512         except QAPIError as err:
513             # Launder QAPI parse errors into Sphinx extension errors
514             # so they are displayed nicely to the user
515             raise ExtensionError(str(err))
516
517     def do_parse(self, rstlist, node):
518         """Parse rST source lines and add them to the specified node
519
520         Take the list of rST source lines rstlist, parse them as
521         rST, and add the resulting docutils nodes as children of node.
522         The nodes are parsed in a way that allows them to include
523         subheadings (titles) without confusing the rendering of
524         anything else.
525         """
526         # This is from kerneldoc.py -- it works around an API change in
527         # Sphinx between 1.6 and 1.7. Unlike kerneldoc.py, we use
528         # sphinx.util.nodes.nested_parse_with_titles() rather than the
529         # plain self.state.nested_parse(), and so we can drop the saving
530         # of title_styles and section_level that kerneldoc.py does,
531         # because nested_parse_with_titles() does that for us.
532         if Use_SSI:
533             with switch_source_input(self.state, rstlist):
534                 nested_parse_with_titles(self.state, rstlist, node)
535         else:
536             save = self.state.memo.reporter
537             self.state.memo.reporter = AutodocReporter(
538                 rstlist, self.state.memo.reporter)
539             try:
540                 nested_parse_with_titles(self.state, rstlist, node)
541             finally:
542                 self.state.memo.reporter = save
543
544
545 def setup(app):
546     """ Register qapi-doc directive with Sphinx"""
547     app.add_config_value('qapidoc_srctree', None, 'env')
548     app.add_directive('qapi-doc', QAPIDocDirective)
549
550     return dict(
551         version=__version__,
552         parallel_read_safe=True,
553         parallel_write_safe=True
554     )
This page took 0.054687 seconds and 4 git commands to generate.