blob: 9872fd5bf6a4516f36d8bb5e5e1566efb54bf70a [file] [log] [blame]
# D-Bus sphinx domain extension
#
# Copyright (C) 2021, Red Hat Inc.
#
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# Author: Marc-André Lureau <marcandre.lureau@redhat.com>
from typing import (
Any,
Dict,
Iterable,
Iterator,
List,
NamedTuple,
Optional,
Tuple,
cast,
)
from docutils import nodes
from docutils.nodes import Element, Node
from docutils.parsers.rst import directives
from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, Index, IndexEntry, ObjType
from sphinx.locale import _
from sphinx.roles import XRefRole
from sphinx.util import nodes as node_utils
from sphinx.util.docfields import Field, TypedField
from sphinx.util.typing import OptionSpec
class DBusDescription(ObjectDescription[str]):
"""Base class for DBus objects"""
option_spec: OptionSpec = ObjectDescription.option_spec.copy()
option_spec.update(
{
"deprecated": directives.flag,
}
)
def get_index_text(self, modname: str, name: str) -> str:
"""Return the text for the index entry of the object."""
raise NotImplementedError("must be implemented in subclasses")
def add_target_and_index(
self, name: str, sig: str, signode: desc_signature
) -> None:
ifacename = self.env.ref_context.get("dbus:interface")
node_id = name
if ifacename:
node_id = f"{ifacename}.{node_id}"
signode["names"].append(name)
signode["ids"].append(node_id)
if "noindexentry" not in self.options:
indextext = self.get_index_text(ifacename, name)
if indextext:
self.indexnode["entries"].append(
("single", indextext, node_id, "", None)
)
domain = cast(DBusDomain, self.env.get_domain("dbus"))
domain.note_object(name, self.objtype, node_id, location=signode)
class DBusInterface(DBusDescription):
"""
Implementation of ``dbus:interface``.
"""
def get_index_text(self, ifacename: str, name: str) -> str:
return ifacename
def before_content(self) -> None:
self.env.ref_context["dbus:interface"] = self.arguments[0]
def after_content(self) -> None:
self.env.ref_context.pop("dbus:interface")
def handle_signature(self, sig: str, signode: desc_signature) -> str:
signode += addnodes.desc_annotation("interface ", "interface ")
signode += addnodes.desc_name(sig, sig)
return sig
def run(self) -> List[Node]:
_, node = super().run()
name = self.arguments[0]
section = nodes.section(ids=[name + "-section"])
section += nodes.title(name, "%s interface" % name)
section += node
return [self.indexnode, section]
class DBusMember(DBusDescription):
signal = False
class DBusMethod(DBusMember):
"""
Implementation of ``dbus:method``.
"""
option_spec: OptionSpec = DBusMember.option_spec.copy()
option_spec.update(
{
"noreply": directives.flag,
}
)
doc_field_types: List[Field] = [
TypedField(
"arg",
label=_("Arguments"),
names=("arg",),
rolename="arg",
typerolename=None,
typenames=("argtype", "type"),
),
TypedField(
"ret",
label=_("Returns"),
names=("ret",),
rolename="ret",
typerolename=None,
typenames=("rettype", "type"),
),
]
def get_index_text(self, ifacename: str, name: str) -> str:
return _("%s() (%s method)") % (name, ifacename)
def handle_signature(self, sig: str, signode: desc_signature) -> str:
params = addnodes.desc_parameterlist()
returns = addnodes.desc_parameterlist()
contentnode = addnodes.desc_content()
self.state.nested_parse(self.content, self.content_offset, contentnode)
for child in contentnode:
if isinstance(child, nodes.field_list):
for field in child:
ty, sg, name = field[0].astext().split(None, 2)
param = addnodes.desc_parameter()
param += addnodes.desc_sig_keyword_type(sg, sg)
param += addnodes.desc_sig_space()
param += addnodes.desc_sig_name(name, name)
if ty == "arg":
params += param
elif ty == "ret":
returns += param
anno = "signal " if self.signal else "method "
signode += addnodes.desc_annotation(anno, anno)
signode += addnodes.desc_name(sig, sig)
signode += params
if not self.signal and "noreply" not in self.options:
ret = addnodes.desc_returns()
ret += returns
signode += ret
return sig
class DBusSignal(DBusMethod):
"""
Implementation of ``dbus:signal``.
"""
doc_field_types: List[Field] = [
TypedField(
"arg",
label=_("Arguments"),
names=("arg",),
rolename="arg",
typerolename=None,
typenames=("argtype", "type"),
),
]
signal = True
def get_index_text(self, ifacename: str, name: str) -> str:
return _("%s() (%s signal)") % (name, ifacename)
class DBusProperty(DBusMember):
"""
Implementation of ``dbus:property``.
"""
option_spec: OptionSpec = DBusMember.option_spec.copy()
option_spec.update(
{
"type": directives.unchanged,
"readonly": directives.flag,
"writeonly": directives.flag,
"readwrite": directives.flag,
"emits-changed": directives.unchanged,
}
)
doc_field_types: List[Field] = []
def get_index_text(self, ifacename: str, name: str) -> str:
return _("%s (%s property)") % (name, ifacename)
def transform_content(self, contentnode: addnodes.desc_content) -> None:
fieldlist = nodes.field_list()
access = None
if "readonly" in self.options:
access = _("read-only")
if "writeonly" in self.options:
access = _("write-only")
if "readwrite" in self.options:
access = _("read & write")
if access:
content = nodes.Text(access)
fieldname = nodes.field_name("", _("Access"))
fieldbody = nodes.field_body("", nodes.paragraph("", "", content))
field = nodes.field("", fieldname, fieldbody)
fieldlist += field
emits = self.options.get("emits-changed", None)
if emits:
content = nodes.Text(emits)
fieldname = nodes.field_name("", _("Emits Changed"))
fieldbody = nodes.field_body("", nodes.paragraph("", "", content))
field = nodes.field("", fieldname, fieldbody)
fieldlist += field
if len(fieldlist) > 0:
contentnode.insert(0, fieldlist)
def handle_signature(self, sig: str, signode: desc_signature) -> str:
contentnode = addnodes.desc_content()
self.state.nested_parse(self.content, self.content_offset, contentnode)
ty = self.options.get("type")
signode += addnodes.desc_annotation("property ", "property ")
signode += addnodes.desc_name(sig, sig)
signode += addnodes.desc_sig_punctuation("", ":")
signode += addnodes.desc_sig_keyword_type(ty, ty)
return sig
def run(self) -> List[Node]:
self.name = "dbus:member"
return super().run()
class DBusXRef(XRefRole):
def process_link(self, env, refnode, has_explicit_title, title, target):
refnode["dbus:interface"] = env.ref_context.get("dbus:interface")
if not has_explicit_title:
title = title.lstrip(".") # only has a meaning for the target
target = target.lstrip("~") # only has a meaning for the title
# if the first character is a tilde, don't display the module/class
# parts of the contents
if title[0:1] == "~":
title = title[1:]
dot = title.rfind(".")
if dot != -1:
title = title[dot + 1 :]
# if the first character is a dot, search more specific namespaces first
# else search builtins first
if target[0:1] == ".":
target = target[1:]
refnode["refspecific"] = True
return title, target
class DBusIndex(Index):
"""
Index subclass to provide a D-Bus interfaces index.
"""
name = "dbusindex"
localname = _("D-Bus Interfaces Index")
shortname = _("dbus")
def generate(
self, docnames: Iterable[str] = None
) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
content: Dict[str, List[IndexEntry]] = {}
# list of prefixes to ignore
ignores: List[str] = self.domain.env.config["dbus_index_common_prefix"]
ignores = sorted(ignores, key=len, reverse=True)
ifaces = sorted(
[
x
for x in self.domain.data["objects"].items()
if x[1].objtype == "interface"
],
key=lambda x: x[0].lower(),
)
for name, (docname, node_id, _) in ifaces:
if docnames and docname not in docnames:
continue
for ignore in ignores:
if name.startswith(ignore):
name = name[len(ignore) :]
stripped = ignore
break
else:
stripped = ""
entries = content.setdefault(name[0].lower(), [])
entries.append(IndexEntry(stripped + name, 0, docname, node_id, "", "", ""))
# sort by first letter
sorted_content = sorted(content.items())
return sorted_content, False
class ObjectEntry(NamedTuple):
docname: str
node_id: str
objtype: str
class DBusDomain(Domain):
"""
Implementation of the D-Bus domain.
"""
name = "dbus"
label = "D-Bus"
object_types: Dict[str, ObjType] = {
"interface": ObjType(_("interface"), "iface", "obj"),
"method": ObjType(_("method"), "meth", "obj"),
"signal": ObjType(_("signal"), "sig", "obj"),
"property": ObjType(_("property"), "attr", "_prop", "obj"),
}
directives = {
"interface": DBusInterface,
"method": DBusMethod,
"signal": DBusSignal,
"property": DBusProperty,
}
roles = {
"iface": DBusXRef(),
"meth": DBusXRef(),
"sig": DBusXRef(),
"prop": DBusXRef(),
}
initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
"objects": {}, # fullname -> ObjectEntry
}
indices = [
DBusIndex,
]
@property
def objects(self) -> Dict[str, ObjectEntry]:
return self.data.setdefault("objects", {}) # fullname -> ObjectEntry
def note_object(
self, name: str, objtype: str, node_id: str, location: Any = None
) -> None:
self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype)
def clear_doc(self, docname: str) -> None:
for fullname, obj in list(self.objects.items()):
if obj.docname == docname:
del self.objects[fullname]
def find_obj(self, typ: str, name: str) -> Optional[Tuple[str, ObjectEntry]]:
# skip parens
if name[-2:] == "()":
name = name[:-2]
if typ in ("meth", "sig", "prop"):
try:
ifacename, name = name.rsplit(".", 1)
except ValueError:
pass
return self.objects.get(name)
def resolve_xref(
self,
env: "BuildEnvironment",
fromdocname: str,
builder: "Builder",
typ: str,
target: str,
node: pending_xref,
contnode: Element,
) -> Optional[Element]:
"""Resolve the pending_xref *node* with the given *typ* and *target*."""
objdef = self.find_obj(typ, target)
if objdef:
return node_utils.make_refnode(
builder, fromdocname, objdef.docname, objdef.node_id, contnode
)
def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
for refname, obj in self.objects.items():
yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1)
def merge_domaindata(self, docnames, otherdata):
for name, obj in otherdata['objects'].items():
if obj.docname in docnames:
self.data['objects'][name] = obj
def setup(app):
app.add_domain(DBusDomain)
app.add_config_value("dbus_index_common_prefix", [], "env")