]> Git Repo - J-linux.git/blob - tools/net/ynl/ynl-gen-rst.py
Merge patch series "riscv: Extension parsing fixes"
[J-linux.git] / tools / net / ynl / ynl-gen-rst.py
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: GPL-2.0
3 # -*- coding: utf-8; mode: python -*-
4
5 """
6     Script to auto generate the documentation for Netlink specifications.
7
8     :copyright:  Copyright (C) 2023  Breno Leitao <[email protected]>
9     :license:    GPL Version 2, June 1991 see linux/COPYING for details.
10
11     This script 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 three big parts:
16         1) RST formatters: Use to convert a string to a RST output
17         2) Parser helpers: Functions to parse the YAML data structure
18         3) Main function and small helpers
19 """
20
21 from typing import Any, Dict, List
22 import os.path
23 import sys
24 import argparse
25 import logging
26 import yaml
27
28
29 SPACE_PER_LEVEL = 4
30
31
32 # RST Formatters
33 # ==============
34 def headroom(level: int) -> str:
35     """Return space to format"""
36     return " " * (level * SPACE_PER_LEVEL)
37
38
39 def bold(text: str) -> str:
40     """Format bold text"""
41     return f"**{text}**"
42
43
44 def inline(text: str) -> str:
45     """Format inline text"""
46     return f"``{text}``"
47
48
49 def sanitize(text: str) -> str:
50     """Remove newlines and multiple spaces"""
51     # This is useful for some fields that are spread across multiple lines
52     return str(text).replace("\n", "").strip()
53
54
55 def rst_fields(key: str, value: str, level: int = 0) -> str:
56     """Return a RST formatted field"""
57     return headroom(level) + f":{key}: {value}"
58
59
60 def rst_definition(key: str, value: Any, level: int = 0) -> str:
61     """Format a single rst definition"""
62     return headroom(level) + key + "\n" + headroom(level + 1) + str(value)
63
64
65 def rst_paragraph(paragraph: str, level: int = 0) -> str:
66     """Return a formatted paragraph"""
67     return headroom(level) + paragraph
68
69
70 def rst_bullet(item: str, level: int = 0) -> str:
71     """Return a formatted a bullet"""
72     return headroom(level) + f"- {item}"
73
74
75 def rst_subsection(title: str) -> str:
76     """Add a sub-section to the document"""
77     return f"{title}\n" + "-" * len(title)
78
79
80 def rst_subsubsection(title: str) -> str:
81     """Add a sub-sub-section to the document"""
82     return f"{title}\n" + "~" * len(title)
83
84
85 def rst_section(namespace: str, prefix: str, title: str) -> str:
86     """Add a section to the document"""
87     return f".. _{namespace}-{prefix}-{title}:\n\n{title}\n" + "=" * len(title)
88
89
90 def rst_subtitle(title: str) -> str:
91     """Add a subtitle to the document"""
92     return "\n" + "-" * len(title) + f"\n{title}\n" + "-" * len(title) + "\n\n"
93
94
95 def rst_title(title: str) -> str:
96     """Add a title to the document"""
97     return "=" * len(title) + f"\n{title}\n" + "=" * len(title) + "\n\n"
98
99
100 def rst_list_inline(list_: List[str], level: int = 0) -> str:
101     """Format a list using inlines"""
102     return headroom(level) + "[" + ", ".join(inline(i) for i in list_) + "]"
103
104
105 def rst_ref(namespace: str, prefix: str, name: str) -> str:
106     """Add a hyperlink to the document"""
107     mappings = {'enum': 'definition',
108                 'fixed-header': 'definition',
109                 'nested-attributes': 'attribute-set',
110                 'struct': 'definition'}
111     if prefix in mappings:
112         prefix = mappings[prefix]
113     return f":ref:`{namespace}-{prefix}-{name}`"
114
115
116 def rst_header() -> str:
117     """The headers for all the auto generated RST files"""
118     lines = []
119
120     lines.append(rst_paragraph(".. SPDX-License-Identifier: GPL-2.0"))
121     lines.append(rst_paragraph(".. NOTE: This document was auto-generated.\n\n"))
122
123     return "\n".join(lines)
124
125
126 def rst_toctree(maxdepth: int = 2) -> str:
127     """Generate a toctree RST primitive"""
128     lines = []
129
130     lines.append(".. toctree::")
131     lines.append(f"   :maxdepth: {maxdepth}\n\n")
132
133     return "\n".join(lines)
134
135
136 def rst_label(title: str) -> str:
137     """Return a formatted label"""
138     return f".. _{title}:\n\n"
139
140
141 # Parsers
142 # =======
143
144
145 def parse_mcast_group(mcast_group: List[Dict[str, Any]]) -> str:
146     """Parse 'multicast' group list and return a formatted string"""
147     lines = []
148     for group in mcast_group:
149         lines.append(rst_bullet(group["name"]))
150
151     return "\n".join(lines)
152
153
154 def parse_do(do_dict: Dict[str, Any], level: int = 0) -> str:
155     """Parse 'do' section and return a formatted string"""
156     lines = []
157     for key in do_dict.keys():
158         lines.append(rst_paragraph(bold(key), level + 1))
159         lines.append(parse_do_attributes(do_dict[key], level + 1) + "\n")
160
161     return "\n".join(lines)
162
163
164 def parse_do_attributes(attrs: Dict[str, Any], level: int = 0) -> str:
165     """Parse 'attributes' section"""
166     if "attributes" not in attrs:
167         return ""
168     lines = [rst_fields("attributes", rst_list_inline(attrs["attributes"]), level + 1)]
169
170     return "\n".join(lines)
171
172
173 def parse_operations(operations: List[Dict[str, Any]], namespace: str) -> str:
174     """Parse operations block"""
175     preprocessed = ["name", "doc", "title", "do", "dump"]
176     linkable = ["fixed-header", "attribute-set"]
177     lines = []
178
179     for operation in operations:
180         lines.append(rst_section(namespace, 'operation', operation["name"]))
181         lines.append(rst_paragraph(sanitize(operation["doc"])) + "\n")
182
183         for key in operation.keys():
184             if key in preprocessed:
185                 # Skip the special fields
186                 continue
187             value = operation[key]
188             if key in linkable:
189                 value = rst_ref(namespace, key, value)
190             lines.append(rst_fields(key, value, 0))
191
192         if "do" in operation:
193             lines.append(rst_paragraph(":do:", 0))
194             lines.append(parse_do(operation["do"], 0))
195         if "dump" in operation:
196             lines.append(rst_paragraph(":dump:", 0))
197             lines.append(parse_do(operation["dump"], 0))
198
199         # New line after fields
200         lines.append("\n")
201
202     return "\n".join(lines)
203
204
205 def parse_entries(entries: List[Dict[str, Any]], level: int) -> str:
206     """Parse a list of entries"""
207     ignored = ["pad"]
208     lines = []
209     for entry in entries:
210         if isinstance(entry, dict):
211             # entries could be a list or a dictionary
212             field_name = entry.get("name", "")
213             if field_name in ignored:
214                 continue
215             type_ = entry.get("type")
216             if type_:
217                 field_name += f" ({inline(type_)})"
218             lines.append(
219                 rst_fields(field_name, sanitize(entry.get("doc", "")), level)
220             )
221         elif isinstance(entry, list):
222             lines.append(rst_list_inline(entry, level))
223         else:
224             lines.append(rst_bullet(inline(sanitize(entry)), level))
225
226     lines.append("\n")
227     return "\n".join(lines)
228
229
230 def parse_definitions(defs: Dict[str, Any], namespace: str) -> str:
231     """Parse definitions section"""
232     preprocessed = ["name", "entries", "members"]
233     ignored = ["render-max"]  # This is not printed
234     lines = []
235
236     for definition in defs:
237         lines.append(rst_section(namespace, 'definition', definition["name"]))
238         for k in definition.keys():
239             if k in preprocessed + ignored:
240                 continue
241             lines.append(rst_fields(k, sanitize(definition[k]), 0))
242
243         # Field list needs to finish with a new line
244         lines.append("\n")
245         if "entries" in definition:
246             lines.append(rst_paragraph(":entries:", 0))
247             lines.append(parse_entries(definition["entries"], 1))
248         if "members" in definition:
249             lines.append(rst_paragraph(":members:", 0))
250             lines.append(parse_entries(definition["members"], 1))
251
252     return "\n".join(lines)
253
254
255 def parse_attr_sets(entries: List[Dict[str, Any]], namespace: str) -> str:
256     """Parse attribute from attribute-set"""
257     preprocessed = ["name", "type"]
258     linkable = ["enum", "nested-attributes", "struct", "sub-message"]
259     ignored = ["checks"]
260     lines = []
261
262     for entry in entries:
263         lines.append(rst_section(namespace, 'attribute-set', entry["name"]))
264         for attr in entry["attributes"]:
265             type_ = attr.get("type")
266             attr_line = attr["name"]
267             if type_:
268                 # Add the attribute type in the same line
269                 attr_line += f" ({inline(type_)})"
270
271             lines.append(rst_subsubsection(attr_line))
272
273             for k in attr.keys():
274                 if k in preprocessed + ignored:
275                     continue
276                 if k in linkable:
277                     value = rst_ref(namespace, k, attr[k])
278                 else:
279                     value = sanitize(attr[k])
280                 lines.append(rst_fields(k, value, 0))
281             lines.append("\n")
282
283     return "\n".join(lines)
284
285
286 def parse_sub_messages(entries: List[Dict[str, Any]], namespace: str) -> str:
287     """Parse sub-message definitions"""
288     lines = []
289
290     for entry in entries:
291         lines.append(rst_section(namespace, 'sub-message', entry["name"]))
292         for fmt in entry["formats"]:
293             value = fmt["value"]
294
295             lines.append(rst_bullet(bold(value)))
296             for attr in ['fixed-header', 'attribute-set']:
297                 if attr in fmt:
298                     lines.append(rst_fields(attr,
299                                             rst_ref(namespace, attr, fmt[attr]),
300                                             1))
301             lines.append("\n")
302
303     return "\n".join(lines)
304
305
306 def parse_yaml(obj: Dict[str, Any]) -> str:
307     """Format the whole YAML into a RST string"""
308     lines = []
309
310     # Main header
311
312     lines.append(rst_header())
313
314     family = obj['name']
315
316     title = f"Family ``{family}`` netlink specification"
317     lines.append(rst_title(title))
318     lines.append(rst_paragraph(".. contents:: :depth: 3\n"))
319
320     if "doc" in obj:
321         lines.append(rst_subtitle("Summary"))
322         lines.append(rst_paragraph(obj["doc"], 0))
323
324     # Operations
325     if "operations" in obj:
326         lines.append(rst_subtitle("Operations"))
327         lines.append(parse_operations(obj["operations"]["list"], family))
328
329     # Multicast groups
330     if "mcast-groups" in obj:
331         lines.append(rst_subtitle("Multicast groups"))
332         lines.append(parse_mcast_group(obj["mcast-groups"]["list"]))
333
334     # Definitions
335     if "definitions" in obj:
336         lines.append(rst_subtitle("Definitions"))
337         lines.append(parse_definitions(obj["definitions"], family))
338
339     # Attributes set
340     if "attribute-sets" in obj:
341         lines.append(rst_subtitle("Attribute sets"))
342         lines.append(parse_attr_sets(obj["attribute-sets"], family))
343
344     # Sub-messages
345     if "sub-messages" in obj:
346         lines.append(rst_subtitle("Sub-messages"))
347         lines.append(parse_sub_messages(obj["sub-messages"], family))
348
349     return "\n".join(lines)
350
351
352 # Main functions
353 # ==============
354
355
356 def parse_arguments() -> argparse.Namespace:
357     """Parse arguments from user"""
358     parser = argparse.ArgumentParser(description="Netlink RST generator")
359
360     parser.add_argument("-v", "--verbose", action="store_true")
361     parser.add_argument("-o", "--output", help="Output file name")
362
363     # Index and input are mutually exclusive
364     group = parser.add_mutually_exclusive_group()
365     group.add_argument(
366         "-x", "--index", action="store_true", help="Generate the index page"
367     )
368     group.add_argument("-i", "--input", help="YAML file name")
369
370     args = parser.parse_args()
371
372     if args.verbose:
373         logging.basicConfig(level=logging.DEBUG)
374
375     if args.input and not os.path.isfile(args.input):
376         logging.warning("%s is not a valid file.", args.input)
377         sys.exit(-1)
378
379     if not args.output:
380         logging.error("No output file specified.")
381         sys.exit(-1)
382
383     if os.path.isfile(args.output):
384         logging.debug("%s already exists. Overwriting it.", args.output)
385
386     return args
387
388
389 def parse_yaml_file(filename: str) -> str:
390     """Transform the YAML specified by filename into a rst-formmated string"""
391     with open(filename, "r", encoding="utf-8") as spec_file:
392         yaml_data = yaml.safe_load(spec_file)
393         content = parse_yaml(yaml_data)
394
395     return content
396
397
398 def write_to_rstfile(content: str, filename: str) -> None:
399     """Write the generated content into an RST file"""
400     logging.debug("Saving RST file to %s", filename)
401
402     with open(filename, "w", encoding="utf-8") as rst_file:
403         rst_file.write(content)
404
405
406 def generate_main_index_rst(output: str) -> None:
407     """Generate the `networking_spec/index` content and write to the file"""
408     lines = []
409
410     lines.append(rst_header())
411     lines.append(rst_label("specs"))
412     lines.append(rst_title("Netlink Family Specifications"))
413     lines.append(rst_toctree(1))
414
415     index_dir = os.path.dirname(output)
416     logging.debug("Looking for .rst files in %s", index_dir)
417     for filename in sorted(os.listdir(index_dir)):
418         if not filename.endswith(".rst") or filename == "index.rst":
419             continue
420         lines.append(f"   {filename.replace('.rst', '')}\n")
421
422     logging.debug("Writing an index file at %s", output)
423     write_to_rstfile("".join(lines), output)
424
425
426 def main() -> None:
427     """Main function that reads the YAML files and generates the RST files"""
428
429     args = parse_arguments()
430
431     if args.input:
432         logging.debug("Parsing %s", args.input)
433         try:
434             content = parse_yaml_file(os.path.join(args.input))
435         except Exception as exception:
436             logging.warning("Failed to parse %s.", args.input)
437             logging.warning(exception)
438             sys.exit(-1)
439
440         write_to_rstfile(content, args.output)
441
442     if args.index:
443         # Generate the index RST file
444         generate_main_index_rst(args.output)
445
446
447 if __name__ == "__main__":
448     main()
This page took 0.053296 seconds and 4 git commands to generate.