1 # D-Bus sphinx domain extension
3 # Copyright (C) 2021, Red Hat Inc.
5 # SPDX-License-Identifier: LGPL-2.1-or-later
7 # Author: Marc-André Lureau <marcandre.lureau@redhat.com>
21 from docutils
import nodes
22 from docutils
.nodes
import Element
, Node
23 from docutils
.parsers
.rst
import directives
24 from sphinx
import addnodes
25 from sphinx
.addnodes
import desc_signature
, pending_xref
26 from sphinx
.directives
import ObjectDescription
27 from sphinx
.domains
import Domain
, Index
, IndexEntry
, ObjType
28 from sphinx
.locale
import _
29 from sphinx
.roles
import XRefRole
30 from sphinx
.util
import nodes
as node_utils
31 from sphinx
.util
.docfields
import Field
, TypedField
32 from sphinx
.util
.typing
import OptionSpec
35 class DBusDescription(ObjectDescription
[str]):
36 """Base class for DBus objects"""
38 option_spec
: OptionSpec
= ObjectDescription
.option_spec
.copy()
41 "deprecated": directives
.flag
,
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")
49 def add_target_and_index(
50 self
, name
: str, sig
: str, signode
: desc_signature
52 ifacename
= self
.env
.ref_context
.get("dbus:interface")
55 node_id
= f
"{ifacename}.{node_id}"
57 signode
["names"].append(name
)
58 signode
["ids"].append(node_id
)
60 if "noindexentry" not in self
.options
:
61 indextext
= self
.get_index_text(ifacename
, name
)
63 self
.indexnode
["entries"].append(
64 ("single", indextext
, node_id
, "", None)
67 domain
= cast(DBusDomain
, self
.env
.get_domain("dbus"))
68 domain
.note_object(name
, self
.objtype
, node_id
, location
=signode
)
71 class DBusInterface(DBusDescription
):
73 Implementation of ``dbus:interface``.
76 def get_index_text(self
, ifacename
: str, name
: str) -> str:
79 def before_content(self
) -> None:
80 self
.env
.ref_context
["dbus:interface"] = self
.arguments
[0]
82 def after_content(self
) -> None:
83 self
.env
.ref_context
.pop("dbus:interface")
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
)
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
)
96 return [self
.indexnode
, section
]
99 class DBusMember(DBusDescription
):
104 class DBusMethod(DBusMember
):
106 Implementation of ``dbus:method``.
109 option_spec
: OptionSpec
= DBusMember
.option_spec
.copy()
112 "noreply": directives
.flag
,
116 doc_field_types
: List
[Field
] = [
119 label
=_("Arguments"),
123 typenames
=("argtype", "type"),
131 typenames
=("rettype", "type"),
135 def get_index_text(self
, ifacename
: str, name
: str) -> str:
136 return _("%s() (%s method)") % (name
, ifacename
)
138 def handle_signature(self
, sig
: str, signode
: desc_signature
) -> str:
139 params
= addnodes
.desc_parameterlist()
140 returns
= addnodes
.desc_parameterlist()
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
):
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
)
157 anno
= "signal " if self
.signal
else "method "
158 signode
+= addnodes
.desc_annotation(anno
, anno
)
159 signode
+= addnodes
.desc_name(sig
, sig
)
161 if not self
.signal
and "noreply" not in self
.options
:
162 ret
= addnodes
.desc_returns()
169 class DBusSignal(DBusMethod
):
171 Implementation of ``dbus:signal``.
174 doc_field_types
: List
[Field
] = [
177 label
=_("Arguments"),
181 typenames
=("argtype", "type"),
186 def get_index_text(self
, ifacename
: str, name
: str) -> str:
187 return _("%s() (%s signal)") % (name
, ifacename
)
190 class DBusProperty(DBusMember
):
192 Implementation of ``dbus:property``.
195 option_spec
: OptionSpec
= DBusMember
.option_spec
.copy()
198 "type": directives
.unchanged
,
199 "readonly": directives
.flag
,
200 "writeonly": directives
.flag
,
201 "readwrite": directives
.flag
,
202 "emits-changed": directives
.unchanged
,
206 doc_field_types
: List
[Field
] = []
208 def get_index_text(self
, ifacename
: str, name
: str) -> str:
209 return _("%s (%s property)") % (name
, ifacename
)
211 def transform_content(self
, contentnode
: addnodes
.desc_content
) -> None:
212 fieldlist
= nodes
.field_list()
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")
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
)
226 emits
= self
.options
.get("emits-changed", None)
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
)
233 if len(fieldlist
) > 0:
234 contentnode
.insert(0, fieldlist
)
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")
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
)
247 def run(self
) -> List
[Node
]:
248 self
.name
= "dbus:member"
252 class 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] == "~":
262 dot
= title
.rfind(".")
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] == ".":
269 refnode
["refspecific"] = True
273 class DBusIndex(Index
):
275 Index subclass to provide a D-Bus interfaces index.
279 localname
= _("D-Bus Interfaces Index")
280 shortname
= _("dbus")
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)
293 for x
in self
.domain
.data
["objects"].items()
294 if x
[1].objtype
== "interface"
296 key
=lambda x
: x
[0].lower(),
298 for name
, (docname
, node_id
, _
) in ifaces
:
299 if docnames
and docname
not in docnames
:
302 for ignore
in ignores
:
303 if name
.startswith(ignore
):
304 name
= name
[len(ignore
) :]
310 entries
= content
.setdefault(name
[0].lower(), [])
311 entries
.append(IndexEntry(stripped
+ name
, 0, docname
, node_id
, "", "", ""))
313 # sort by first letter
314 sorted_content
= sorted(content
.items())
316 return sorted_content
, False
319 class ObjectEntry(NamedTuple
):
325 class DBusDomain(Domain
):
327 Implementation of the D-Bus domain.
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"),
339 "interface": DBusInterface
,
340 "method": DBusMethod
,
341 "signal": DBusSignal
,
342 "property": DBusProperty
,
350 initial_data
: Dict
[str, Dict
[str, Tuple
[Any
]]] = {
351 "objects": {}, # fullname -> ObjectEntry
358 def objects(self
) -> Dict
[str, ObjectEntry
]:
359 return self
.data
.setdefault("objects", {}) # fullname -> ObjectEntry
362 self
, name
: str, objtype
: str, node_id
: str, location
: Any
= None
364 self
.objects
[name
] = ObjectEntry(self
.env
.docname
, node_id
, objtype
)
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
]
371 def find_obj(self
, typ
: str, name
: str) -> Optional
[Tuple
[str, ObjectEntry
]]:
373 if name
[-2:] == "()":
375 if typ
in ("meth", "sig", "prop"):
377 ifacename
, name
= name
.rsplit(".", 1)
380 return self
.objects
.get(name
)
384 env
: "BuildEnvironment",
391 ) -> Optional
[Element
]:
392 """Resolve the pending_xref *node* with the given *typ* and *target*."""
393 objdef
= self
.find_obj(typ
, target
)
395 return node_utils
.make_refnode(
396 builder
, fromdocname
, objdef
.docname
, objdef
.node_id
, contnode
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)
403 def merge_domaindata(self
, docnames
, otherdata
):
404 for name
, obj
in otherdata
['objects'].items():
405 if obj
.docname
in docnames
:
406 self
.data
['objects'][name
] = obj
409 app
.add_domain(DBusDomain
)
410 app
.add_config_value("dbus_index_common_prefix", [], "env")