 ed8d95182b
			
		
	
	
		ed8d95182b
		
	
	
	
	
		
			
			Save a bit of build time by passing the number of jobs option to sphinx. We cannot use the -j option from make because meson does not support setting build time parameters for custom targets. Use nproc instead or the equivalent sphinx option "-j auto", if that is available (version >=1.7.0). Also make sure our plugins support parallelism and report it properly to sphinx. Particularly, implement the merge_domaindata method in DBusDomain that is used to merge in data from other subprocesses. Tested-by: Daniel P. Berrangé <berrange@redhat.com> Signed-off-by: Fabiano Rosas <farosas@suse.de> Message-Id: <20230503203947.3417-2-farosas@suse.de> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
		
			
				
	
	
		
			411 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			411 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # 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")
 |