1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
import ast
import sys
from pathlib import Path
"""
This program takes all the Machine class methods and prints its methods in
markdown-style. These can then be included in the NixOS test driver
markdown style, assuming the docstrings themselves are also in markdown.
These are included in the test driver documentation in the NixOS manual.
See https://nixos.org/manual/nixos/stable/#ssec-machine-objects
The python input looks like this:
```py
...
class Machine(...):
...
def some_function(self, param1, param2):
""
documentation string of some_function.
foo bar baz.
""
...
```
Output will be:
```markdown
...
some_function(param1, param2)
: documentation string of some_function.
foo bar baz.
...
```
"""
def function_docstrings(functions: list[ast.FunctionDef]) -> str | None:
"""Extracts docstrings from a list of function definitions."""
documented_functions = [f for f in functions if ast.get_docstring(f) is not None]
if not documented_functions:
return None
docstrings = []
for function in documented_functions:
docstr = ast.get_docstring(function)
assert docstr is not None
args = ", ".join(a.arg for a in function.args.args[1:])
args = f"({args})"
docstr = "\n".join(f" {line}" for line in docstr.strip().splitlines())
docstrings.append(f"{function.name}{args}\n\n:{docstr[1:]}\n")
return "\n".join(docstrings)
def machine_methods(
class_name: str, class_definitions: list[ast.ClassDef]
) -> list[ast.FunctionDef]:
"""
Given a class name and a list of class definitions, returns the list of
function definitions for the class matching the given name.
"""
machine_class = next(filter(lambda x: x.name == class_name, class_definitions))
assert machine_class is not None
methods = [node for node in machine_class.body if isinstance(node, ast.FunctionDef)]
methods.sort(key=lambda x: x.name)
# Do not document internal functions prefixed with underscore
methods = [m for m in methods if not m.name.startswith("_")]
return methods
def main() -> None:
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <path-to-test-driver>")
sys.exit(1)
module = ast.parse(Path(sys.argv[1]).read_text())
class_definitions = [node for node in module.body if isinstance(node, ast.ClassDef)]
base_machine_methods = machine_methods("BaseMachine", class_definitions)
base_method_names = {method.name for method in base_machine_methods}
qemu_machine_methods = [
method
for method in machine_methods("QemuMachine", class_definitions)
if method.name not in base_method_names
]
nspawn_machine_methods = [
method
for method in machine_methods("NspawnMachine", class_definitions)
if method.name not in base_method_names
]
print("#### Generic machine objects {#ssec-all-machine-objects} \n")
print(function_docstrings(base_machine_methods))
print("#### QEMU VM objects {#ssec-qemu-machine-objects}\n")
print(
function_docstrings(qemu_machine_methods)
or "No methods specific to QEMU virtual machines."
)
print("#### `systemd-nspawn` container objects {#ssec-nspawn-machine-objects}\n")
print(
function_docstrings(nspawn_machine_methods)
or "No methods specific to `systemd-nspawn` containers."
)
if __name__ == "__main__":
main()
|