1""" 2QAPI domain extension. 3""" 4 5from __future__ import annotations 6 7from typing import ( 8 TYPE_CHECKING, 9 AbstractSet, 10 Any, 11 Dict, 12 Iterable, 13 List, 14 NamedTuple, 15 Optional, 16 Tuple, 17) 18 19from sphinx.domains import ( 20 Domain, 21 Index, 22 IndexEntry, 23 ObjType, 24) 25from sphinx.locale import _, __ 26from sphinx.util import logging 27 28 29if TYPE_CHECKING: 30 from sphinx.application import Sphinx 31 32logger = logging.getLogger(__name__) 33 34 35class ObjectEntry(NamedTuple): 36 docname: str 37 node_id: str 38 objtype: str 39 aliased: bool 40 41 42class QAPIIndex(Index): 43 """ 44 Index subclass to provide the QAPI definition index. 45 """ 46 47 # pylint: disable=too-few-public-methods 48 49 name = "index" 50 localname = _("QAPI Index") 51 shortname = _("QAPI Index") 52 53 def generate( 54 self, 55 docnames: Optional[Iterable[str]] = None, 56 ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]: 57 assert isinstance(self.domain, QAPIDomain) 58 content: Dict[str, List[IndexEntry]] = {} 59 collapse = False 60 61 # list of all object (name, ObjectEntry) pairs, sorted by name 62 # (ignoring the module) 63 objects = sorted( 64 self.domain.objects.items(), 65 key=lambda x: x[0].split(".")[-1].lower(), 66 ) 67 68 for objname, obj in objects: 69 if docnames and obj.docname not in docnames: 70 continue 71 72 # Strip the module name out: 73 objname = objname.split(".")[-1] 74 75 # Add an alphabetical entry: 76 entries = content.setdefault(objname[0].upper(), []) 77 entries.append( 78 IndexEntry( 79 objname, 0, obj.docname, obj.node_id, obj.objtype, "", "" 80 ) 81 ) 82 83 # Add a categorical entry: 84 category = obj.objtype.title() + "s" 85 entries = content.setdefault(category, []) 86 entries.append( 87 IndexEntry(objname, 0, obj.docname, obj.node_id, "", "", "") 88 ) 89 90 # alphabetically sort categories; type names first, ABC entries last. 91 sorted_content = sorted( 92 content.items(), 93 key=lambda x: (len(x[0]) == 1, x[0]), 94 ) 95 return sorted_content, collapse 96 97 98class QAPIDomain(Domain): 99 """QAPI language domain.""" 100 101 name = "qapi" 102 label = "QAPI" 103 104 # This table associates cross-reference object types (key) with an 105 # ObjType instance, which defines the valid cross-reference roles 106 # for each object type. 107 108 # Actual table entries for module, command, event, etc will come in 109 # forthcoming commits. 110 object_types: Dict[str, ObjType] = {} 111 112 directives = {} 113 roles = {} 114 115 # Moved into the data property at runtime; 116 # this is the internal index of reference-able objects. 117 initial_data: Dict[str, Dict[str, Tuple[Any]]] = { 118 "objects": {}, # fullname -> ObjectEntry 119 } 120 121 # Index pages to generate; each entry is an Index class. 122 indices = [ 123 QAPIIndex, 124 ] 125 126 @property 127 def objects(self) -> Dict[str, ObjectEntry]: 128 ret = self.data.setdefault("objects", {}) 129 return ret # type: ignore[no-any-return] 130 131 def note_object( 132 self, 133 name: str, 134 objtype: str, 135 node_id: str, 136 aliased: bool = False, 137 location: Any = None, 138 ) -> None: 139 """Note a QAPI object for cross reference.""" 140 if name in self.objects: 141 other = self.objects[name] 142 if other.aliased and aliased is False: 143 # The original definition found. Override it! 144 pass 145 elif other.aliased is False and aliased: 146 # The original definition is already registered. 147 return 148 else: 149 # duplicated 150 logger.warning( 151 __( 152 "duplicate object description of %s, " 153 "other instance in %s, use :no-index: for one of them" 154 ), 155 name, 156 other.docname, 157 location=location, 158 ) 159 self.objects[name] = ObjectEntry( 160 self.env.docname, node_id, objtype, aliased 161 ) 162 163 def clear_doc(self, docname: str) -> None: 164 for fullname, obj in list(self.objects.items()): 165 if obj.docname == docname: 166 del self.objects[fullname] 167 168 def merge_domaindata( 169 self, docnames: AbstractSet[str], otherdata: Dict[str, Any] 170 ) -> None: 171 for fullname, obj in otherdata["objects"].items(): 172 if obj.docname in docnames: 173 # Sphinx's own python domain doesn't appear to bother to 174 # check for collisions. Assert they don't happen and 175 # we'll fix it if/when the case arises. 176 assert fullname not in self.objects, ( 177 "bug - collision on merge?" 178 f" {fullname=} {obj=} {self.objects[fullname]=}" 179 ) 180 self.objects[fullname] = obj 181 182 def resolve_any_xref(self, *args: Any, **kwargs: Any) -> Any: 183 # pylint: disable=unused-argument 184 return [] 185 186 187def setup(app: Sphinx) -> Dict[str, Any]: 188 app.setup_extension("sphinx.directives") 189 app.add_domain(QAPIDomain) 190 191 return { 192 "version": "1.0", 193 "env_version": 1, 194 "parallel_read_safe": True, 195 "parallel_write_safe": True, 196 } 197