Path: blob/master/tools/net/ynl/pyynl/lib/doc_generator.py
29274 views
#!/usr/bin/env python31# SPDX-License-Identifier: GPL-2.02# -*- coding: utf-8; mode: python -*-34"""5Class to auto generate the documentation for Netlink specifications.67:copyright: Copyright (C) 2023 Breno Leitao <[email protected]>8:license: GPL Version 2, June 1991 see linux/COPYING for details.910This class performs extensive parsing to the Linux kernel's netlink YAML11spec files, in an effort to avoid needing to heavily mark up the original12YAML file.1314This code is split in two classes:151) RST formatters: Use to convert a string to a RST output162) YAML Netlink (YNL) doc generator: Generate docs from YAML data17"""1819from typing import Any, Dict, List20import yaml2122LINE_STR = '__lineno__'2324class NumberedSafeLoader(yaml.SafeLoader): # pylint: disable=R090125"""Override the SafeLoader class to add line number to parsed data"""2627def construct_mapping(self, node, *args, **kwargs):28mapping = super().construct_mapping(node, *args, **kwargs)29mapping[LINE_STR] = node.start_mark.line3031return mapping3233class RstFormatters:34"""RST Formatters"""3536SPACE_PER_LEVEL = 43738@staticmethod39def headroom(level: int) -> str:40"""Return space to format"""41return " " * (level * RstFormatters.SPACE_PER_LEVEL)4243@staticmethod44def bold(text: str) -> str:45"""Format bold text"""46return f"**{text}**"4748@staticmethod49def inline(text: str) -> str:50"""Format inline text"""51return f"``{text}``"5253@staticmethod54def sanitize(text: str) -> str:55"""Remove newlines and multiple spaces"""56# This is useful for some fields that are spread across multiple lines57return str(text).replace("\n", " ").strip()5859def rst_fields(self, key: str, value: str, level: int = 0) -> str:60"""Return a RST formatted field"""61return self.headroom(level) + f":{key}: {value}"6263def rst_definition(self, key: str, value: Any, level: int = 0) -> str:64"""Format a single rst definition"""65return self.headroom(level) + key + "\n" + self.headroom(level + 1) + str(value)6667def rst_paragraph(self, paragraph: str, level: int = 0) -> str:68"""Return a formatted paragraph"""69return self.headroom(level) + paragraph7071def rst_bullet(self, item: str, level: int = 0) -> str:72"""Return a formatted a bullet"""73return self.headroom(level) + f"- {item}"7475@staticmethod76def rst_subsection(title: str) -> str:77"""Add a sub-section to the document"""78return f"{title}\n" + "-" * len(title)7980@staticmethod81def rst_subsubsection(title: str) -> str:82"""Add a sub-sub-section to the document"""83return f"{title}\n" + "~" * len(title)8485@staticmethod86def rst_section(namespace: str, prefix: str, title: str) -> str:87"""Add a section to the document"""88return f".. _{namespace}-{prefix}-{title}:\n\n{title}\n" + "=" * len(title)8990@staticmethod91def rst_subtitle(title: str) -> str:92"""Add a subtitle to the document"""93return "\n" + "-" * len(title) + f"\n{title}\n" + "-" * len(title) + "\n\n"9495@staticmethod96def rst_title(title: str) -> str:97"""Add a title to the document"""98return "=" * len(title) + f"\n{title}\n" + "=" * len(title) + "\n\n"99100def rst_list_inline(self, list_: List[str], level: int = 0) -> str:101"""Format a list using inlines"""102return self.headroom(level) + "[" + ", ".join(self.inline(i) for i in list_) + "]"103104@staticmethod105def rst_ref(namespace: str, prefix: str, name: str) -> str:106"""Add a hyperlink to the document"""107mappings = {'enum': 'definition',108'fixed-header': 'definition',109'nested-attributes': 'attribute-set',110'struct': 'definition'}111if prefix in mappings:112prefix = mappings[prefix]113return f":ref:`{namespace}-{prefix}-{name}`"114115def rst_header(self) -> str:116"""The headers for all the auto generated RST files"""117lines = []118119lines.append(self.rst_paragraph(".. SPDX-License-Identifier: GPL-2.0"))120lines.append(self.rst_paragraph(".. NOTE: This document was auto-generated.\n\n"))121122return "\n".join(lines)123124@staticmethod125def rst_toctree(maxdepth: int = 2) -> str:126"""Generate a toctree RST primitive"""127lines = []128129lines.append(".. toctree::")130lines.append(f" :maxdepth: {maxdepth}\n\n")131132return "\n".join(lines)133134@staticmethod135def rst_label(title: str) -> str:136"""Return a formatted label"""137return f".. _{title}:\n\n"138139@staticmethod140def rst_lineno(lineno: int) -> str:141"""Return a lineno comment"""142return f".. LINENO {lineno}\n"143144class YnlDocGenerator:145"""YAML Netlink specs Parser"""146147fmt = RstFormatters()148149def parse_mcast_group(self, mcast_group: List[Dict[str, Any]]) -> str:150"""Parse 'multicast' group list and return a formatted string"""151lines = []152for group in mcast_group:153lines.append(self.fmt.rst_bullet(group["name"]))154155return "\n".join(lines)156157def parse_do(self, do_dict: Dict[str, Any], level: int = 0) -> str:158"""Parse 'do' section and return a formatted string"""159lines = []160if LINE_STR in do_dict:161lines.append(self.fmt.rst_lineno(do_dict[LINE_STR]))162163for key in do_dict.keys():164if key == LINE_STR:165continue166lines.append(self.fmt.rst_paragraph(self.fmt.bold(key), level + 1))167if key in ['request', 'reply']:168lines.append(self.parse_do_attributes(do_dict[key], level + 1) + "\n")169else:170lines.append(self.fmt.headroom(level + 2) + do_dict[key] + "\n")171172return "\n".join(lines)173174def parse_do_attributes(self, attrs: Dict[str, Any], level: int = 0) -> str:175"""Parse 'attributes' section"""176if "attributes" not in attrs:177return ""178lines = [self.fmt.rst_fields("attributes",179self.fmt.rst_list_inline(attrs["attributes"]),180level + 1)]181182return "\n".join(lines)183184def parse_operations(self, operations: List[Dict[str, Any]], namespace: str) -> str:185"""Parse operations block"""186preprocessed = ["name", "doc", "title", "do", "dump", "flags"]187linkable = ["fixed-header", "attribute-set"]188lines = []189190for operation in operations:191if LINE_STR in operation:192lines.append(self.fmt.rst_lineno(operation[LINE_STR]))193194lines.append(self.fmt.rst_section(namespace, 'operation',195operation["name"]))196lines.append(self.fmt.rst_paragraph(operation["doc"]) + "\n")197198for key in operation.keys():199if key == LINE_STR:200continue201202if key in preprocessed:203# Skip the special fields204continue205value = operation[key]206if key in linkable:207value = self.fmt.rst_ref(namespace, key, value)208lines.append(self.fmt.rst_fields(key, value, 0))209if 'flags' in operation:210lines.append(self.fmt.rst_fields('flags',211self.fmt.rst_list_inline(operation['flags'])))212213if "do" in operation:214lines.append(self.fmt.rst_paragraph(":do:", 0))215lines.append(self.parse_do(operation["do"], 0))216if "dump" in operation:217lines.append(self.fmt.rst_paragraph(":dump:", 0))218lines.append(self.parse_do(operation["dump"], 0))219220# New line after fields221lines.append("\n")222223return "\n".join(lines)224225def parse_entries(self, entries: List[Dict[str, Any]], level: int) -> str:226"""Parse a list of entries"""227ignored = ["pad"]228lines = []229for entry in entries:230if isinstance(entry, dict):231# entries could be a list or a dictionary232field_name = entry.get("name", "")233if field_name in ignored:234continue235type_ = entry.get("type")236if type_:237field_name += f" ({self.fmt.inline(type_)})"238lines.append(239self.fmt.rst_fields(field_name,240self.fmt.sanitize(entry.get("doc", "")),241level)242)243elif isinstance(entry, list):244lines.append(self.fmt.rst_list_inline(entry, level))245else:246lines.append(self.fmt.rst_bullet(self.fmt.inline(self.fmt.sanitize(entry)),247level))248249lines.append("\n")250return "\n".join(lines)251252def parse_definitions(self, defs: Dict[str, Any], namespace: str) -> str:253"""Parse definitions section"""254preprocessed = ["name", "entries", "members"]255ignored = ["render-max"] # This is not printed256lines = []257258for definition in defs:259if LINE_STR in definition:260lines.append(self.fmt.rst_lineno(definition[LINE_STR]))261262lines.append(self.fmt.rst_section(namespace, 'definition', definition["name"]))263for k in definition.keys():264if k == LINE_STR:265continue266if k in preprocessed + ignored:267continue268lines.append(self.fmt.rst_fields(k, self.fmt.sanitize(definition[k]), 0))269270# Field list needs to finish with a new line271lines.append("\n")272if "entries" in definition:273lines.append(self.fmt.rst_paragraph(":entries:", 0))274lines.append(self.parse_entries(definition["entries"], 1))275if "members" in definition:276lines.append(self.fmt.rst_paragraph(":members:", 0))277lines.append(self.parse_entries(definition["members"], 1))278279return "\n".join(lines)280281def parse_attr_sets(self, entries: List[Dict[str, Any]], namespace: str) -> str:282"""Parse attribute from attribute-set"""283preprocessed = ["name", "type"]284linkable = ["enum", "nested-attributes", "struct", "sub-message"]285ignored = ["checks"]286lines = []287288for entry in entries:289lines.append(self.fmt.rst_section(namespace, 'attribute-set',290entry["name"]))291292if "doc" in entry:293lines.append(self.fmt.rst_paragraph(entry["doc"], 0) + "\n")294295for attr in entry["attributes"]:296if LINE_STR in attr:297lines.append(self.fmt.rst_lineno(attr[LINE_STR]))298299type_ = attr.get("type")300attr_line = attr["name"]301if type_:302# Add the attribute type in the same line303attr_line += f" ({self.fmt.inline(type_)})"304305lines.append(self.fmt.rst_subsubsection(attr_line))306307for k in attr.keys():308if k == LINE_STR:309continue310if k in preprocessed + ignored:311continue312if k in linkable:313value = self.fmt.rst_ref(namespace, k, attr[k])314else:315value = self.fmt.sanitize(attr[k])316lines.append(self.fmt.rst_fields(k, value, 0))317lines.append("\n")318319return "\n".join(lines)320321def parse_sub_messages(self, entries: List[Dict[str, Any]], namespace: str) -> str:322"""Parse sub-message definitions"""323lines = []324325for entry in entries:326lines.append(self.fmt.rst_section(namespace, 'sub-message',327entry["name"]))328for fmt in entry["formats"]:329value = fmt["value"]330331lines.append(self.fmt.rst_bullet(self.fmt.bold(value)))332for attr in ['fixed-header', 'attribute-set']:333if attr in fmt:334lines.append(self.fmt.rst_fields(attr,335self.fmt.rst_ref(namespace,336attr,337fmt[attr]),3381))339lines.append("\n")340341return "\n".join(lines)342343def parse_yaml(self, obj: Dict[str, Any]) -> str:344"""Format the whole YAML into a RST string"""345lines = []346347# Main header348lineno = obj.get('__lineno__', 0)349lines.append(self.fmt.rst_lineno(lineno))350351family = obj['name']352353lines.append(self.fmt.rst_header())354lines.append(self.fmt.rst_label("netlink-" + family))355356title = f"Family ``{family}`` netlink specification"357lines.append(self.fmt.rst_title(title))358lines.append(self.fmt.rst_paragraph(".. contents:: :depth: 3\n"))359360if "doc" in obj:361lines.append(self.fmt.rst_subtitle("Summary"))362lines.append(self.fmt.rst_paragraph(obj["doc"], 0))363364# Operations365if "operations" in obj:366lines.append(self.fmt.rst_subtitle("Operations"))367lines.append(self.parse_operations(obj["operations"]["list"],368family))369370# Multicast groups371if "mcast-groups" in obj:372lines.append(self.fmt.rst_subtitle("Multicast groups"))373lines.append(self.parse_mcast_group(obj["mcast-groups"]["list"]))374375# Definitions376if "definitions" in obj:377lines.append(self.fmt.rst_subtitle("Definitions"))378lines.append(self.parse_definitions(obj["definitions"], family))379380# Attributes set381if "attribute-sets" in obj:382lines.append(self.fmt.rst_subtitle("Attribute sets"))383lines.append(self.parse_attr_sets(obj["attribute-sets"], family))384385# Sub-messages386if "sub-messages" in obj:387lines.append(self.fmt.rst_subtitle("Sub-messages"))388lines.append(self.parse_sub_messages(obj["sub-messages"], family))389390return "\n".join(lines)391392# Main functions393# ==============394395def parse_yaml_file(self, filename: str) -> str:396"""Transform the YAML specified by filename into an RST-formatted string"""397with open(filename, "r", encoding="utf-8") as spec_file:398numbered_yaml = yaml.load(spec_file, Loader=NumberedSafeLoader)399content = self.parse_yaml(numbered_yaml)400401return content402403404