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