123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029 |
- """
- QAPI domain extension.
- """
- # The best laid plans of mice and men, ...
- # pylint: disable=too-many-lines
- from __future__ import annotations
- import re
- from typing import (
- TYPE_CHECKING,
- List,
- NamedTuple,
- Tuple,
- cast,
- )
- from docutils import nodes
- from docutils.parsers.rst import directives
- from compat import (
- CompatField,
- CompatGroupedField,
- CompatTypedField,
- KeywordNode,
- ParserFix,
- Signature,
- SpaceNode,
- )
- 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 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, []
- class QAPIDescription(ParserFix):
- """
- 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:
- # 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:
- # 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
- CompatGroupedField(
- "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 [
- KeywordNode("", self.objtype.title()),
- SpaceNode(" "),
- ]
- 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 += [
- SpaceNode(" "),
- 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, contentnode: 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:
- contentnode.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, 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(CompatTypedField):
- 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
- CompatField(
- "error",
- label=_("Errors"),
- names=("error", "errors"),
- has_arg=False,
- ),
- # :return TypeName: descr
- CompatGroupedField(
- "returnvalue",
- label=_("Return"),
- rolename="type",
- names=("return",),
- can_collapse=True,
- ),
- ]
- )
- class QAPIEnum(QAPIObject):
- """Description of a QAPI Enum."""
- doc_field_types = QAPIObject.doc_field_types.copy()
- doc_field_types.extend(
- [
- # :value name: descr
- CompatGroupedField(
- "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
- CompatTypedField(
- "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")
- 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
- # list of all object (name, ObjectEntry) pairs, sorted by name
- # (ignoring the module)
- objects = sorted(
- self.domain.objects.items(),
- key=lambda x: x[0].split(".")[-1].lower(),
- )
- for objname, obj in objects:
- if docnames and obj.docname not in docnames:
- continue
- # Strip the module name out:
- objname = objname.split(".")[-1]
- # Add an alphabetical entry:
- entries = content.setdefault(objname[0].upper(), [])
- entries.append(
- IndexEntry(
- objname, 0, obj.docname, obj.node_id, obj.objtype, "", ""
- )
- )
- # Add a categorical entry:
- category = obj.objtype.title() + "s"
- entries = content.setdefault(category, [])
- entries.append(
- IndexEntry(objname, 0, obj.docname, obj.node_id, "", "", "")
- )
- # alphabetically sort categories; 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 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_domain(QAPIDomain)
- return {
- "version": "1.0",
- "env_version": 1,
- "parallel_read_safe": True,
- "parallel_write_safe": True,
- }
|