2
0

dbusdomain.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. # D-Bus sphinx domain 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. from typing import (
  9. Any,
  10. Dict,
  11. Iterable,
  12. Iterator,
  13. List,
  14. NamedTuple,
  15. Optional,
  16. Tuple,
  17. cast,
  18. )
  19. from docutils import nodes
  20. from docutils.nodes import Element, Node
  21. from docutils.parsers.rst import directives
  22. from sphinx import addnodes
  23. from sphinx.addnodes import desc_signature, pending_xref
  24. from sphinx.directives import ObjectDescription
  25. from sphinx.domains import Domain, Index, IndexEntry, ObjType
  26. from sphinx.locale import _
  27. from sphinx.roles import XRefRole
  28. from sphinx.util import nodes as node_utils
  29. from sphinx.util.docfields import Field, TypedField
  30. from sphinx.util.typing import OptionSpec
  31. class DBusDescription(ObjectDescription[str]):
  32. """Base class for DBus objects"""
  33. option_spec: OptionSpec = ObjectDescription.option_spec.copy()
  34. option_spec.update(
  35. {
  36. "deprecated": directives.flag,
  37. }
  38. )
  39. def get_index_text(self, modname: str, name: str) -> str:
  40. """Return the text for the index entry of the object."""
  41. raise NotImplementedError("must be implemented in subclasses")
  42. def add_target_and_index(
  43. self, name: str, sig: str, signode: desc_signature
  44. ) -> None:
  45. ifacename = self.env.ref_context.get("dbus:interface")
  46. node_id = name
  47. if ifacename:
  48. node_id = f"{ifacename}.{node_id}"
  49. signode["names"].append(name)
  50. signode["ids"].append(node_id)
  51. if "noindexentry" not in self.options:
  52. indextext = self.get_index_text(ifacename, name)
  53. if indextext:
  54. self.indexnode["entries"].append(
  55. ("single", indextext, node_id, "", None)
  56. )
  57. domain = cast(DBusDomain, self.env.get_domain("dbus"))
  58. domain.note_object(name, self.objtype, node_id, location=signode)
  59. class DBusInterface(DBusDescription):
  60. """
  61. Implementation of ``dbus:interface``.
  62. """
  63. def get_index_text(self, ifacename: str, name: str) -> str:
  64. return ifacename
  65. def before_content(self) -> None:
  66. self.env.ref_context["dbus:interface"] = self.arguments[0]
  67. def after_content(self) -> None:
  68. self.env.ref_context.pop("dbus:interface")
  69. def handle_signature(self, sig: str, signode: desc_signature) -> str:
  70. signode += addnodes.desc_annotation("interface ", "interface ")
  71. signode += addnodes.desc_name(sig, sig)
  72. return sig
  73. def run(self) -> List[Node]:
  74. _, node = super().run()
  75. name = self.arguments[0]
  76. section = nodes.section(ids=[name + "-section"])
  77. section += nodes.title(name, "%s interface" % name)
  78. section += node
  79. return [self.indexnode, section]
  80. class DBusMember(DBusDescription):
  81. signal = False
  82. class DBusMethod(DBusMember):
  83. """
  84. Implementation of ``dbus:method``.
  85. """
  86. option_spec: OptionSpec = DBusMember.option_spec.copy()
  87. option_spec.update(
  88. {
  89. "noreply": directives.flag,
  90. }
  91. )
  92. doc_field_types: List[Field] = [
  93. TypedField(
  94. "arg",
  95. label=_("Arguments"),
  96. names=("arg",),
  97. rolename="arg",
  98. typerolename=None,
  99. typenames=("argtype", "type"),
  100. ),
  101. TypedField(
  102. "ret",
  103. label=_("Returns"),
  104. names=("ret",),
  105. rolename="ret",
  106. typerolename=None,
  107. typenames=("rettype", "type"),
  108. ),
  109. ]
  110. def get_index_text(self, ifacename: str, name: str) -> str:
  111. return _("%s() (%s method)") % (name, ifacename)
  112. def handle_signature(self, sig: str, signode: desc_signature) -> str:
  113. params = addnodes.desc_parameterlist()
  114. returns = addnodes.desc_parameterlist()
  115. contentnode = addnodes.desc_content()
  116. self.state.nested_parse(self.content, self.content_offset, contentnode)
  117. for child in contentnode:
  118. if isinstance(child, nodes.field_list):
  119. for field in child:
  120. ty, sg, name = field[0].astext().split(None, 2)
  121. param = addnodes.desc_parameter()
  122. param += addnodes.desc_sig_keyword_type(sg, sg)
  123. param += addnodes.desc_sig_space()
  124. param += addnodes.desc_sig_name(name, name)
  125. if ty == "arg":
  126. params += param
  127. elif ty == "ret":
  128. returns += param
  129. anno = "signal " if self.signal else "method "
  130. signode += addnodes.desc_annotation(anno, anno)
  131. signode += addnodes.desc_name(sig, sig)
  132. signode += params
  133. if not self.signal and "noreply" not in self.options:
  134. ret = addnodes.desc_returns()
  135. ret += returns
  136. signode += ret
  137. return sig
  138. class DBusSignal(DBusMethod):
  139. """
  140. Implementation of ``dbus:signal``.
  141. """
  142. doc_field_types: List[Field] = [
  143. TypedField(
  144. "arg",
  145. label=_("Arguments"),
  146. names=("arg",),
  147. rolename="arg",
  148. typerolename=None,
  149. typenames=("argtype", "type"),
  150. ),
  151. ]
  152. signal = True
  153. def get_index_text(self, ifacename: str, name: str) -> str:
  154. return _("%s() (%s signal)") % (name, ifacename)
  155. class DBusProperty(DBusMember):
  156. """
  157. Implementation of ``dbus:property``.
  158. """
  159. option_spec: OptionSpec = DBusMember.option_spec.copy()
  160. option_spec.update(
  161. {
  162. "type": directives.unchanged,
  163. "readonly": directives.flag,
  164. "writeonly": directives.flag,
  165. "readwrite": directives.flag,
  166. "emits-changed": directives.unchanged,
  167. }
  168. )
  169. doc_field_types: List[Field] = []
  170. def get_index_text(self, ifacename: str, name: str) -> str:
  171. return _("%s (%s property)") % (name, ifacename)
  172. def transform_content(self, contentnode: addnodes.desc_content) -> None:
  173. fieldlist = nodes.field_list()
  174. access = None
  175. if "readonly" in self.options:
  176. access = _("read-only")
  177. if "writeonly" in self.options:
  178. access = _("write-only")
  179. if "readwrite" in self.options:
  180. access = _("read & write")
  181. if access:
  182. content = nodes.Text(access)
  183. fieldname = nodes.field_name("", _("Access"))
  184. fieldbody = nodes.field_body("", nodes.paragraph("", "", content))
  185. field = nodes.field("", fieldname, fieldbody)
  186. fieldlist += field
  187. emits = self.options.get("emits-changed", None)
  188. if emits:
  189. content = nodes.Text(emits)
  190. fieldname = nodes.field_name("", _("Emits Changed"))
  191. fieldbody = nodes.field_body("", nodes.paragraph("", "", content))
  192. field = nodes.field("", fieldname, fieldbody)
  193. fieldlist += field
  194. if len(fieldlist) > 0:
  195. contentnode.insert(0, fieldlist)
  196. def handle_signature(self, sig: str, signode: desc_signature) -> str:
  197. contentnode = addnodes.desc_content()
  198. self.state.nested_parse(self.content, self.content_offset, contentnode)
  199. ty = self.options.get("type")
  200. signode += addnodes.desc_annotation("property ", "property ")
  201. signode += addnodes.desc_name(sig, sig)
  202. signode += addnodes.desc_sig_punctuation("", ":")
  203. signode += addnodes.desc_sig_keyword_type(ty, ty)
  204. return sig
  205. def run(self) -> List[Node]:
  206. self.name = "dbus:member"
  207. return super().run()
  208. class DBusXRef(XRefRole):
  209. def process_link(self, env, refnode, has_explicit_title, title, target):
  210. refnode["dbus:interface"] = env.ref_context.get("dbus:interface")
  211. if not has_explicit_title:
  212. title = title.lstrip(".") # only has a meaning for the target
  213. target = target.lstrip("~") # only has a meaning for the title
  214. # if the first character is a tilde, don't display the module/class
  215. # parts of the contents
  216. if title[0:1] == "~":
  217. title = title[1:]
  218. dot = title.rfind(".")
  219. if dot != -1:
  220. title = title[dot + 1 :]
  221. # if the first character is a dot, search more specific namespaces first
  222. # else search builtins first
  223. if target[0:1] == ".":
  224. target = target[1:]
  225. refnode["refspecific"] = True
  226. return title, target
  227. class DBusIndex(Index):
  228. """
  229. Index subclass to provide a D-Bus interfaces index.
  230. """
  231. name = "dbusindex"
  232. localname = _("D-Bus Interfaces Index")
  233. shortname = _("dbus")
  234. def generate(
  235. self, docnames: Iterable[str] = None
  236. ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
  237. content: Dict[str, List[IndexEntry]] = {}
  238. # list of prefixes to ignore
  239. ignores: List[str] = self.domain.env.config["dbus_index_common_prefix"]
  240. ignores = sorted(ignores, key=len, reverse=True)
  241. ifaces = sorted(
  242. [
  243. x
  244. for x in self.domain.data["objects"].items()
  245. if x[1].objtype == "interface"
  246. ],
  247. key=lambda x: x[0].lower(),
  248. )
  249. for name, (docname, node_id, _) in ifaces:
  250. if docnames and docname not in docnames:
  251. continue
  252. for ignore in ignores:
  253. if name.startswith(ignore):
  254. name = name[len(ignore) :]
  255. stripped = ignore
  256. break
  257. else:
  258. stripped = ""
  259. entries = content.setdefault(name[0].lower(), [])
  260. entries.append(IndexEntry(stripped + name, 0, docname, node_id, "", "", ""))
  261. # sort by first letter
  262. sorted_content = sorted(content.items())
  263. return sorted_content, False
  264. class ObjectEntry(NamedTuple):
  265. docname: str
  266. node_id: str
  267. objtype: str
  268. class DBusDomain(Domain):
  269. """
  270. Implementation of the D-Bus domain.
  271. """
  272. name = "dbus"
  273. label = "D-Bus"
  274. object_types: Dict[str, ObjType] = {
  275. "interface": ObjType(_("interface"), "iface", "obj"),
  276. "method": ObjType(_("method"), "meth", "obj"),
  277. "signal": ObjType(_("signal"), "sig", "obj"),
  278. "property": ObjType(_("property"), "attr", "_prop", "obj"),
  279. }
  280. directives = {
  281. "interface": DBusInterface,
  282. "method": DBusMethod,
  283. "signal": DBusSignal,
  284. "property": DBusProperty,
  285. }
  286. roles = {
  287. "iface": DBusXRef(),
  288. "meth": DBusXRef(),
  289. "sig": DBusXRef(),
  290. "prop": DBusXRef(),
  291. }
  292. initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
  293. "objects": {}, # fullname -> ObjectEntry
  294. }
  295. indices = [
  296. DBusIndex,
  297. ]
  298. @property
  299. def objects(self) -> Dict[str, ObjectEntry]:
  300. return self.data.setdefault("objects", {}) # fullname -> ObjectEntry
  301. def note_object(
  302. self, name: str, objtype: str, node_id: str, location: Any = None
  303. ) -> None:
  304. self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype)
  305. def clear_doc(self, docname: str) -> None:
  306. for fullname, obj in list(self.objects.items()):
  307. if obj.docname == docname:
  308. del self.objects[fullname]
  309. def find_obj(self, typ: str, name: str) -> Optional[Tuple[str, ObjectEntry]]:
  310. # skip parens
  311. if name[-2:] == "()":
  312. name = name[:-2]
  313. if typ in ("meth", "sig", "prop"):
  314. try:
  315. ifacename, name = name.rsplit(".", 1)
  316. except ValueError:
  317. pass
  318. return self.objects.get(name)
  319. def resolve_xref(
  320. self,
  321. env: "BuildEnvironment",
  322. fromdocname: str,
  323. builder: "Builder",
  324. typ: str,
  325. target: str,
  326. node: pending_xref,
  327. contnode: Element,
  328. ) -> Optional[Element]:
  329. """Resolve the pending_xref *node* with the given *typ* and *target*."""
  330. objdef = self.find_obj(typ, target)
  331. if objdef:
  332. return node_utils.make_refnode(
  333. builder, fromdocname, objdef.docname, objdef.node_id, contnode
  334. )
  335. def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
  336. for refname, obj in self.objects.items():
  337. yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1)
  338. def setup(app):
  339. app.add_domain(DBusDomain)
  340. app.add_config_value("dbus_index_common_prefix", [], "env")