1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 3# -*- coding: utf-8; mode: python -*- 4 5""" 6 Class to auto generate the documentation for Netlink specifications. 7 8 :copyright: Copyright (C) 2023 Breno Leitao <leitao@debian.org> 9 :license: GPL Version 2, June 1991 see linux/COPYING for details. 10 11 This class performs extensive parsing to the Linux kernel's netlink YAML 12 spec files, in an effort to avoid needing to heavily mark up the original 13 YAML file. 14 15 This code is split in two classes: 16 1) RST formatters: Use to convert a string to a RST output 17 2) YAML Netlink (YNL) doc generator: Generate docs from YAML data 18""" 19 20from typing import Any, Dict, List 21import yaml 22 23 24class RstFormatters: 25 """RST Formatters""" 26 27 SPACE_PER_LEVEL = 4 28 29 @staticmethod 30 def headroom(level: int) -> str: 31 """Return space to format""" 32 return " " * (level * RstFormatters.SPACE_PER_LEVEL) 33 34 @staticmethod 35 def bold(text: str) -> str: 36 """Format bold text""" 37 return f"**{text}**" 38 39 @staticmethod 40 def inline(text: str) -> str: 41 """Format inline text""" 42 return f"``{text}``" 43 44 @staticmethod 45 def sanitize(text: str) -> str: 46 """Remove newlines and multiple spaces""" 47 # This is useful for some fields that are spread across multiple lines 48 return str(text).replace("\n", " ").strip() 49 50 def rst_fields(self, key: str, value: str, level: int = 0) -> str: 51 """Return a RST formatted field""" 52 return self.headroom(level) + f":{key}: {value}" 53 54 def rst_definition(self, key: str, value: Any, level: int = 0) -> str: 55 """Format a single rst definition""" 56 return self.headroom(level) + key + "\n" + self.headroom(level + 1) + str(value) 57 58 def rst_paragraph(self, paragraph: str, level: int = 0) -> str: 59 """Return a formatted paragraph""" 60 return self.headroom(level) + paragraph 61 62 def rst_bullet(self, item: str, level: int = 0) -> str: 63 """Return a formatted a bullet""" 64 return self.headroom(level) + f"- {item}" 65 66 @staticmethod 67 def rst_subsection(title: str) -> str: 68 """Add a sub-section to the document""" 69 return f"{title}\n" + "-" * len(title) 70 71 @staticmethod 72 def rst_subsubsection(title: str) -> str: 73 """Add a sub-sub-section to the document""" 74 return f"{title}\n" + "~" * len(title) 75 76 @staticmethod 77 def rst_section(namespace: str, prefix: str, title: str) -> str: 78 """Add a section to the document""" 79 return f".. _{namespace}-{prefix}-{title}:\n\n{title}\n" + "=" * len(title) 80 81 @staticmethod 82 def rst_subtitle(title: str) -> str: 83 """Add a subtitle to the document""" 84 return "\n" + "-" * len(title) + f"\n{title}\n" + "-" * len(title) + "\n\n" 85 86 @staticmethod 87 def rst_title(title: str) -> str: 88 """Add a title to the document""" 89 return "=" * len(title) + f"\n{title}\n" + "=" * len(title) + "\n\n" 90 91 def rst_list_inline(self, list_: List[str], level: int = 0) -> str: 92 """Format a list using inlines""" 93 return self.headroom(level) + "[" + ", ".join(self.inline(i) for i in list_) + "]" 94 95 @staticmethod 96 def rst_ref(namespace: str, prefix: str, name: str) -> str: 97 """Add a hyperlink to the document""" 98 mappings = {'enum': 'definition', 99 'fixed-header': 'definition', 100 'nested-attributes': 'attribute-set', 101 'struct': 'definition'} 102 if prefix in mappings: 103 prefix = mappings[prefix] 104 return f":ref:`{namespace}-{prefix}-{name}`" 105 106 def rst_header(self) -> str: 107 """The headers for all the auto generated RST files""" 108 lines = [] 109 110 lines.append(self.rst_paragraph(".. SPDX-License-Identifier: GPL-2.0")) 111 lines.append(self.rst_paragraph(".. NOTE: This document was auto-generated.\n\n")) 112 113 return "\n".join(lines) 114 115 @staticmethod 116 def rst_toctree(maxdepth: int = 2) -> str: 117 """Generate a toctree RST primitive""" 118 lines = [] 119 120 lines.append(".. toctree::") 121 lines.append(f" :maxdepth: {maxdepth}\n\n") 122 123 return "\n".join(lines) 124 125 @staticmethod 126 def rst_label(title: str) -> str: 127 """Return a formatted label""" 128 return f".. _{title}:\n\n" 129 130class YnlDocGenerator: 131 """YAML Netlink specs Parser""" 132 133 fmt = RstFormatters() 134 135 def parse_mcast_group(self, mcast_group: List[Dict[str, Any]]) -> str: 136 """Parse 'multicast' group list and return a formatted string""" 137 lines = [] 138 for group in mcast_group: 139 lines.append(self.fmt.rst_bullet(group["name"])) 140 141 return "\n".join(lines) 142 143 def parse_do(self, do_dict: Dict[str, Any], level: int = 0) -> str: 144 """Parse 'do' section and return a formatted string""" 145 lines = [] 146 for key in do_dict.keys(): 147 lines.append(self.fmt.rst_paragraph(self.fmt.bold(key), level + 1)) 148 if key in ['request', 'reply']: 149 lines.append(self.parse_do_attributes(do_dict[key], level + 1) + "\n") 150 else: 151 lines.append(self.fmt.headroom(level + 2) + do_dict[key] + "\n") 152 153 return "\n".join(lines) 154 155 def parse_do_attributes(self, attrs: Dict[str, Any], level: int = 0) -> str: 156 """Parse 'attributes' section""" 157 if "attributes" not in attrs: 158 return "" 159 lines = [self.fmt.rst_fields("attributes", 160 self.fmt.rst_list_inline(attrs["attributes"]), 161 level + 1)] 162 163 return "\n".join(lines) 164 165 def parse_operations(self, operations: List[Dict[str, Any]], namespace: str) -> str: 166 """Parse operations block""" 167 preprocessed = ["name", "doc", "title", "do", "dump", "flags"] 168 linkable = ["fixed-header", "attribute-set"] 169 lines = [] 170 171 for operation in operations: 172 lines.append(self.fmt.rst_section(namespace, 'operation', 173 operation["name"])) 174 lines.append(self.fmt.rst_paragraph(operation["doc"]) + "\n") 175 176 for key in operation.keys(): 177 if key in preprocessed: 178 # Skip the special fields 179 continue 180 value = operation[key] 181 if key in linkable: 182 value = self.fmt.rst_ref(namespace, key, value) 183 lines.append(self.fmt.rst_fields(key, value, 0)) 184 if 'flags' in operation: 185 lines.append(self.fmt.rst_fields('flags', 186 self.fmt.rst_list_inline(operation['flags']))) 187 188 if "do" in operation: 189 lines.append(self.fmt.rst_paragraph(":do:", 0)) 190 lines.append(self.parse_do(operation["do"], 0)) 191 if "dump" in operation: 192 lines.append(self.fmt.rst_paragraph(":dump:", 0)) 193 lines.append(self.parse_do(operation["dump"], 0)) 194 195 # New line after fields 196 lines.append("\n") 197 198 return "\n".join(lines) 199 200 def parse_entries(self, entries: List[Dict[str, Any]], level: int) -> str: 201 """Parse a list of entries""" 202 ignored = ["pad"] 203 lines = [] 204 for entry in entries: 205 if isinstance(entry, dict): 206 # entries could be a list or a dictionary 207 field_name = entry.get("name", "") 208 if field_name in ignored: 209 continue 210 type_ = entry.get("type") 211 if type_: 212 field_name += f" ({self.fmt.inline(type_)})" 213 lines.append( 214 self.fmt.rst_fields(field_name, 215 self.fmt.sanitize(entry.get("doc", "")), 216 level) 217 ) 218 elif isinstance(entry, list): 219 lines.append(self.fmt.rst_list_inline(entry, level)) 220 else: 221 lines.append(self.fmt.rst_bullet(self.fmt.inline(self.fmt.sanitize(entry)), 222 level)) 223 224 lines.append("\n") 225 return "\n".join(lines) 226 227 def parse_definitions(self, defs: Dict[str, Any], namespace: str) -> str: 228 """Parse definitions section""" 229 preprocessed = ["name", "entries", "members"] 230 ignored = ["render-max"] # This is not printed 231 lines = [] 232 233 for definition in defs: 234 lines.append(self.fmt.rst_section(namespace, 'definition', definition["name"])) 235 for k in definition.keys(): 236 if k in preprocessed + ignored: 237 continue 238 lines.append(self.fmt.rst_fields(k, self.fmt.sanitize(definition[k]), 0)) 239 240 # Field list needs to finish with a new line 241 lines.append("\n") 242 if "entries" in definition: 243 lines.append(self.fmt.rst_paragraph(":entries:", 0)) 244 lines.append(self.parse_entries(definition["entries"], 1)) 245 if "members" in definition: 246 lines.append(self.fmt.rst_paragraph(":members:", 0)) 247 lines.append(self.parse_entries(definition["members"], 1)) 248 249 return "\n".join(lines) 250 251 def parse_attr_sets(self, entries: List[Dict[str, Any]], namespace: str) -> str: 252 """Parse attribute from attribute-set""" 253 preprocessed = ["name", "type"] 254 linkable = ["enum", "nested-attributes", "struct", "sub-message"] 255 ignored = ["checks"] 256 lines = [] 257 258 for entry in entries: 259 lines.append(self.fmt.rst_section(namespace, 'attribute-set', 260 entry["name"])) 261 for attr in entry["attributes"]: 262 type_ = attr.get("type") 263 attr_line = attr["name"] 264 if type_: 265 # Add the attribute type in the same line 266 attr_line += f" ({self.fmt.inline(type_)})" 267 268 lines.append(self.fmt.rst_subsubsection(attr_line)) 269 270 for k in attr.keys(): 271 if k in preprocessed + ignored: 272 continue 273 if k in linkable: 274 value = self.fmt.rst_ref(namespace, k, attr[k]) 275 else: 276 value = self.fmt.sanitize(attr[k]) 277 lines.append(self.fmt.rst_fields(k, value, 0)) 278 lines.append("\n") 279 280 return "\n".join(lines) 281 282 def parse_sub_messages(self, entries: List[Dict[str, Any]], namespace: str) -> str: 283 """Parse sub-message definitions""" 284 lines = [] 285 286 for entry in entries: 287 lines.append(self.fmt.rst_section(namespace, 'sub-message', 288 entry["name"])) 289 for fmt in entry["formats"]: 290 value = fmt["value"] 291 292 lines.append(self.fmt.rst_bullet(self.fmt.bold(value))) 293 for attr in ['fixed-header', 'attribute-set']: 294 if attr in fmt: 295 lines.append(self.fmt.rst_fields(attr, 296 self.fmt.rst_ref(namespace, 297 attr, 298 fmt[attr]), 299 1)) 300 lines.append("\n") 301 302 return "\n".join(lines) 303 304 def parse_yaml(self, obj: Dict[str, Any]) -> str: 305 """Format the whole YAML into a RST string""" 306 lines = [] 307 308 # Main header 309 310 family = obj['name'] 311 312 lines.append(self.fmt.rst_header()) 313 lines.append(self.fmt.rst_label("netlink-" + family)) 314 315 title = f"Family ``{family}`` netlink specification" 316 lines.append(self.fmt.rst_title(title)) 317 lines.append(self.fmt.rst_paragraph(".. contents:: :depth: 3\n")) 318 319 if "doc" in obj: 320 lines.append(self.fmt.rst_subtitle("Summary")) 321 lines.append(self.fmt.rst_paragraph(obj["doc"], 0)) 322 323 # Operations 324 if "operations" in obj: 325 lines.append(self.fmt.rst_subtitle("Operations")) 326 lines.append(self.parse_operations(obj["operations"]["list"], 327 family)) 328 329 # Multicast groups 330 if "mcast-groups" in obj: 331 lines.append(self.fmt.rst_subtitle("Multicast groups")) 332 lines.append(self.parse_mcast_group(obj["mcast-groups"]["list"])) 333 334 # Definitions 335 if "definitions" in obj: 336 lines.append(self.fmt.rst_subtitle("Definitions")) 337 lines.append(self.parse_definitions(obj["definitions"], family)) 338 339 # Attributes set 340 if "attribute-sets" in obj: 341 lines.append(self.fmt.rst_subtitle("Attribute sets")) 342 lines.append(self.parse_attr_sets(obj["attribute-sets"], family)) 343 344 # Sub-messages 345 if "sub-messages" in obj: 346 lines.append(self.fmt.rst_subtitle("Sub-messages")) 347 lines.append(self.parse_sub_messages(obj["sub-messages"], family)) 348 349 return "\n".join(lines) 350 351 # Main functions 352 # ============== 353 354 def parse_yaml_file(self, filename: str) -> str: 355 """Transform the YAML specified by filename into an RST-formatted string""" 356 with open(filename, "r", encoding="utf-8") as spec_file: 357 yaml_data = yaml.safe_load(spec_file) 358 content = self.parse_yaml(yaml_data) 359 360 return content 361