2
0

dbusdoc.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. # D-Bus XML documentation extension
  2. #
  3. # Copyright (C) 2021, Red Hat Inc.
  4. #
  5. # SPDX-License-Identifier: LGPL-2.1-or-later
  6. #
  7. # Author: Marc-André Lureau <marcandre.lureau@redhat.com>
  8. """dbus-doc is a Sphinx extension that provides documentation from D-Bus XML."""
  9. import os
  10. import re
  11. from typing import (
  12. TYPE_CHECKING,
  13. Any,
  14. Callable,
  15. Dict,
  16. Iterator,
  17. List,
  18. Optional,
  19. Sequence,
  20. Set,
  21. Tuple,
  22. Type,
  23. TypeVar,
  24. Union,
  25. )
  26. import sphinx
  27. from docutils import nodes
  28. from docutils.nodes import Element, Node
  29. from docutils.parsers.rst import Directive, directives
  30. from docutils.parsers.rst.states import RSTState
  31. from docutils.statemachine import StringList, ViewList
  32. from sphinx.application import Sphinx
  33. from sphinx.errors import ExtensionError
  34. from sphinx.util import logging
  35. from sphinx.util.docstrings import prepare_docstring
  36. from sphinx.util.docutils import SphinxDirective, switch_source_input
  37. from sphinx.util.nodes import nested_parse_with_titles
  38. import dbusdomain
  39. from dbusparser import parse_dbus_xml
  40. logger = logging.getLogger(__name__)
  41. __version__ = "1.0"
  42. class DBusDoc:
  43. def __init__(self, sphinx_directive, dbusfile):
  44. self._cur_doc = None
  45. self._sphinx_directive = sphinx_directive
  46. self._dbusfile = dbusfile
  47. self._top_node = nodes.section()
  48. self.result = StringList()
  49. self.indent = ""
  50. def add_line(self, line: str, *lineno: int) -> None:
  51. """Append one line of generated reST to the output."""
  52. if line.strip(): # not a blank line
  53. self.result.append(self.indent + line, self._dbusfile, *lineno)
  54. else:
  55. self.result.append("", self._dbusfile, *lineno)
  56. def add_method(self, method):
  57. self.add_line(f".. dbus:method:: {method.name}")
  58. self.add_line("")
  59. self.indent += " "
  60. for arg in method.in_args:
  61. self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}")
  62. for arg in method.out_args:
  63. self.add_line(f":ret {arg.signature} {arg.name}: {arg.doc_string}")
  64. self.add_line("")
  65. for line in prepare_docstring("\n" + method.doc_string):
  66. self.add_line(line)
  67. self.indent = self.indent[:-3]
  68. def add_signal(self, signal):
  69. self.add_line(f".. dbus:signal:: {signal.name}")
  70. self.add_line("")
  71. self.indent += " "
  72. for arg in signal.args:
  73. self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}")
  74. self.add_line("")
  75. for line in prepare_docstring("\n" + signal.doc_string):
  76. self.add_line(line)
  77. self.indent = self.indent[:-3]
  78. def add_property(self, prop):
  79. self.add_line(f".. dbus:property:: {prop.name}")
  80. self.indent += " "
  81. self.add_line(f":type: {prop.signature}")
  82. access = {"read": "readonly", "write": "writeonly", "readwrite": "readwrite"}[
  83. prop.access
  84. ]
  85. self.add_line(f":{access}:")
  86. if prop.emits_changed_signal:
  87. self.add_line(f":emits-changed: yes")
  88. self.add_line("")
  89. for line in prepare_docstring("\n" + prop.doc_string):
  90. self.add_line(line)
  91. self.indent = self.indent[:-3]
  92. def add_interface(self, iface):
  93. self.add_line(f".. dbus:interface:: {iface.name}")
  94. self.add_line("")
  95. self.indent += " "
  96. for line in prepare_docstring("\n" + iface.doc_string):
  97. self.add_line(line)
  98. for method in iface.methods:
  99. self.add_method(method)
  100. for sig in iface.signals:
  101. self.add_signal(sig)
  102. for prop in iface.properties:
  103. self.add_property(prop)
  104. self.indent = self.indent[:-3]
  105. def parse_generated_content(state: RSTState, content: StringList) -> List[Node]:
  106. """Parse a generated content by Documenter."""
  107. with switch_source_input(state, content):
  108. node = nodes.paragraph()
  109. node.document = state.document
  110. state.nested_parse(content, 0, node)
  111. return node.children
  112. class DBusDocDirective(SphinxDirective):
  113. """Extract documentation from the specified D-Bus XML file"""
  114. has_content = True
  115. required_arguments = 1
  116. optional_arguments = 0
  117. final_argument_whitespace = True
  118. def run(self):
  119. reporter = self.state.document.reporter
  120. try:
  121. source, lineno = reporter.get_source_and_line(self.lineno) # type: ignore
  122. except AttributeError:
  123. source, lineno = (None, None)
  124. logger.debug("[dbusdoc] %s:%s: input:\n%s", source, lineno, self.block_text)
  125. env = self.state.document.settings.env
  126. dbusfile = env.config.qapidoc_srctree + "/" + self.arguments[0]
  127. with open(dbusfile, "rb") as f:
  128. xml_data = f.read()
  129. xml = parse_dbus_xml(xml_data)
  130. doc = DBusDoc(self, dbusfile)
  131. for iface in xml:
  132. doc.add_interface(iface)
  133. result = parse_generated_content(self.state, doc.result)
  134. return result
  135. def setup(app: Sphinx) -> Dict[str, Any]:
  136. """Register dbus-doc directive with Sphinx"""
  137. app.add_config_value("dbusdoc_srctree", None, "env")
  138. app.add_directive("dbus-doc", DBusDocDirective)
  139. dbusdomain.setup(app)
  140. return dict(version=__version__, parallel_read_safe=True, parallel_write_safe=True)