qapidoc.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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. qapidoc is a Sphinx extension that implements the qapi-doc directive
  11. The purpose of this extension is to read the documentation comments
  12. in QAPI schema files, and insert them all into the current document.
  13. It implements one new rST directive, "qapi-doc::".
  14. Each qapi-doc:: directive takes one argument, which is the
  15. pathname of the schema file to process, relative to the source tree.
  16. The docs/conf.py file must set the qapidoc_srctree config value to
  17. the root of the QEMU source tree.
  18. The Sphinx documentation on writing extensions is at:
  19. https://www.sphinx-doc.org/en/master/development/index.html
  20. """
  21. from __future__ import annotations
  22. import os
  23. import sys
  24. from typing import TYPE_CHECKING
  25. from docutils import nodes
  26. from docutils.parsers.rst import Directive, directives
  27. from qapi.error import QAPIError
  28. from qapi.schema import QAPISchema, QAPISchemaVisitor
  29. from qapidoc_legacy import QAPISchemaGenRSTVisitor # type: ignore
  30. from sphinx import addnodes
  31. from sphinx.directives.code import CodeBlock
  32. from sphinx.errors import ExtensionError
  33. from sphinx.util.docutils import switch_source_input
  34. from sphinx.util.nodes import nested_parse_with_titles
  35. if TYPE_CHECKING:
  36. from typing import Any, List, Sequence
  37. from docutils.statemachine import StringList
  38. from sphinx.application import Sphinx
  39. from sphinx.util.typing import ExtensionMetadata
  40. __version__ = "1.0"
  41. class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
  42. """A QAPI schema visitor which adds Sphinx dependencies each module
  43. This class calls the Sphinx note_dependency() function to tell Sphinx
  44. that the generated documentation output depends on the input
  45. schema file associated with each module in the QAPI input.
  46. """
  47. def __init__(self, env: Any, qapidir: str) -> None:
  48. self._env = env
  49. self._qapidir = qapidir
  50. def visit_module(self, name: str) -> None:
  51. if name != "./builtin":
  52. qapifile = self._qapidir + "/" + name
  53. self._env.note_dependency(os.path.abspath(qapifile))
  54. super().visit_module(name)
  55. class NestedDirective(Directive):
  56. def run(self) -> Sequence[nodes.Node]:
  57. raise NotImplementedError
  58. def do_parse(self, rstlist: StringList, node: nodes.Node) -> None:
  59. """
  60. Parse rST source lines and add them to the specified node
  61. Take the list of rST source lines rstlist, parse them as
  62. rST, and add the resulting docutils nodes as children of node.
  63. The nodes are parsed in a way that allows them to include
  64. subheadings (titles) without confusing the rendering of
  65. anything else.
  66. """
  67. with switch_source_input(self.state, rstlist):
  68. nested_parse_with_titles(self.state, rstlist, node)
  69. class QAPIDocDirective(NestedDirective):
  70. """Extract documentation from the specified QAPI .json file"""
  71. required_argument = 1
  72. optional_arguments = 1
  73. option_spec = {
  74. "qapifile": directives.unchanged_required,
  75. "transmogrify": directives.flag,
  76. }
  77. has_content = False
  78. def new_serialno(self) -> str:
  79. """Return a unique new ID string suitable for use as a node's ID"""
  80. env = self.state.document.settings.env
  81. return "qapidoc-%d" % env.new_serialno("qapidoc")
  82. def transmogrify(self, schema: QAPISchema) -> nodes.Element:
  83. raise NotImplementedError
  84. def legacy(self, schema: QAPISchema) -> nodes.Element:
  85. vis = QAPISchemaGenRSTVisitor(self)
  86. vis.visit_begin(schema)
  87. for doc in schema.docs:
  88. if doc.symbol:
  89. vis.symbol(doc, schema.lookup_entity(doc.symbol))
  90. else:
  91. vis.freeform(doc)
  92. return vis.get_document_node() # type: ignore
  93. def run(self) -> Sequence[nodes.Node]:
  94. env = self.state.document.settings.env
  95. qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0]
  96. qapidir = os.path.dirname(qapifile)
  97. transmogrify = "transmogrify" in self.options
  98. try:
  99. schema = QAPISchema(qapifile)
  100. # First tell Sphinx about all the schema files that the
  101. # output documentation depends on (including 'qapifile' itself)
  102. schema.visit(QAPISchemaGenDepVisitor(env, qapidir))
  103. except QAPIError as err:
  104. # Launder QAPI parse errors into Sphinx extension errors
  105. # so they are displayed nicely to the user
  106. raise ExtensionError(str(err)) from err
  107. if transmogrify:
  108. contentnode = self.transmogrify(schema)
  109. else:
  110. contentnode = self.legacy(schema)
  111. return contentnode.children
  112. class QMPExample(CodeBlock, NestedDirective):
  113. """
  114. Custom admonition for QMP code examples.
  115. When the :annotated: option is present, the body of this directive
  116. is parsed as normal rST, but with any '::' code blocks set to use
  117. the QMP lexer. Code blocks must be explicitly written by the user,
  118. but this allows for intermingling explanatory paragraphs with
  119. arbitrary rST syntax and code blocks for more involved examples.
  120. When :annotated: is absent, the directive body is treated as a
  121. simple standalone QMP code block literal.
  122. """
  123. required_argument = 0
  124. optional_arguments = 0
  125. has_content = True
  126. option_spec = {
  127. "annotated": directives.flag,
  128. "title": directives.unchanged,
  129. }
  130. def _highlightlang(self) -> addnodes.highlightlang:
  131. """Return the current highlightlang setting for the document"""
  132. node = None
  133. doc = self.state.document
  134. if hasattr(doc, "findall"):
  135. # docutils >= 0.18.1
  136. for node in doc.findall(addnodes.highlightlang):
  137. pass
  138. else:
  139. for elem in doc.traverse():
  140. if isinstance(elem, addnodes.highlightlang):
  141. node = elem
  142. if node:
  143. return node
  144. # No explicit directive found, use defaults
  145. node = addnodes.highlightlang(
  146. lang=self.env.config.highlight_language,
  147. force=False,
  148. # Yes, Sphinx uses this value to effectively disable line
  149. # numbers and not 0 or None or -1 or something. ¯\_(ツ)_/¯
  150. linenothreshold=sys.maxsize,
  151. )
  152. return node
  153. def admonition_wrap(self, *content: nodes.Node) -> List[nodes.Node]:
  154. title = "Example:"
  155. if "title" in self.options:
  156. title = f"{title} {self.options['title']}"
  157. admon = nodes.admonition(
  158. "",
  159. nodes.title("", title),
  160. *content,
  161. classes=["admonition", "admonition-example"],
  162. )
  163. return [admon]
  164. def run_annotated(self) -> List[nodes.Node]:
  165. lang_node = self._highlightlang()
  166. content_node: nodes.Element = nodes.section()
  167. # Configure QMP highlighting for "::" blocks, if needed
  168. if lang_node["lang"] != "QMP":
  169. content_node += addnodes.highlightlang(
  170. lang="QMP",
  171. force=False, # "True" ignores lexing errors
  172. linenothreshold=lang_node["linenothreshold"],
  173. )
  174. self.do_parse(self.content, content_node)
  175. # Restore prior language highlighting, if needed
  176. if lang_node["lang"] != "QMP":
  177. content_node += addnodes.highlightlang(**lang_node.attributes)
  178. return content_node.children
  179. def run(self) -> List[nodes.Node]:
  180. annotated = "annotated" in self.options
  181. if annotated:
  182. content_nodes = self.run_annotated()
  183. else:
  184. self.arguments = ["QMP"]
  185. content_nodes = super().run()
  186. return self.admonition_wrap(*content_nodes)
  187. def setup(app: Sphinx) -> ExtensionMetadata:
  188. """Register qapi-doc directive with Sphinx"""
  189. app.add_config_value("qapidoc_srctree", None, "env")
  190. app.add_directive("qapi-doc", QAPIDocDirective)
  191. app.add_directive("qmp-example", QMPExample)
  192. return {
  193. "version": __version__,
  194. "parallel_read_safe": True,
  195. "parallel_write_safe": True,
  196. }