mirror of https://github.com/xemu-project/xemu.git
docs/sphinx: add sphinx modules to include D-Bus documentation
Add a new dbus-doc directive to import D-Bus interfaces documentation from the introspection XML. The comments annotations follow the gtkdoc/kerneldoc style, and should be formatted with reST. Note: I realize after the fact that I was implementing those modules with sphinx 4, and that we have much lower requirements. Instead of lowering the features and code (removing type annotations etc), let's have a warning in the documentation when the D-Bus modules can't be used, and point to the source XML file in that case. Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com> Acked-by: Gerd Hoffmann <kraxel@redhat.com>
This commit is contained in:
parent
20f19713ef
commit
2668dc7b5d
|
@ -73,6 +73,12 @@ needs_sphinx = '1.6'
|
||||||
# ones.
|
# ones.
|
||||||
extensions = ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc']
|
extensions = ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc']
|
||||||
|
|
||||||
|
if sphinx.version_info[:3] > (4, 0, 0):
|
||||||
|
tags.add('sphinx4')
|
||||||
|
extensions += ['dbusdoc']
|
||||||
|
else:
|
||||||
|
extensions += ['fakedbusdoc']
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = [os.path.join(qemu_docdir, '_templates')]
|
templates_path = [os.path.join(qemu_docdir, '_templates')]
|
||||||
|
|
||||||
|
@ -311,3 +317,5 @@ kerneldoc_bin = ['perl', os.path.join(qemu_docdir, '../scripts/kernel-doc')]
|
||||||
kerneldoc_srctree = os.path.join(qemu_docdir, '..')
|
kerneldoc_srctree = os.path.join(qemu_docdir, '..')
|
||||||
hxtool_srctree = os.path.join(qemu_docdir, '..')
|
hxtool_srctree = os.path.join(qemu_docdir, '..')
|
||||||
qapidoc_srctree = os.path.join(qemu_docdir, '..')
|
qapidoc_srctree = os.path.join(qemu_docdir, '..')
|
||||||
|
dbusdoc_srctree = os.path.join(qemu_docdir, '..')
|
||||||
|
dbus_index_common_prefix = ["org.qemu."]
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
# D-Bus XML documentation extension
|
||||||
|
#
|
||||||
|
# Copyright (C) 2021, Red Hat Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#
|
||||||
|
# Author: Marc-André Lureau <marcandre.lureau@redhat.com>
|
||||||
|
"""dbus-doc is a Sphinx extension that provides documentation from D-Bus XML."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
|
import sphinx
|
||||||
|
from docutils import nodes
|
||||||
|
from docutils.nodes import Element, Node
|
||||||
|
from docutils.parsers.rst import Directive, directives
|
||||||
|
from docutils.parsers.rst.states import RSTState
|
||||||
|
from docutils.statemachine import StringList, ViewList
|
||||||
|
from sphinx.application import Sphinx
|
||||||
|
from sphinx.errors import ExtensionError
|
||||||
|
from sphinx.util import logging
|
||||||
|
from sphinx.util.docstrings import prepare_docstring
|
||||||
|
from sphinx.util.docutils import SphinxDirective, switch_source_input
|
||||||
|
from sphinx.util.nodes import nested_parse_with_titles
|
||||||
|
|
||||||
|
import dbusdomain
|
||||||
|
from dbusparser import parse_dbus_xml
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
__version__ = "1.0"
|
||||||
|
|
||||||
|
|
||||||
|
class DBusDoc:
|
||||||
|
def __init__(self, sphinx_directive, dbusfile):
|
||||||
|
self._cur_doc = None
|
||||||
|
self._sphinx_directive = sphinx_directive
|
||||||
|
self._dbusfile = dbusfile
|
||||||
|
self._top_node = nodes.section()
|
||||||
|
self.result = StringList()
|
||||||
|
self.indent = ""
|
||||||
|
|
||||||
|
def add_line(self, line: str, *lineno: int) -> None:
|
||||||
|
"""Append one line of generated reST to the output."""
|
||||||
|
if line.strip(): # not a blank line
|
||||||
|
self.result.append(self.indent + line, self._dbusfile, *lineno)
|
||||||
|
else:
|
||||||
|
self.result.append("", self._dbusfile, *lineno)
|
||||||
|
|
||||||
|
def add_method(self, method):
|
||||||
|
self.add_line(f".. dbus:method:: {method.name}")
|
||||||
|
self.add_line("")
|
||||||
|
self.indent += " "
|
||||||
|
for arg in method.in_args:
|
||||||
|
self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}")
|
||||||
|
for arg in method.out_args:
|
||||||
|
self.add_line(f":ret {arg.signature} {arg.name}: {arg.doc_string}")
|
||||||
|
self.add_line("")
|
||||||
|
for line in prepare_docstring("\n" + method.doc_string):
|
||||||
|
self.add_line(line)
|
||||||
|
self.indent = self.indent[:-3]
|
||||||
|
|
||||||
|
def add_signal(self, signal):
|
||||||
|
self.add_line(f".. dbus:signal:: {signal.name}")
|
||||||
|
self.add_line("")
|
||||||
|
self.indent += " "
|
||||||
|
for arg in signal.args:
|
||||||
|
self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}")
|
||||||
|
self.add_line("")
|
||||||
|
for line in prepare_docstring("\n" + signal.doc_string):
|
||||||
|
self.add_line(line)
|
||||||
|
self.indent = self.indent[:-3]
|
||||||
|
|
||||||
|
def add_property(self, prop):
|
||||||
|
self.add_line(f".. dbus:property:: {prop.name}")
|
||||||
|
self.indent += " "
|
||||||
|
self.add_line(f":type: {prop.signature}")
|
||||||
|
access = {"read": "readonly", "write": "writeonly", "readwrite": "readwrite"}[
|
||||||
|
prop.access
|
||||||
|
]
|
||||||
|
self.add_line(f":{access}:")
|
||||||
|
if prop.emits_changed_signal:
|
||||||
|
self.add_line(f":emits-changed: yes")
|
||||||
|
self.add_line("")
|
||||||
|
for line in prepare_docstring("\n" + prop.doc_string):
|
||||||
|
self.add_line(line)
|
||||||
|
self.indent = self.indent[:-3]
|
||||||
|
|
||||||
|
def add_interface(self, iface):
|
||||||
|
self.add_line(f".. dbus:interface:: {iface.name}")
|
||||||
|
self.add_line("")
|
||||||
|
self.indent += " "
|
||||||
|
for line in prepare_docstring("\n" + iface.doc_string):
|
||||||
|
self.add_line(line)
|
||||||
|
for method in iface.methods:
|
||||||
|
self.add_method(method)
|
||||||
|
for sig in iface.signals:
|
||||||
|
self.add_signal(sig)
|
||||||
|
for prop in iface.properties:
|
||||||
|
self.add_property(prop)
|
||||||
|
self.indent = self.indent[:-3]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_generated_content(state: RSTState, content: StringList) -> List[Node]:
|
||||||
|
"""Parse a generated content by Documenter."""
|
||||||
|
with switch_source_input(state, content):
|
||||||
|
node = nodes.paragraph()
|
||||||
|
node.document = state.document
|
||||||
|
state.nested_parse(content, 0, node)
|
||||||
|
|
||||||
|
return node.children
|
||||||
|
|
||||||
|
|
||||||
|
class DBusDocDirective(SphinxDirective):
|
||||||
|
"""Extract documentation from the specified D-Bus XML file"""
|
||||||
|
|
||||||
|
has_content = True
|
||||||
|
required_arguments = 1
|
||||||
|
optional_arguments = 0
|
||||||
|
final_argument_whitespace = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
reporter = self.state.document.reporter
|
||||||
|
|
||||||
|
try:
|
||||||
|
source, lineno = reporter.get_source_and_line(self.lineno) # type: ignore
|
||||||
|
except AttributeError:
|
||||||
|
source, lineno = (None, None)
|
||||||
|
|
||||||
|
logger.debug("[dbusdoc] %s:%s: input:\n%s", source, lineno, self.block_text)
|
||||||
|
|
||||||
|
env = self.state.document.settings.env
|
||||||
|
dbusfile = env.config.qapidoc_srctree + "/" + self.arguments[0]
|
||||||
|
with open(dbusfile, "rb") as f:
|
||||||
|
xml_data = f.read()
|
||||||
|
xml = parse_dbus_xml(xml_data)
|
||||||
|
doc = DBusDoc(self, dbusfile)
|
||||||
|
for iface in xml:
|
||||||
|
doc.add_interface(iface)
|
||||||
|
|
||||||
|
result = parse_generated_content(self.state, doc.result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app: Sphinx) -> Dict[str, Any]:
|
||||||
|
"""Register dbus-doc directive with Sphinx"""
|
||||||
|
app.add_config_value("dbusdoc_srctree", None, "env")
|
||||||
|
app.add_directive("dbus-doc", DBusDocDirective)
|
||||||
|
dbusdomain.setup(app)
|
||||||
|
|
||||||
|
return dict(version=__version__, parallel_read_safe=True, parallel_write_safe=True)
|
|
@ -0,0 +1,406 @@
|
||||||
|
# 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 setup(app):
|
||||||
|
app.add_domain(DBusDomain)
|
||||||
|
app.add_config_value("dbus_index_common_prefix", [], "env")
|
|
@ -0,0 +1,373 @@
|
||||||
|
# Based from "GDBus - GLib D-Bus Library":
|
||||||
|
#
|
||||||
|
# Copyright (C) 2008-2011 Red Hat, Inc.
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General
|
||||||
|
# Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# Author: David Zeuthen <davidz@redhat.com>
|
||||||
|
|
||||||
|
import xml.parsers.expat
|
||||||
|
|
||||||
|
|
||||||
|
class Annotation:
|
||||||
|
def __init__(self, key, value):
|
||||||
|
self.key = key
|
||||||
|
self.value = value
|
||||||
|
self.annotations = []
|
||||||
|
self.since = ""
|
||||||
|
|
||||||
|
|
||||||
|
class Arg:
|
||||||
|
def __init__(self, name, signature):
|
||||||
|
self.name = name
|
||||||
|
self.signature = signature
|
||||||
|
self.annotations = []
|
||||||
|
self.doc_string = ""
|
||||||
|
self.since = ""
|
||||||
|
|
||||||
|
|
||||||
|
class Method:
|
||||||
|
def __init__(self, name, h_type_implies_unix_fd=True):
|
||||||
|
self.name = name
|
||||||
|
self.h_type_implies_unix_fd = h_type_implies_unix_fd
|
||||||
|
self.in_args = []
|
||||||
|
self.out_args = []
|
||||||
|
self.annotations = []
|
||||||
|
self.doc_string = ""
|
||||||
|
self.since = ""
|
||||||
|
self.deprecated = False
|
||||||
|
self.unix_fd = False
|
||||||
|
|
||||||
|
|
||||||
|
class Signal:
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.args = []
|
||||||
|
self.annotations = []
|
||||||
|
self.doc_string = ""
|
||||||
|
self.since = ""
|
||||||
|
self.deprecated = False
|
||||||
|
|
||||||
|
|
||||||
|
class Property:
|
||||||
|
def __init__(self, name, signature, access):
|
||||||
|
self.name = name
|
||||||
|
self.signature = signature
|
||||||
|
self.access = access
|
||||||
|
self.annotations = []
|
||||||
|
self.arg = Arg("value", self.signature)
|
||||||
|
self.arg.annotations = self.annotations
|
||||||
|
self.readable = False
|
||||||
|
self.writable = False
|
||||||
|
if self.access == "readwrite":
|
||||||
|
self.readable = True
|
||||||
|
self.writable = True
|
||||||
|
elif self.access == "read":
|
||||||
|
self.readable = True
|
||||||
|
elif self.access == "write":
|
||||||
|
self.writable = True
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid access type "{}"'.format(self.access))
|
||||||
|
self.doc_string = ""
|
||||||
|
self.since = ""
|
||||||
|
self.deprecated = False
|
||||||
|
self.emits_changed_signal = True
|
||||||
|
|
||||||
|
|
||||||
|
class Interface:
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.methods = []
|
||||||
|
self.signals = []
|
||||||
|
self.properties = []
|
||||||
|
self.annotations = []
|
||||||
|
self.doc_string = ""
|
||||||
|
self.doc_string_brief = ""
|
||||||
|
self.since = ""
|
||||||
|
self.deprecated = False
|
||||||
|
|
||||||
|
|
||||||
|
class DBusXMLParser:
|
||||||
|
STATE_TOP = "top"
|
||||||
|
STATE_NODE = "node"
|
||||||
|
STATE_INTERFACE = "interface"
|
||||||
|
STATE_METHOD = "method"
|
||||||
|
STATE_SIGNAL = "signal"
|
||||||
|
STATE_PROPERTY = "property"
|
||||||
|
STATE_ARG = "arg"
|
||||||
|
STATE_ANNOTATION = "annotation"
|
||||||
|
STATE_IGNORED = "ignored"
|
||||||
|
|
||||||
|
def __init__(self, xml_data, h_type_implies_unix_fd=True):
|
||||||
|
self._parser = xml.parsers.expat.ParserCreate()
|
||||||
|
self._parser.CommentHandler = self.handle_comment
|
||||||
|
self._parser.CharacterDataHandler = self.handle_char_data
|
||||||
|
self._parser.StartElementHandler = self.handle_start_element
|
||||||
|
self._parser.EndElementHandler = self.handle_end_element
|
||||||
|
|
||||||
|
self.parsed_interfaces = []
|
||||||
|
self._cur_object = None
|
||||||
|
|
||||||
|
self.state = DBusXMLParser.STATE_TOP
|
||||||
|
self.state_stack = []
|
||||||
|
self._cur_object = None
|
||||||
|
self._cur_object_stack = []
|
||||||
|
|
||||||
|
self.doc_comment_last_symbol = ""
|
||||||
|
|
||||||
|
self._h_type_implies_unix_fd = h_type_implies_unix_fd
|
||||||
|
|
||||||
|
self._parser.Parse(xml_data)
|
||||||
|
|
||||||
|
COMMENT_STATE_BEGIN = "begin"
|
||||||
|
COMMENT_STATE_PARAMS = "params"
|
||||||
|
COMMENT_STATE_BODY = "body"
|
||||||
|
COMMENT_STATE_SKIP = "skip"
|
||||||
|
|
||||||
|
def handle_comment(self, data):
|
||||||
|
comment_state = DBusXMLParser.COMMENT_STATE_BEGIN
|
||||||
|
lines = data.split("\n")
|
||||||
|
symbol = ""
|
||||||
|
body = ""
|
||||||
|
in_para = False
|
||||||
|
params = {}
|
||||||
|
for line in lines:
|
||||||
|
orig_line = line
|
||||||
|
line = line.lstrip()
|
||||||
|
if comment_state == DBusXMLParser.COMMENT_STATE_BEGIN:
|
||||||
|
if len(line) > 0:
|
||||||
|
colon_index = line.find(": ")
|
||||||
|
if colon_index == -1:
|
||||||
|
if line.endswith(":"):
|
||||||
|
symbol = line[0 : len(line) - 1]
|
||||||
|
comment_state = DBusXMLParser.COMMENT_STATE_PARAMS
|
||||||
|
else:
|
||||||
|
comment_state = DBusXMLParser.COMMENT_STATE_SKIP
|
||||||
|
else:
|
||||||
|
symbol = line[0:colon_index]
|
||||||
|
rest_of_line = line[colon_index + 2 :].strip()
|
||||||
|
if len(rest_of_line) > 0:
|
||||||
|
body += rest_of_line + "\n"
|
||||||
|
comment_state = DBusXMLParser.COMMENT_STATE_PARAMS
|
||||||
|
elif comment_state == DBusXMLParser.COMMENT_STATE_PARAMS:
|
||||||
|
if line.startswith("@"):
|
||||||
|
colon_index = line.find(": ")
|
||||||
|
if colon_index == -1:
|
||||||
|
comment_state = DBusXMLParser.COMMENT_STATE_BODY
|
||||||
|
if not in_para:
|
||||||
|
in_para = True
|
||||||
|
body += orig_line + "\n"
|
||||||
|
else:
|
||||||
|
param = line[1:colon_index]
|
||||||
|
docs = line[colon_index + 2 :]
|
||||||
|
params[param] = docs
|
||||||
|
else:
|
||||||
|
comment_state = DBusXMLParser.COMMENT_STATE_BODY
|
||||||
|
if len(line) > 0:
|
||||||
|
if not in_para:
|
||||||
|
in_para = True
|
||||||
|
body += orig_line + "\n"
|
||||||
|
elif comment_state == DBusXMLParser.COMMENT_STATE_BODY:
|
||||||
|
if len(line) > 0:
|
||||||
|
if not in_para:
|
||||||
|
in_para = True
|
||||||
|
body += orig_line + "\n"
|
||||||
|
else:
|
||||||
|
if in_para:
|
||||||
|
body += "\n"
|
||||||
|
in_para = False
|
||||||
|
if in_para:
|
||||||
|
body += "\n"
|
||||||
|
|
||||||
|
if symbol != "":
|
||||||
|
self.doc_comment_last_symbol = symbol
|
||||||
|
self.doc_comment_params = params
|
||||||
|
self.doc_comment_body = body
|
||||||
|
|
||||||
|
def handle_char_data(self, data):
|
||||||
|
# print 'char_data=%s'%data
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_start_element(self, name, attrs):
|
||||||
|
old_state = self.state
|
||||||
|
old_cur_object = self._cur_object
|
||||||
|
if self.state == DBusXMLParser.STATE_IGNORED:
|
||||||
|
self.state = DBusXMLParser.STATE_IGNORED
|
||||||
|
elif self.state == DBusXMLParser.STATE_TOP:
|
||||||
|
if name == DBusXMLParser.STATE_NODE:
|
||||||
|
self.state = DBusXMLParser.STATE_NODE
|
||||||
|
else:
|
||||||
|
self.state = DBusXMLParser.STATE_IGNORED
|
||||||
|
elif self.state == DBusXMLParser.STATE_NODE:
|
||||||
|
if name == DBusXMLParser.STATE_INTERFACE:
|
||||||
|
self.state = DBusXMLParser.STATE_INTERFACE
|
||||||
|
iface = Interface(attrs["name"])
|
||||||
|
self._cur_object = iface
|
||||||
|
self.parsed_interfaces.append(iface)
|
||||||
|
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||||
|
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||||
|
anno = Annotation(attrs["name"], attrs["value"])
|
||||||
|
self._cur_object.annotations.append(anno)
|
||||||
|
self._cur_object = anno
|
||||||
|
else:
|
||||||
|
self.state = DBusXMLParser.STATE_IGNORED
|
||||||
|
|
||||||
|
# assign docs, if any
|
||||||
|
if "name" in attrs and self.doc_comment_last_symbol == attrs["name"]:
|
||||||
|
self._cur_object.doc_string = self.doc_comment_body
|
||||||
|
if "short_description" in self.doc_comment_params:
|
||||||
|
short_description = self.doc_comment_params["short_description"]
|
||||||
|
self._cur_object.doc_string_brief = short_description
|
||||||
|
if "since" in self.doc_comment_params:
|
||||||
|
self._cur_object.since = self.doc_comment_params["since"].strip()
|
||||||
|
|
||||||
|
elif self.state == DBusXMLParser.STATE_INTERFACE:
|
||||||
|
if name == DBusXMLParser.STATE_METHOD:
|
||||||
|
self.state = DBusXMLParser.STATE_METHOD
|
||||||
|
method = Method(
|
||||||
|
attrs["name"], h_type_implies_unix_fd=self._h_type_implies_unix_fd
|
||||||
|
)
|
||||||
|
self._cur_object.methods.append(method)
|
||||||
|
self._cur_object = method
|
||||||
|
elif name == DBusXMLParser.STATE_SIGNAL:
|
||||||
|
self.state = DBusXMLParser.STATE_SIGNAL
|
||||||
|
signal = Signal(attrs["name"])
|
||||||
|
self._cur_object.signals.append(signal)
|
||||||
|
self._cur_object = signal
|
||||||
|
elif name == DBusXMLParser.STATE_PROPERTY:
|
||||||
|
self.state = DBusXMLParser.STATE_PROPERTY
|
||||||
|
prop = Property(attrs["name"], attrs["type"], attrs["access"])
|
||||||
|
self._cur_object.properties.append(prop)
|
||||||
|
self._cur_object = prop
|
||||||
|
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||||
|
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||||
|
anno = Annotation(attrs["name"], attrs["value"])
|
||||||
|
self._cur_object.annotations.append(anno)
|
||||||
|
self._cur_object = anno
|
||||||
|
else:
|
||||||
|
self.state = DBusXMLParser.STATE_IGNORED
|
||||||
|
|
||||||
|
# assign docs, if any
|
||||||
|
if "name" in attrs and self.doc_comment_last_symbol == attrs["name"]:
|
||||||
|
self._cur_object.doc_string = self.doc_comment_body
|
||||||
|
if "since" in self.doc_comment_params:
|
||||||
|
self._cur_object.since = self.doc_comment_params["since"].strip()
|
||||||
|
|
||||||
|
elif self.state == DBusXMLParser.STATE_METHOD:
|
||||||
|
if name == DBusXMLParser.STATE_ARG:
|
||||||
|
self.state = DBusXMLParser.STATE_ARG
|
||||||
|
arg_name = None
|
||||||
|
if "name" in attrs:
|
||||||
|
arg_name = attrs["name"]
|
||||||
|
arg = Arg(arg_name, attrs["type"])
|
||||||
|
direction = attrs.get("direction", "in")
|
||||||
|
if direction == "in":
|
||||||
|
self._cur_object.in_args.append(arg)
|
||||||
|
elif direction == "out":
|
||||||
|
self._cur_object.out_args.append(arg)
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid direction "{}"'.format(direction))
|
||||||
|
self._cur_object = arg
|
||||||
|
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||||
|
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||||
|
anno = Annotation(attrs["name"], attrs["value"])
|
||||||
|
self._cur_object.annotations.append(anno)
|
||||||
|
self._cur_object = anno
|
||||||
|
else:
|
||||||
|
self.state = DBusXMLParser.STATE_IGNORED
|
||||||
|
|
||||||
|
# assign docs, if any
|
||||||
|
if self.doc_comment_last_symbol == old_cur_object.name:
|
||||||
|
if "name" in attrs and attrs["name"] in self.doc_comment_params:
|
||||||
|
doc_string = self.doc_comment_params[attrs["name"]]
|
||||||
|
if doc_string is not None:
|
||||||
|
self._cur_object.doc_string = doc_string
|
||||||
|
if "since" in self.doc_comment_params:
|
||||||
|
self._cur_object.since = self.doc_comment_params[
|
||||||
|
"since"
|
||||||
|
].strip()
|
||||||
|
|
||||||
|
elif self.state == DBusXMLParser.STATE_SIGNAL:
|
||||||
|
if name == DBusXMLParser.STATE_ARG:
|
||||||
|
self.state = DBusXMLParser.STATE_ARG
|
||||||
|
arg_name = None
|
||||||
|
if "name" in attrs:
|
||||||
|
arg_name = attrs["name"]
|
||||||
|
arg = Arg(arg_name, attrs["type"])
|
||||||
|
self._cur_object.args.append(arg)
|
||||||
|
self._cur_object = arg
|
||||||
|
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||||
|
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||||
|
anno = Annotation(attrs["name"], attrs["value"])
|
||||||
|
self._cur_object.annotations.append(anno)
|
||||||
|
self._cur_object = anno
|
||||||
|
else:
|
||||||
|
self.state = DBusXMLParser.STATE_IGNORED
|
||||||
|
|
||||||
|
# assign docs, if any
|
||||||
|
if self.doc_comment_last_symbol == old_cur_object.name:
|
||||||
|
if "name" in attrs and attrs["name"] in self.doc_comment_params:
|
||||||
|
doc_string = self.doc_comment_params[attrs["name"]]
|
||||||
|
if doc_string is not None:
|
||||||
|
self._cur_object.doc_string = doc_string
|
||||||
|
if "since" in self.doc_comment_params:
|
||||||
|
self._cur_object.since = self.doc_comment_params[
|
||||||
|
"since"
|
||||||
|
].strip()
|
||||||
|
|
||||||
|
elif self.state == DBusXMLParser.STATE_PROPERTY:
|
||||||
|
if name == DBusXMLParser.STATE_ANNOTATION:
|
||||||
|
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||||
|
anno = Annotation(attrs["name"], attrs["value"])
|
||||||
|
self._cur_object.annotations.append(anno)
|
||||||
|
self._cur_object = anno
|
||||||
|
else:
|
||||||
|
self.state = DBusXMLParser.STATE_IGNORED
|
||||||
|
|
||||||
|
elif self.state == DBusXMLParser.STATE_ARG:
|
||||||
|
if name == DBusXMLParser.STATE_ANNOTATION:
|
||||||
|
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||||
|
anno = Annotation(attrs["name"], attrs["value"])
|
||||||
|
self._cur_object.annotations.append(anno)
|
||||||
|
self._cur_object = anno
|
||||||
|
else:
|
||||||
|
self.state = DBusXMLParser.STATE_IGNORED
|
||||||
|
|
||||||
|
elif self.state == DBusXMLParser.STATE_ANNOTATION:
|
||||||
|
if name == DBusXMLParser.STATE_ANNOTATION:
|
||||||
|
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||||
|
anno = Annotation(attrs["name"], attrs["value"])
|
||||||
|
self._cur_object.annotations.append(anno)
|
||||||
|
self._cur_object = anno
|
||||||
|
else:
|
||||||
|
self.state = DBusXMLParser.STATE_IGNORED
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'Unhandled state "{}" while entering element with name "{}"'.format(
|
||||||
|
self.state, name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.state_stack.append(old_state)
|
||||||
|
self._cur_object_stack.append(old_cur_object)
|
||||||
|
|
||||||
|
def handle_end_element(self, name):
|
||||||
|
self.state = self.state_stack.pop()
|
||||||
|
self._cur_object = self._cur_object_stack.pop()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dbus_xml(xml_data):
|
||||||
|
parser = DBusXMLParser(xml_data, True)
|
||||||
|
return parser.parsed_interfaces
|
|
@ -0,0 +1,25 @@
|
||||||
|
# D-Bus XML documentation extension, compatibility gunk for <sphinx4
|
||||||
|
#
|
||||||
|
# Copyright (C) 2021, Red Hat Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#
|
||||||
|
# Author: Marc-André Lureau <marcandre.lureau@redhat.com>
|
||||||
|
"""dbus-doc is a Sphinx extension that provides documentation from D-Bus XML."""
|
||||||
|
|
||||||
|
from sphinx.application import Sphinx
|
||||||
|
from sphinx.util.docutils import SphinxDirective
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDBusDocDirective(SphinxDirective):
|
||||||
|
has_content = True
|
||||||
|
required_arguments = 1
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app: Sphinx) -> Dict[str, Any]:
|
||||||
|
"""Register a fake dbus-doc directive with Sphinx"""
|
||||||
|
app.add_directive("dbus-doc", FakeDBusDocDirective)
|
Loading…
Reference in New Issue