| """ | |
| QAPI domain extension. | |
| """ | |
| # The best laid plans of mice and men, ... | |
| # pylint: disable=too-many-lines | |
| from __future__ import annotations | |
| import re | |
| import types | |
| from typing import ( | |
| TYPE_CHECKING, | |
| List, | |
| NamedTuple, | |
| Tuple, | |
| Type, | |
| cast, | |
| ) | |
| from docutils import nodes | |
| from docutils.parsers.rst import directives | |
| from sphinx import addnodes | |
| 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 docfields, logging | |
| from sphinx.util.docutils import SphinxDirective | |
| from sphinx.util.nodes import make_id, make_refnode | |
| if TYPE_CHECKING: | |
| from typing import ( | |
| AbstractSet, | |
| Any, | |
| Dict, | |
| Iterable, | |
| Optional, | |
| Union, | |
| ) | |
| from docutils.nodes import Element, Node | |
| from sphinx.addnodes import desc_signature, pending_xref | |
| from sphinx.application import Sphinx | |
| from sphinx.builders import Builder | |
| from sphinx.environment import BuildEnvironment | |
| from sphinx.util.typing import OptionSpec | |
| logger = logging.getLogger(__name__) | |
| def _unpack_field( | |
| field: nodes.Node, | |
| ) -> Tuple[nodes.field_name, nodes.field_body]: | |
| """ | |
| docutils helper: unpack a field node in a type-safe manner. | |
| """ | |
| assert isinstance(field, nodes.field) | |
| assert len(field.children) == 2 | |
| assert isinstance(field.children[0], nodes.field_name) | |
| assert isinstance(field.children[1], nodes.field_body) | |
| return (field.children[0], field.children[1]) | |
| class ObjectEntry(NamedTuple): | |
| docname: str | |
| node_id: str | |
| objtype: str | |
| aliased: bool | |
| class QAPIXRefRole(XRefRole): | |
| def process_link( | |
| self, | |
| env: BuildEnvironment, | |
| refnode: Element, | |
| has_explicit_title: bool, | |
| title: str, | |
| target: str, | |
| ) -> tuple[str, str]: | |
| refnode["qapi:namespace"] = env.ref_context.get("qapi:namespace") | |
| refnode["qapi:module"] = env.ref_context.get("qapi:module") | |
| # Cross-references that begin with a tilde adjust the title to | |
| # only show the reference without a leading module, even if one | |
| # was provided. This is a Sphinx-standard syntax; give it | |
| # priority over QAPI-specific type markup below. | |
| hide_module = False | |
| if target.startswith("~"): | |
| hide_module = True | |
| target = target[1:] | |
| # Type names that end with "?" are considered optional | |
| # arguments and should be documented as such, but it's not | |
| # part of the xref itself. | |
| if target.endswith("?"): | |
| refnode["qapi:optional"] = True | |
| target = target[:-1] | |
| # Type names wrapped in brackets denote lists. strip the | |
| # brackets and remember to add them back later. | |
| if target.startswith("[") and target.endswith("]"): | |
| refnode["qapi:array"] = True | |
| target = target[1:-1] | |
| if has_explicit_title: | |
| # Don't mess with the title at all if it was explicitly set. | |
| # Explicit title syntax for references is e.g. | |
| # :qapi:type:`target <explicit title>` | |
| # and this explicit title overrides everything else here. | |
| return title, target | |
| title = target | |
| if hide_module: | |
| title = target.split(".")[-1] | |
| return title, target | |
| def result_nodes( | |
| self, | |
| document: nodes.document, | |
| env: BuildEnvironment, | |
| node: Element, | |
| is_ref: bool, | |
| ) -> Tuple[List[nodes.Node], List[nodes.system_message]]: | |
| # node here is the pending_xref node (or whatever nodeclass was | |
| # configured at XRefRole class instantiation time). | |
| results: List[nodes.Node] = [node] | |
| if node.get("qapi:array"): | |
| results.insert(0, nodes.literal("[", "[")) | |
| results.append(nodes.literal("]", "]")) | |
| if node.get("qapi:optional"): | |
| results.append(nodes.Text(", ")) | |
| results.append(nodes.emphasis("?", "optional")) | |
| return results, [] | |
| Signature = str | |
| class QAPIDescription(ObjectDescription[Signature]): | |
| """ | |
| Generic QAPI description. | |
| This is meant to be an abstract class, not instantiated | |
| directly. This class handles the abstract details of indexing, the | |
| TOC, and reference targets for QAPI descriptions. | |
| """ | |
| def handle_signature(self, sig: str, signode: desc_signature) -> Signature: | |
| # pylint: disable=unused-argument | |
| # Do nothing. The return value here is the "name" of the entity | |
| # being documented; for QAPI, this is the same as the | |
| # "signature", which is just a name. | |
| # Normally this method must also populate signode with nodes to | |
| # render the signature; here we do nothing instead - the | |
| # subclasses will handle this. | |
| return sig | |
| def get_index_text(self, name: Signature) -> Tuple[str, str]: | |
| """Return the text for the index entry of the object.""" | |
| # NB: this is used for the global index, not the QAPI index. | |
| return ("single", f"{name} (QMP {self.objtype})") | |
| def _get_context(self) -> Tuple[str, str]: | |
| namespace = self.options.get( | |
| "namespace", self.env.ref_context.get("qapi:namespace", "") | |
| ) | |
| modname = self.options.get( | |
| "module", self.env.ref_context.get("qapi:module", "") | |
| ) | |
| return namespace, modname | |
| def _get_fqn(self, name: Signature) -> str: | |
| namespace, modname = self._get_context() | |
| # If we're documenting a module, don't include the module as | |
| # part of the FQN; we ARE the module! | |
| if self.objtype == "module": | |
| modname = "" | |
| if modname: | |
| name = f"{modname}.{name}" | |
| if namespace: | |
| name = f"{namespace}:{name}" | |
| return name | |
| def add_target_and_index( | |
| self, name: Signature, sig: str, signode: desc_signature | |
| ) -> None: | |
| # pylint: disable=unused-argument | |
| # name is the return value of handle_signature. | |
| # sig is the original, raw text argument to handle_signature. | |
| # For QAPI, these are identical, currently. | |
| assert self.objtype | |
| if not (fullname := signode.get("fullname", "")): | |
| fullname = self._get_fqn(name) | |
| node_id = make_id( | |
| self.env, self.state.document, self.objtype, fullname | |
| ) | |
| signode["ids"].append(node_id) | |
| self.state.document.note_explicit_target(signode) | |
| domain = cast(QAPIDomain, self.env.get_domain("qapi")) | |
| domain.note_object(fullname, self.objtype, node_id, location=signode) | |
| if "no-index-entry" not in self.options: | |
| arity, indextext = self.get_index_text(name) | |
| assert self.indexnode is not None | |
| if indextext: | |
| self.indexnode["entries"].append( | |
| (arity, indextext, node_id, "", None) | |
| ) | |
| @staticmethod | |
| def split_fqn(name: str) -> Tuple[str, str, str]: | |
| if ":" in name: | |
| ns, name = name.split(":") | |
| else: | |
| ns = "" | |
| if "." in name: | |
| module, name = name.split(".") | |
| else: | |
| module = "" | |
| return (ns, module, name) | |
| def _object_hierarchy_parts( | |
| self, sig_node: desc_signature | |
| ) -> Tuple[str, ...]: | |
| if "fullname" not in sig_node: | |
| return () | |
| return self.split_fqn(sig_node["fullname"]) | |
| def _toc_entry_name(self, sig_node: desc_signature) -> str: | |
| # This controls the name in the TOC and on the sidebar. | |
| # This is the return type of _object_hierarchy_parts(). | |
| toc_parts = cast(Tuple[str, ...], sig_node.get("_toc_parts", ())) | |
| if not toc_parts: | |
| return "" | |
| config = self.env.app.config | |
| namespace, modname, name = toc_parts | |
| if config.toc_object_entries_show_parents == "domain": | |
| ret = name | |
| if modname and modname != self.env.ref_context.get( | |
| "qapi:module", "" | |
| ): | |
| ret = f"{modname}.{name}" | |
| if namespace and namespace != self.env.ref_context.get( | |
| "qapi:namespace", "" | |
| ): | |
| ret = f"{namespace}:{ret}" | |
| return ret | |
| if config.toc_object_entries_show_parents == "hide": | |
| return name | |
| if config.toc_object_entries_show_parents == "all": | |
| return sig_node.get("fullname", name) | |
| return "" | |
| class QAPIObject(QAPIDescription): | |
| """ | |
| Description of a generic QAPI object. | |
| It's not used directly, but is instead subclassed by specific directives. | |
| """ | |
| # Inherit some standard options from Sphinx's ObjectDescription | |
| option_spec: OptionSpec = ( # type:ignore[misc] | |
| ObjectDescription.option_spec.copy() | |
| ) | |
| option_spec.update( | |
| { | |
| # Context overrides: | |
| "namespace": directives.unchanged, | |
| "module": directives.unchanged, | |
| # These are QAPI originals: | |
| "since": directives.unchanged, | |
| "ifcond": directives.unchanged, | |
| "deprecated": directives.flag, | |
| "unstable": directives.flag, | |
| } | |
| ) | |
| doc_field_types = [ | |
| # :feat name: descr | |
| docfields.GroupedField( | |
| "feature", | |
| label=_("Features"), | |
| names=("feat",), | |
| can_collapse=False, | |
| ), | |
| ] | |
| def get_signature_prefix(self) -> List[nodes.Node]: | |
| """Return a prefix to put before the object name in the signature.""" | |
| assert self.objtype | |
| return [ | |
| addnodes.desc_sig_keyword("", self.objtype.title()), | |
| addnodes.desc_sig_space(" "), | |
| ] | |
| def get_signature_suffix(self) -> List[nodes.Node]: | |
| """Return a suffix to put after the object name in the signature.""" | |
| ret: List[nodes.Node] = [] | |
| if "since" in self.options: | |
| ret += [ | |
| addnodes.desc_sig_space(" "), | |
| addnodes.desc_sig_element( | |
| "", f"(Since: {self.options['since']})" | |
| ), | |
| ] | |
| return ret | |
| def handle_signature(self, sig: str, signode: desc_signature) -> Signature: | |
| """ | |
| Transform a QAPI definition name into RST nodes. | |
| This method was originally intended for handling function | |
| signatures. In the QAPI domain, however, we only pass the | |
| definition name as the directive argument and handle everything | |
| else in the content body with field lists. | |
| As such, the only argument here is "sig", which is just the QAPI | |
| definition name. | |
| """ | |
| # No module or domain info allowed in the signature! | |
| assert ":" not in sig | |
| assert "." not in sig | |
| namespace, modname = self._get_context() | |
| signode["fullname"] = self._get_fqn(sig) | |
| signode["namespace"] = namespace | |
| signode["module"] = modname | |
| sig_prefix = self.get_signature_prefix() | |
| if sig_prefix: | |
| signode += addnodes.desc_annotation( | |
| str(sig_prefix), "", *sig_prefix | |
| ) | |
| signode += addnodes.desc_name(sig, sig) | |
| signode += self.get_signature_suffix() | |
| return sig | |
| def _add_infopips(self, content_node: addnodes.desc_content) -> None: | |
| # Add various eye-catches and things that go below the signature | |
| # bar, but precede the user-defined content. | |
| infopips = nodes.container() | |
| infopips.attributes["classes"].append("qapi-infopips") | |
| def _add_pip( | |
| source: str, content: Union[str, List[nodes.Node]], classname: str | |
| ) -> None: | |
| node = nodes.container(source) | |
| if isinstance(content, str): | |
| node.append(nodes.Text(content)) | |
| else: | |
| node.extend(content) | |
| node.attributes["classes"].extend(["qapi-infopip", classname]) | |
| infopips.append(node) | |
| if "deprecated" in self.options: | |
| _add_pip( | |
| ":deprecated:", | |
| f"This {self.objtype} is deprecated.", | |
| "qapi-deprecated", | |
| ) | |
| if "unstable" in self.options: | |
| _add_pip( | |
| ":unstable:", | |
| f"This {self.objtype} is unstable/experimental.", | |
| "qapi-unstable", | |
| ) | |
| if self.options.get("ifcond", ""): | |
| ifcond = self.options["ifcond"] | |
| _add_pip( | |
| f":ifcond: {ifcond}", | |
| [ | |
| nodes.emphasis("", "Availability"), | |
| nodes.Text(": "), | |
| nodes.literal(ifcond, ifcond), | |
| ], | |
| "qapi-ifcond", | |
| ) | |
| if infopips.children: | |
| content_node.insert(0, infopips) | |
| def _validate_field(self, field: nodes.field) -> None: | |
| """Validate field lists in this QAPI Object Description.""" | |
| name, _ = _unpack_field(field) | |
| allowed_fields = set(self.env.app.config.qapi_allowed_fields) | |
| field_label = name.astext() | |
| if field_label in allowed_fields: | |
| # Explicitly allowed field list name, OK. | |
| return | |
| try: | |
| # split into field type and argument (if provided) | |
| # e.g. `:arg type name: descr` is | |
| # field_type = "arg", field_arg = "type name". | |
| field_type, field_arg = field_label.split(None, 1) | |
| except ValueError: | |
| # No arguments provided | |
| field_type = field_label | |
| field_arg = "" | |
| typemap = self.get_field_type_map() | |
| if field_type in typemap: | |
| # This is a special docfield, yet-to-be-processed. Catch | |
| # correct names, but incorrect arguments. This mismatch WILL | |
| # cause Sphinx to render this field incorrectly (without a | |
| # warning), which is never what we want. | |
| typedesc = typemap[field_type][0] | |
| if typedesc.has_arg != bool(field_arg): | |
| msg = f"docfield field list type {field_type!r} " | |
| if typedesc.has_arg: | |
| msg += "requires an argument." | |
| else: | |
| msg += "takes no arguments." | |
| logger.warning(msg, location=field) | |
| else: | |
| # This is unrecognized entirely. It's valid rST to use | |
| # arbitrary fields, but let's ensure the documentation | |
| # writer has done this intentionally. | |
| valid = ", ".join(sorted(set(typemap) | allowed_fields)) | |
| msg = ( | |
| f"Unrecognized field list name {field_label!r}.\n" | |
| f"Valid fields for qapi:{self.objtype} are: {valid}\n" | |
| "\n" | |
| "If this usage is intentional, please add it to " | |
| "'qapi_allowed_fields' in docs/conf.py." | |
| ) | |
| logger.warning(msg, location=field) | |
| def transform_content( | |
| self, | |
| # Sphinx changed the name of the content_node parameter | |
| # sometime after Sphinx 7.2.6, so no matter which name we | |
| # choose, either our minimum tests or our bleeding edge tests | |
| # will complain about the parameter rename. Use the eventual | |
| # name and silence the error for our minreq tests. | |
| # | |
| # pylint: disable=arguments-renamed | |
| content_node: addnodes.desc_content | |
| ) -> None: | |
| # This hook runs after before_content and the nested parse, but | |
| # before the DocFieldTransformer is executed. | |
| super().transform_content(content_node) | |
| self._add_infopips(content_node) | |
| # Validate field lists. | |
| for child in content_node: | |
| if isinstance(child, nodes.field_list): | |
| for field in child.children: | |
| assert isinstance(field, nodes.field) | |
| self._validate_field(field) | |
| class SpecialTypedField(docfields.TypedField): | |
| def make_field(self, *args: Any, **kwargs: Any) -> nodes.field: | |
| ret = super().make_field(*args, **kwargs) | |
| # Look for the characteristic " -- " text node that Sphinx | |
| # inserts for each TypedField entry ... | |
| for node in ret.traverse(lambda n: str(n) == " -- "): | |
| par = node.parent | |
| if par.children[0].astext() != "q_dummy": | |
| continue | |
| # If the first node's text is q_dummy, this is a dummy | |
| # field we want to strip down to just its contents. | |
| del par.children[:-1] | |
| return ret | |
| class QAPICommand(QAPIObject): | |
| """Description of a QAPI Command.""" | |
| doc_field_types = QAPIObject.doc_field_types.copy() | |
| doc_field_types.extend( | |
| [ | |
| # :arg TypeName ArgName: descr | |
| SpecialTypedField( | |
| "argument", | |
| label=_("Arguments"), | |
| names=("arg",), | |
| typerolename="type", | |
| can_collapse=False, | |
| ), | |
| # :error: descr | |
| docfields.Field( | |
| "error", | |
| label=_("Errors"), | |
| names=("error", "errors"), | |
| has_arg=False, | |
| ), | |
| # :return TypeName: descr | |
| docfields.GroupedField( | |
| "returnvalue", | |
| label=_("Return"), | |
| rolename="type", | |
| names=("return",), | |
| can_collapse=True, | |
| ), | |
| # :return-nodesc: TypeName | |
| docfields.Field( | |
| "returnvalue", | |
| label=_("Return"), | |
| names=("return-nodesc",), | |
| bodyrolename="type", | |
| has_arg=False, | |
| ), | |
| ] | |
| ) | |
| class QAPIEnum(QAPIObject): | |
| """Description of a QAPI Enum.""" | |
| doc_field_types = QAPIObject.doc_field_types.copy() | |
| doc_field_types.extend( | |
| [ | |
| # :value name: descr | |
| docfields.GroupedField( | |
| "value", | |
| label=_("Values"), | |
| names=("value",), | |
| can_collapse=False, | |
| ) | |
| ] | |
| ) | |
| class QAPIAlternate(QAPIObject): | |
| """Description of a QAPI Alternate.""" | |
| doc_field_types = QAPIObject.doc_field_types.copy() | |
| doc_field_types.extend( | |
| [ | |
| # :alt type name: descr | |
| docfields.TypedField( | |
| "alternative", | |
| label=_("Alternatives"), | |
| names=("alt",), | |
| typerolename="type", | |
| can_collapse=False, | |
| ), | |
| ] | |
| ) | |
| class QAPIObjectWithMembers(QAPIObject): | |
| """Base class for Events/Structs/Unions""" | |
| doc_field_types = QAPIObject.doc_field_types.copy() | |
| doc_field_types.extend( | |
| [ | |
| # :member type name: descr | |
| SpecialTypedField( | |
| "member", | |
| label=_("Members"), | |
| names=("memb",), | |
| typerolename="type", | |
| can_collapse=False, | |
| ), | |
| ] | |
| ) | |
| class QAPIEvent(QAPIObjectWithMembers): | |
| # pylint: disable=too-many-ancestors | |
| """Description of a QAPI Event.""" | |
| class QAPIJSONObject(QAPIObjectWithMembers): | |
| # pylint: disable=too-many-ancestors | |
| """Description of a QAPI Object: structs and unions.""" | |
| class QAPIModule(QAPIDescription): | |
| """ | |
| Directive to mark description of a new module. | |
| This directive doesn't generate any special formatting, and is just | |
| a pass-through for the content body. Named section titles are | |
| allowed in the content body. | |
| Use this directive to create entries for the QAPI module in the | |
| global index and the QAPI index; as well as to associate subsequent | |
| definitions with the module they are defined in for purposes of | |
| search and QAPI index organization. | |
| :arg: The name of the module. | |
| :opt no-index: Don't add cross-reference targets or index entries. | |
| :opt no-typesetting: Don't render the content body (but preserve any | |
| cross-reference target IDs in the squelched output.) | |
| Example:: | |
| .. qapi:module:: block-core | |
| :no-index: | |
| :no-typesetting: | |
| Lorem ipsum, dolor sit amet ... | |
| """ | |
| def run(self) -> List[Node]: | |
| modname = self.arguments[0].strip() | |
| self.env.ref_context["qapi:module"] = modname | |
| ret = super().run() | |
| # ObjectDescription always creates a visible signature bar. We | |
| # want module items to be "invisible", however. | |
| # Extract the content body of the directive: | |
| assert isinstance(ret[-1], addnodes.desc) | |
| desc_node = ret.pop(-1) | |
| assert isinstance(desc_node.children[1], addnodes.desc_content) | |
| ret.extend(desc_node.children[1].children) | |
| # Re-home node_ids so anchor refs still work: | |
| node_ids: List[str] | |
| if node_ids := [ | |
| node_id | |
| for el in desc_node.children[0].traverse(nodes.Element) | |
| for node_id in cast(List[str], el.get("ids", ())) | |
| ]: | |
| target_node = nodes.target(ids=node_ids) | |
| ret.insert(1, target_node) | |
| return ret | |
| class QAPINamespace(SphinxDirective): | |
| has_content = False | |
| required_arguments = 1 | |
| def run(self) -> List[Node]: | |
| namespace = self.arguments[0].strip() | |
| self.env.ref_context["qapi:namespace"] = namespace | |
| return [] | |
| class QAPIIndex(Index): | |
| """ | |
| Index subclass to provide the QAPI definition index. | |
| """ | |
| # pylint: disable=too-few-public-methods | |
| name = "index" | |
| localname = _("QAPI Index") | |
| shortname = _("QAPI Index") | |
| namespace = "" | |
| def generate( | |
| self, | |
| docnames: Optional[Iterable[str]] = None, | |
| ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]: | |
| assert isinstance(self.domain, QAPIDomain) | |
| content: Dict[str, List[IndexEntry]] = {} | |
| collapse = False | |
| for objname, obj in self.domain.objects.items(): | |
| if docnames and obj.docname not in docnames: | |
| continue | |
| ns, _mod, name = QAPIDescription.split_fqn(objname) | |
| if self.namespace != ns: | |
| continue | |
| # Add an alphabetical entry: | |
| entries = content.setdefault(name[0].upper(), []) | |
| entries.append( | |
| IndexEntry( | |
| name, 0, obj.docname, obj.node_id, obj.objtype, "", "" | |
| ) | |
| ) | |
| # Add a categorical entry: | |
| category = obj.objtype.title() + "s" | |
| entries = content.setdefault(category, []) | |
| entries.append( | |
| IndexEntry(name, 0, obj.docname, obj.node_id, "", "", "") | |
| ) | |
| # Sort entries within each category alphabetically | |
| for category in content: | |
| content[category] = sorted(content[category]) | |
| # Sort the categories themselves; type names first, ABC entries last. | |
| sorted_content = sorted( | |
| content.items(), | |
| key=lambda x: (len(x[0]) == 1, x[0]), | |
| ) | |
| return sorted_content, collapse | |
| class QAPIDomain(Domain): | |
| """QAPI language domain.""" | |
| name = "qapi" | |
| label = "QAPI" | |
| # This table associates cross-reference object types (key) with an | |
| # ObjType instance, which defines the valid cross-reference roles | |
| # for each object type. | |
| # | |
| # e.g., the :qapi:type: cross-reference role can refer to enum, | |
| # struct, union, or alternate objects; but :qapi:obj: can refer to | |
| # anything. Each object also gets its own targeted cross-reference role. | |
| object_types: Dict[str, ObjType] = { | |
| "module": ObjType(_("module"), "mod", "any"), | |
| "command": ObjType(_("command"), "cmd", "any"), | |
| "event": ObjType(_("event"), "event", "any"), | |
| "enum": ObjType(_("enum"), "enum", "type", "any"), | |
| "object": ObjType(_("object"), "obj", "type", "any"), | |
| "alternate": ObjType(_("alternate"), "alt", "type", "any"), | |
| } | |
| # Each of these provides a rST directive, | |
| # e.g. .. qapi:module:: block-core | |
| directives = { | |
| "namespace": QAPINamespace, | |
| "module": QAPIModule, | |
| "command": QAPICommand, | |
| "event": QAPIEvent, | |
| "enum": QAPIEnum, | |
| "object": QAPIJSONObject, | |
| "alternate": QAPIAlternate, | |
| } | |
| # These are all cross-reference roles; e.g. | |
| # :qapi:cmd:`query-block`. The keys correlate to the names used in | |
| # the object_types table values above. | |
| roles = { | |
| "mod": QAPIXRefRole(), | |
| "cmd": QAPIXRefRole(), | |
| "event": QAPIXRefRole(), | |
| "enum": QAPIXRefRole(), | |
| "obj": QAPIXRefRole(), # specifically structs and unions. | |
| "alt": QAPIXRefRole(), | |
| # reference any data type (excludes modules, commands, events) | |
| "type": QAPIXRefRole(), | |
| "any": QAPIXRefRole(), # reference *any* type of QAPI object. | |
| } | |
| # Moved into the data property at runtime; | |
| # this is the internal index of reference-able objects. | |
| initial_data: Dict[str, Dict[str, Tuple[Any]]] = { | |
| "objects": {}, # fullname -> ObjectEntry | |
| } | |
| # Index pages to generate; each entry is an Index class. | |
| indices = [ | |
| QAPIIndex, | |
| ] | |
| @property | |
| def objects(self) -> Dict[str, ObjectEntry]: | |
| ret = self.data.setdefault("objects", {}) | |
| return ret # type: ignore[no-any-return] | |
| def setup(self) -> None: | |
| namespaces = set(self.env.app.config.qapi_namespaces) | |
| for namespace in namespaces: | |
| new_index: Type[QAPIIndex] = types.new_class( | |
| f"{namespace}Index", bases=(QAPIIndex,) | |
| ) | |
| new_index.name = f"{namespace.lower()}-index" | |
| new_index.localname = _(f"{namespace} Index") | |
| new_index.shortname = _(f"{namespace} Index") | |
| new_index.namespace = namespace | |
| self.indices.append(new_index) | |
| super().setup() | |
| def note_object( | |
| self, | |
| name: str, | |
| objtype: str, | |
| node_id: str, | |
| aliased: bool = False, | |
| location: Any = None, | |
| ) -> None: | |
| """Note a QAPI object for cross reference.""" | |
| if name in self.objects: | |
| other = self.objects[name] | |
| if other.aliased and aliased is False: | |
| # The original definition found. Override it! | |
| pass | |
| elif other.aliased is False and aliased: | |
| # The original definition is already registered. | |
| return | |
| else: | |
| # duplicated | |
| logger.warning( | |
| __( | |
| "duplicate object description of %s, " | |
| "other instance in %s, use :no-index: for one of them" | |
| ), | |
| name, | |
| other.docname, | |
| location=location, | |
| ) | |
| self.objects[name] = ObjectEntry( | |
| self.env.docname, node_id, objtype, aliased | |
| ) | |
| def clear_doc(self, docname: str) -> None: | |
| for fullname, obj in list(self.objects.items()): | |
| if obj.docname == docname: | |
| del self.objects[fullname] | |
| def merge_domaindata( | |
| self, docnames: AbstractSet[str], otherdata: Dict[str, Any] | |
| ) -> None: | |
| for fullname, obj in otherdata["objects"].items(): | |
| if obj.docname in docnames: | |
| # Sphinx's own python domain doesn't appear to bother to | |
| # check for collisions. Assert they don't happen and | |
| # we'll fix it if/when the case arises. | |
| assert fullname not in self.objects, ( | |
| "bug - collision on merge?" | |
| f" {fullname=} {obj=} {self.objects[fullname]=}" | |
| ) | |
| self.objects[fullname] = obj | |
| def find_obj( | |
| self, namespace: str, modname: str, name: str, typ: Optional[str] | |
| ) -> List[Tuple[str, ObjectEntry]]: | |
| """ | |
| Find a QAPI object for "name", maybe using contextual information. | |
| Returns a list of (name, object entry) tuples. | |
| :param namespace: The current namespace context (if any!) under | |
| which we are searching. | |
| :param modname: The current module context (if any!) under | |
| which we are searching. | |
| :param name: The name of the x-ref to resolve; may or may not | |
| include leading context. | |
| :param type: The role name of the x-ref we're resolving, if | |
| provided. This is absent for "any" role lookups. | |
| """ | |
| if not name: | |
| return [] | |
| # ## | |
| # what to search for | |
| # ## | |
| parts = list(QAPIDescription.split_fqn(name)) | |
| explicit = tuple(bool(x) for x in parts) | |
| # Fill in the blanks where possible: | |
| if namespace and not parts[0]: | |
| parts[0] = namespace | |
| if modname and not parts[1]: | |
| parts[1] = modname | |
| implicit_fqn = "" | |
| if all(parts): | |
| implicit_fqn = f"{parts[0]}:{parts[1]}.{parts[2]}" | |
| if typ is None: | |
| # :any: lookup, search everything: | |
| objtypes: List[str] = list(self.object_types) | |
| else: | |
| # type is specified and will be a role (e.g. obj, mod, cmd) | |
| # convert this to eligible object types (e.g. command, module) | |
| # using the QAPIDomain.object_types table. | |
| objtypes = self.objtypes_for_role(typ, []) | |
| # ## | |
| # search! | |
| # ## | |
| def _search(needle: str) -> List[str]: | |
| if ( | |
| needle | |
| and needle in self.objects | |
| and self.objects[needle].objtype in objtypes | |
| ): | |
| return [needle] | |
| return [] | |
| if found := _search(name): | |
| # Exact match! | |
| pass | |
| elif found := _search(implicit_fqn): | |
| # Exact match using contextual information to fill in the gaps. | |
| pass | |
| else: | |
| # No exact hits, perform applicable fuzzy searches. | |
| searches = [] | |
| esc = tuple(re.escape(s) for s in parts) | |
| # Try searching for ns:*.name or ns:name | |
| if explicit[0] and not explicit[1]: | |
| searches.append(f"^{esc[0]}:([^\\.]+\\.)?{esc[2]}$") | |
| # Try searching for *:module.name or module.name | |
| if explicit[1] and not explicit[0]: | |
| searches.append(f"(^|:){esc[1]}\\.{esc[2]}$") | |
| # Try searching for context-ns:*.name or context-ns:name | |
| if parts[0] and not (explicit[0] or explicit[1]): | |
| searches.append(f"^{esc[0]}:([^\\.]+\\.)?{esc[2]}$") | |
| # Try searching for *:context-mod.name or context-mod.name | |
| if parts[1] and not (explicit[0] or explicit[1]): | |
| searches.append(f"(^|:){esc[1]}\\.{esc[2]}$") | |
| # Try searching for *:name, *.name, or name | |
| if not (explicit[0] or explicit[1]): | |
| searches.append(f"(^|:|\\.){esc[2]}$") | |
| for search in searches: | |
| if found := [ | |
| oname | |
| for oname in self.objects | |
| if re.search(search, oname) | |
| and self.objects[oname].objtype in objtypes | |
| ]: | |
| break | |
| matches = [(oname, self.objects[oname]) for oname in found] | |
| if len(matches) > 1: | |
| matches = [m for m in matches if not m[1].aliased] | |
| return matches | |
| def resolve_xref( | |
| self, | |
| env: BuildEnvironment, | |
| fromdocname: str, | |
| builder: Builder, | |
| typ: str, | |
| target: str, | |
| node: pending_xref, | |
| contnode: Element, | |
| ) -> nodes.reference | None: | |
| namespace = node.get("qapi:namespace") | |
| modname = node.get("qapi:module") | |
| matches = self.find_obj(namespace, modname, target, typ) | |
| if not matches: | |
| # Normally, we could pass warn_dangling=True to QAPIXRefRole(), | |
| # but that will trigger on references to these built-in types, | |
| # which we'd like to ignore instead. | |
| # Take care of that warning here instead, so long as the | |
| # reference isn't to one of our built-in core types. | |
| if target not in ( | |
| "string", | |
| "number", | |
| "int", | |
| "boolean", | |
| "null", | |
| "value", | |
| "q_empty", | |
| ): | |
| logger.warning( | |
| __("qapi:%s reference target not found: %r"), | |
| typ, | |
| target, | |
| type="ref", | |
| subtype="qapi", | |
| location=node, | |
| ) | |
| return None | |
| if len(matches) > 1: | |
| logger.warning( | |
| __("more than one target found for cross-reference %r: %s"), | |
| target, | |
| ", ".join(match[0] for match in matches), | |
| type="ref", | |
| subtype="qapi", | |
| location=node, | |
| ) | |
| name, obj = matches[0] | |
| return make_refnode( | |
| builder, fromdocname, obj.docname, obj.node_id, contnode, name | |
| ) | |
| def resolve_any_xref( | |
| self, | |
| env: BuildEnvironment, | |
| fromdocname: str, | |
| builder: Builder, | |
| target: str, | |
| node: pending_xref, | |
| contnode: Element, | |
| ) -> List[Tuple[str, nodes.reference]]: | |
| results: List[Tuple[str, nodes.reference]] = [] | |
| matches = self.find_obj( | |
| node.get("qapi:namespace"), node.get("qapi:module"), target, None | |
| ) | |
| for name, obj in matches: | |
| rolename = self.role_for_objtype(obj.objtype) | |
| assert rolename is not None | |
| role = f"qapi:{rolename}" | |
| refnode = make_refnode( | |
| builder, fromdocname, obj.docname, obj.node_id, contnode, name | |
| ) | |
| results.append((role, refnode)) | |
| return results | |
| def setup(app: Sphinx) -> Dict[str, Any]: | |
| app.setup_extension("sphinx.directives") | |
| app.add_config_value( | |
| "qapi_allowed_fields", | |
| set(), | |
| "env", # Setting impacts parsing phase | |
| types=set, | |
| ) | |
| app.add_config_value( | |
| "qapi_namespaces", | |
| set(), | |
| "env", | |
| types=set, | |
| ) | |
| app.add_domain(QAPIDomain) | |
| return { | |
| "version": "1.0", | |
| "env_version": 1, | |
| "parallel_read_safe": True, | |
| "parallel_write_safe": True, | |
| } |