11
qapidoc is a Sphinx extension that implements the qapi-doc directive
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.
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.
20
The docs/conf.py file must set the qapidoc_srctree config value to
21
the root of the QEMU source tree.
23
The Sphinx documentation on writing extensions is at:
24
https://www.sphinx-doc.org/en/master/development/index.html
31
from typing import List
33
from docutils import nodes
34
from docutils.parsers.rst import Directive, directives
35
from docutils.statemachine import ViewList
36
from qapi.error import QAPIError, QAPISemError
37
from qapi.gen import QAPISchemaVisitor
38
from qapi.schema import QAPISchema
40
from sphinx import addnodes
41
from sphinx.directives.code import CodeBlock
42
from sphinx.errors import ExtensionError
43
from sphinx.util.docutils import switch_source_input
44
from sphinx.util.nodes import nested_parse_with_titles
50
def dedent(text: str) -> str:
53
lines = text.splitlines(True)
54
if re.match(r"\s+", lines[0]):
57
return textwrap.dedent(text)
60
return lines[0] + textwrap.dedent("".join(lines[1:]))
67
class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
68
"""A QAPI schema visitor which generates docutils/Sphinx nodes
70
This class builds up a tree of docutils/Sphinx nodes corresponding
71
to documentation for the various QAPI objects. To use it, first
72
create a QAPISchemaGenRSTVisitor object, and call its
73
visit_begin() method. Then you can call one of the two methods
74
'freeform' (to add documentation for a freeform documentation
75
chunk) or 'symbol' (to add documentation for a QAPI symbol). These
76
will cause the visitor to build up the tree of document
77
nodes. Once you've added all the documentation via 'freeform' and
78
'symbol' method calls, you can call 'get_document_nodes' to get
79
the final list of document nodes (in a form suitable for returning
80
from a Sphinx directive's 'run' method).
82
def __init__(self, sphinx_directive):
84
self._sphinx_directive = sphinx_directive
85
self._top_node = nodes.section()
86
self._active_headings = [self._top_node]
88
def _make_dlitem(self, term, defn):
89
"""Return a dlitem node with the specified term and definition.
91
term should be a list of Text and literal nodes.
92
defn should be one of:
93
- a string, which will be handed to _parse_text_into_node
94
- a list of Text and literal nodes, which will be put into
97
dlitem = nodes.definition_list_item()
98
dlterm = nodes.term('', '', *term)
101
dldef = nodes.definition()
102
if isinstance(defn, list):
103
dldef += nodes.paragraph('', '', *defn)
105
self._parse_text_into_node(defn, dldef)
109
def _make_section(self, title):
110
"""Return a section node with optional title"""
111
section = nodes.section(ids=[self._sphinx_directive.new_serialno()])
113
section += nodes.title(title, title)
116
def _nodes_for_ifcond(self, ifcond, with_if=True):
117
"""Return list of Text, literal nodes for the ifcond
119
Return a list which gives text like ' (If: condition)'.
120
If with_if is False, we don't return the "(If: " and ")".
123
doc = ifcond.docgen()
126
doc = nodes.literal('', doc)
130
nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')]
132
nodelist.append(nodes.Text(')'))
135
def _nodes_for_one_member(self, member):
136
"""Return list of Text, literal nodes for this member
138
Return a list of doctree nodes which give text like
139
'name: type (optional) (If: ...)' suitable for use as the
140
'term' part of a definition list item.
142
term = [nodes.literal('', member.name)]
143
if member.type.doc_type():
144
term.append(nodes.Text(': '))
145
term.append(nodes.literal('', member.type.doc_type()))
147
term.append(nodes.Text(' (optional)'))
148
if member.ifcond.is_present():
149
term.extend(self._nodes_for_ifcond(member.ifcond))
152
def _nodes_for_variant_when(self, branches, variant):
153
"""Return list of Text, literal nodes for variant 'when' clause
155
Return a list of doctree nodes which give text like
156
'when tagname is variant (If: ...)' suitable for use in
157
the 'branches' part of a definition list.
159
term = [nodes.Text(' when '),
160
nodes.literal('', branches.tag_member.name),
162
nodes.literal('', '"%s"' % variant.name)]
163
if variant.ifcond.is_present():
164
term.extend(self._nodes_for_ifcond(variant.ifcond))
167
def _nodes_for_members(self, doc, what, base=None, branches=None):
168
"""Return list of doctree nodes for the table of members"""
169
dlnode = nodes.definition_list()
170
for section in doc.args.values():
171
term = self._nodes_for_one_member(section.member)
174
defn = dedent(section.text)
176
defn = [nodes.Text('Not documented')]
178
dlnode += self._make_dlitem(term, defn)
181
dlnode += self._make_dlitem([nodes.Text('The members of '),
182
nodes.literal('', base.doc_type())],
186
for v in branches.variants:
187
if v.type.name == 'q_empty':
189
assert not v.type.is_implicit()
190
term = [nodes.Text('The members of '),
191
nodes.literal('', v.type.doc_type())]
192
term.extend(self._nodes_for_variant_when(branches, v))
193
dlnode += self._make_dlitem(term, None)
195
if not dlnode.children:
198
section = self._make_section(what)
202
def _nodes_for_enum_values(self, doc):
203
"""Return list of doctree nodes for the table of enum values"""
205
dlnode = nodes.definition_list()
206
for section in doc.args.values():
207
termtext = [nodes.literal('', section.member.name)]
208
if section.member.ifcond.is_present():
209
termtext.extend(self._nodes_for_ifcond(section.member.ifcond))
212
defn = dedent(section.text)
214
defn = [nodes.Text('Not documented')]
216
dlnode += self._make_dlitem(termtext, defn)
222
section = self._make_section('Values')
226
def _nodes_for_arguments(self, doc, arg_type):
227
"""Return list of doctree nodes for the arguments section"""
228
if arg_type and not arg_type.is_implicit():
230
section = self._make_section('Arguments')
231
dlnode = nodes.definition_list()
232
dlnode += self._make_dlitem(
233
[nodes.Text('The members of '),
234
nodes.literal('', arg_type.name)],
239
return self._nodes_for_members(doc, 'Arguments')
241
def _nodes_for_features(self, doc):
242
"""Return list of doctree nodes for the table of features"""
244
dlnode = nodes.definition_list()
245
for section in doc.features.values():
246
dlnode += self._make_dlitem(
247
[nodes.literal('', section.member.name)], dedent(section.text))
253
section = self._make_section('Features')
257
def _nodes_for_example(self, exampletext):
258
"""Return list of doctree nodes for a code example snippet"""
259
return [nodes.literal_block(exampletext, exampletext)]
261
def _nodes_for_sections(self, doc):
262
"""Return list of doctree nodes for additional sections"""
264
for section in doc.sections:
265
if section.tag and section.tag == 'TODO':
272
container = nodes.container()
273
self._parse_text_into_node(section.text, container)
274
nodelist += container.children
277
snode = self._make_section(section.tag)
278
if section.tag.startswith('Example'):
279
snode += self._nodes_for_example(dedent(section.text))
281
self._parse_text_into_node(dedent(section.text), snode)
282
nodelist.append(snode)
285
def _nodes_for_if_section(self, ifcond):
286
"""Return list of doctree nodes for the "If" section"""
288
if ifcond.is_present():
289
snode = self._make_section('If')
290
snode += nodes.paragraph(
291
'', '', *self._nodes_for_ifcond(ifcond, with_if=False)
293
nodelist.append(snode)
296
def _add_doc(self, typ, sections):
297
"""Add documentation for a command/object/enum...
299
We assume we're documenting the thing defined in self._cur_doc.
300
typ is the type of thing being added ("Command", "Object", etc)
302
sections is a list of nodes for sections to add to the definition.
306
snode = nodes.section(ids=[self._sphinx_directive.new_serialno()])
307
snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol),
308
nodes.Text(' (' + typ + ')')])
309
self._parse_text_into_node(doc.body.text, snode)
313
self._add_node_to_current_heading(snode)
315
def visit_enum_type(self, name, info, ifcond, features, members, prefix):
317
self._add_doc('Enum',
318
self._nodes_for_enum_values(doc)
319
+ self._nodes_for_features(doc)
320
+ self._nodes_for_sections(doc)
321
+ self._nodes_for_if_section(ifcond))
323
def visit_object_type(self, name, info, ifcond, features,
324
base, members, branches):
326
if base and base.is_implicit():
328
self._add_doc('Object',
329
self._nodes_for_members(doc, 'Members', base, branches)
330
+ self._nodes_for_features(doc)
331
+ self._nodes_for_sections(doc)
332
+ self._nodes_for_if_section(ifcond))
334
def visit_alternate_type(self, name, info, ifcond, features,
337
self._add_doc('Alternate',
338
self._nodes_for_members(doc, 'Members')
339
+ self._nodes_for_features(doc)
340
+ self._nodes_for_sections(doc)
341
+ self._nodes_for_if_section(ifcond))
343
def visit_command(self, name, info, ifcond, features, arg_type,
344
ret_type, gen, success_response, boxed, allow_oob,
345
allow_preconfig, coroutine):
347
self._add_doc('Command',
348
self._nodes_for_arguments(doc, arg_type)
349
+ self._nodes_for_features(doc)
350
+ self._nodes_for_sections(doc)
351
+ self._nodes_for_if_section(ifcond))
353
def visit_event(self, name, info, ifcond, features, arg_type, boxed):
355
self._add_doc('Event',
356
self._nodes_for_arguments(doc, arg_type)
357
+ self._nodes_for_features(doc)
358
+ self._nodes_for_sections(doc)
359
+ self._nodes_for_if_section(ifcond))
361
def symbol(self, doc, entity):
362
"""Add documentation for one symbol to the document tree
364
This is the main entry point which causes us to add documentation
365
nodes for a symbol (which could be a 'command', 'object', 'event',
366
etc). We do this by calling 'visit' on the schema entity, which
367
will then call back into one of our visit_* methods, depending
368
on what kind of thing this symbol is.
374
def _start_new_heading(self, heading, level):
375
"""Start a new heading at the specified heading level
377
Create a new section whose title is 'heading' and which is placed
378
in the docutils node tree as a child of the most recent level-1
379
heading. Subsequent document sections (commands, freeform doc chunks,
380
etc) will be placed as children of this new heading section.
382
if len(self._active_headings) < level:
383
raise QAPISemError(self._cur_doc.info,
384
'Level %d subheading found outside a '
386
% (level, level - 1))
387
snode = self._make_section(heading)
388
self._active_headings[level - 1] += snode
389
self._active_headings = self._active_headings[:level]
390
self._active_headings.append(snode)
393
def _add_node_to_current_heading(self, node):
394
"""Add the node to whatever the current active heading is"""
395
self._active_headings[-1] += node
397
def freeform(self, doc):
398
"""Add a piece of 'freeform' documentation to the document tree
400
A 'freeform' document chunk doesn't relate to any particular
401
symbol (for instance, it could be an introduction).
403
If the freeform document starts with a line of the form
404
'= Heading text', this is a section or subsection heading, with
405
the heading level indicated by the number of '=' signs.
410
assert not doc.sections
412
assert not doc.features
416
if re.match(r'=+ ', text):
419
(heading, _, text) = text.partition('\n')
420
(leader, _, heading) = heading.partition(' ')
421
node = self._start_new_heading(heading, len(leader))
425
self._parse_text_into_node(text, node)
428
def _parse_text_into_node(self, doctext, node):
429
"""Parse a chunk of QAPI-doc-format text into the node
431
The doc comment can contain most inline rST markup, including
432
bulleted and enumerated lists.
433
As an extra permitted piece of markup, @var will be turned
438
doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext)
441
for line in doctext.splitlines():
447
rstlist.append(line, self._cur_doc.info.fname,
448
self._cur_doc.info.line)
456
rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
457
self._sphinx_directive.do_parse(rstlist, node)
459
def get_document_nodes(self):
460
"""Return the list of docutils nodes which make up the document"""
461
return self._top_node.children
468
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
469
"""A QAPI schema visitor which adds Sphinx dependencies each module
471
This class calls the Sphinx note_dependency() function to tell Sphinx
472
that the generated documentation output depends on the input
473
schema file associated with each module in the QAPI input.
476
def __init__(self, env, qapidir):
478
self._qapidir = qapidir
480
def visit_module(self, name):
481
if name != "./builtin":
482
qapifile = self._qapidir + "/" + name
483
self._env.note_dependency(os.path.abspath(qapifile))
484
super().visit_module(name)
487
class NestedDirective(Directive):
489
raise NotImplementedError
491
def do_parse(self, rstlist, node):
493
Parse rST source lines and add them to the specified node
495
Take the list of rST source lines rstlist, parse them as
496
rST, and add the resulting docutils nodes as children of node.
497
The nodes are parsed in a way that allows them to include
498
subheadings (titles) without confusing the rendering of
501
with switch_source_input(self.state, rstlist):
502
nested_parse_with_titles(self.state, rstlist, node)
505
class QAPIDocDirective(NestedDirective):
506
"""Extract documentation from the specified QAPI .json file"""
508
required_argument = 1
509
optional_arguments = 1
510
option_spec = {"qapifile": directives.unchanged_required}
513
def new_serialno(self):
514
"""Return a unique new ID string suitable for use as a node's ID"""
515
env = self.state.document.settings.env
516
return "qapidoc-%d" % env.new_serialno("qapidoc")
519
env = self.state.document.settings.env
520
qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0]
521
qapidir = os.path.dirname(qapifile)
524
schema = QAPISchema(qapifile)
528
schema.visit(QAPISchemaGenDepVisitor(env, qapidir))
530
vis = QAPISchemaGenRSTVisitor(self)
531
vis.visit_begin(schema)
532
for doc in schema.docs:
534
vis.symbol(doc, schema.lookup_entity(doc.symbol))
537
return vis.get_document_nodes()
538
except QAPIError as err:
541
raise ExtensionError(str(err)) from err
544
class QMPExample(CodeBlock, NestedDirective):
546
Custom admonition for QMP code examples.
548
When the :annotated: option is present, the body of this directive
549
is parsed as normal rST, but with any '::' code blocks set to use
550
the QMP lexer. Code blocks must be explicitly written by the user,
551
but this allows for intermingling explanatory paragraphs with
552
arbitrary rST syntax and code blocks for more involved examples.
554
When :annotated: is absent, the directive body is treated as a
555
simple standalone QMP code block literal.
558
required_argument = 0
559
optional_arguments = 0
562
"annotated": directives.flag,
563
"title": directives.unchanged,
566
def _highlightlang(self) -> addnodes.highlightlang:
567
"""Return the current highlightlang setting for the document"""
569
doc = self.state.document
571
if hasattr(doc, "findall"):
573
for node in doc.findall(addnodes.highlightlang):
576
for elem in doc.traverse():
577
if isinstance(elem, addnodes.highlightlang):
584
node = addnodes.highlightlang(
585
lang=self.env.config.highlight_language,
589
linenothreshold=sys.maxsize,
593
def admonition_wrap(self, *content) -> List[nodes.Node]:
595
if "title" in self.options:
596
title = f"{title} {self.options['title']}"
598
admon = nodes.admonition(
600
nodes.title("", title),
602
classes=["admonition", "admonition-example"],
606
def run_annotated(self) -> List[nodes.Node]:
607
lang_node = self._highlightlang()
609
content_node: nodes.Element = nodes.section()
612
if lang_node["lang"] != "QMP":
613
content_node += addnodes.highlightlang(
616
linenothreshold=lang_node["linenothreshold"],
619
self.do_parse(self.content, content_node)
622
if lang_node["lang"] != "QMP":
623
content_node += addnodes.highlightlang(**lang_node.attributes)
625
return content_node.children
627
def run(self) -> List[nodes.Node]:
628
annotated = "annotated" in self.options
631
content_nodes = self.run_annotated()
633
self.arguments = ["QMP"]
634
content_nodes = super().run()
636
return self.admonition_wrap(*content_nodes)
640
"""Register qapi-doc directive with Sphinx"""
641
app.add_config_value("qapidoc_srctree", None, "env")
642
app.add_directive("qapi-doc", QAPIDocDirective)
643
app.add_directive("qmp-example", QMPExample)
646
"version": __version__,
647
"parallel_read_safe": True,
648
"parallel_write_safe": True,