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