Path: blob/master/tools/net/ynl/pyynl/lib/doc_generator.py
50360 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_op_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_op_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", "event"]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))219if "event" in operation:220lines.append(self.fmt.rst_paragraph(":event:", 0))221lines.append(self.parse_op_attributes(operation["event"], 0))222223# New line after fields224lines.append("\n")225226return "\n".join(lines)227228def parse_entries(self, entries: List[Dict[str, Any]], level: int) -> str:229"""Parse a list of entries"""230ignored = ["pad"]231lines = []232for entry in entries:233if isinstance(entry, dict):234# entries could be a list or a dictionary235field_name = entry.get("name", "")236if field_name in ignored:237continue238type_ = entry.get("type")239if type_:240field_name += f" ({self.fmt.inline(type_)})"241lines.append(242self.fmt.rst_fields(field_name,243self.fmt.sanitize(entry.get("doc", "")),244level)245)246elif isinstance(entry, list):247lines.append(self.fmt.rst_list_inline(entry, level))248else:249lines.append(self.fmt.rst_bullet(self.fmt.inline(self.fmt.sanitize(entry)),250level))251252lines.append("\n")253return "\n".join(lines)254255def parse_definitions(self, defs: Dict[str, Any], namespace: str) -> str:256"""Parse definitions section"""257preprocessed = ["name", "entries", "members"]258ignored = ["render-max"] # This is not printed259lines = []260261for definition in defs:262if LINE_STR in definition:263lines.append(self.fmt.rst_lineno(definition[LINE_STR]))264265lines.append(self.fmt.rst_section(namespace, 'definition', definition["name"]))266for k in definition.keys():267if k == LINE_STR:268continue269if k in preprocessed + ignored:270continue271lines.append(self.fmt.rst_fields(k, self.fmt.sanitize(definition[k]), 0))272273# Field list needs to finish with a new line274lines.append("\n")275if "entries" in definition:276lines.append(self.fmt.rst_paragraph(":entries:", 0))277lines.append(self.parse_entries(definition["entries"], 1))278if "members" in definition:279lines.append(self.fmt.rst_paragraph(":members:", 0))280lines.append(self.parse_entries(definition["members"], 1))281282return "\n".join(lines)283284def parse_attr_sets(self, entries: List[Dict[str, Any]], namespace: str) -> str:285"""Parse attribute from attribute-set"""286preprocessed = ["name", "type"]287linkable = ["enum", "nested-attributes", "struct", "sub-message"]288ignored = ["checks"]289lines = []290291for entry in entries:292lines.append(self.fmt.rst_section(namespace, 'attribute-set',293entry["name"]))294295if "doc" in entry:296lines.append(self.fmt.rst_paragraph(entry["doc"], 0) + "\n")297298for attr in entry["attributes"]:299if LINE_STR in attr:300lines.append(self.fmt.rst_lineno(attr[LINE_STR]))301302type_ = attr.get("type")303attr_line = attr["name"]304if type_:305# Add the attribute type in the same line306attr_line += f" ({self.fmt.inline(type_)})"307308lines.append(self.fmt.rst_subsubsection(attr_line))309310for k in attr.keys():311if k == LINE_STR:312continue313if k in preprocessed + ignored:314continue315if k in linkable:316value = self.fmt.rst_ref(namespace, k, attr[k])317else:318value = self.fmt.sanitize(attr[k])319lines.append(self.fmt.rst_fields(k, value, 0))320lines.append("\n")321322return "\n".join(lines)323324def parse_sub_messages(self, entries: List[Dict[str, Any]], namespace: str) -> str:325"""Parse sub-message definitions"""326lines = []327328for entry in entries:329lines.append(self.fmt.rst_section(namespace, 'sub-message',330entry["name"]))331for fmt in entry["formats"]:332value = fmt["value"]333334lines.append(self.fmt.rst_bullet(self.fmt.bold(value)))335for attr in ['fixed-header', 'attribute-set']:336if attr in fmt:337lines.append(self.fmt.rst_fields(attr,338self.fmt.rst_ref(namespace,339attr,340fmt[attr]),3411))342lines.append("\n")343344return "\n".join(lines)345346def parse_yaml(self, obj: Dict[str, Any]) -> str:347"""Format the whole YAML into a RST string"""348lines = []349350# Main header351lineno = obj.get('__lineno__', 0)352lines.append(self.fmt.rst_lineno(lineno))353354family = obj['name']355356lines.append(self.fmt.rst_header())357lines.append(self.fmt.rst_label("netlink-" + family))358359title = f"Family ``{family}`` netlink specification"360lines.append(self.fmt.rst_title(title))361lines.append(self.fmt.rst_paragraph(".. contents:: :depth: 3\n"))362363if "doc" in obj:364lines.append(self.fmt.rst_subtitle("Summary"))365lines.append(self.fmt.rst_paragraph(obj["doc"], 0))366367# Operations368if "operations" in obj:369lines.append(self.fmt.rst_subtitle("Operations"))370lines.append(self.parse_operations(obj["operations"]["list"],371family))372373# Multicast groups374if "mcast-groups" in obj:375lines.append(self.fmt.rst_subtitle("Multicast groups"))376lines.append(self.parse_mcast_group(obj["mcast-groups"]["list"]))377378# Definitions379if "definitions" in obj:380lines.append(self.fmt.rst_subtitle("Definitions"))381lines.append(self.parse_definitions(obj["definitions"], family))382383# Attributes set384if "attribute-sets" in obj:385lines.append(self.fmt.rst_subtitle("Attribute sets"))386lines.append(self.parse_attr_sets(obj["attribute-sets"], family))387388# Sub-messages389if "sub-messages" in obj:390lines.append(self.fmt.rst_subtitle("Sub-messages"))391lines.append(self.parse_sub_messages(obj["sub-messages"], family))392393return "\n".join(lines)394395# Main functions396# ==============397398def parse_yaml_file(self, filename: str) -> str:399"""Transform the YAML specified by filename into an RST-formatted string"""400with open(filename, "r", encoding="utf-8") as spec_file:401numbered_yaml = yaml.load(spec_file, Loader=NumberedSafeLoader)402content = self.parse_yaml(numbered_yaml)403404return content405406407