doc gen: fix doc generator
[csit.git] / resources / tools / doc_gen / gen_rst.py
1 # Copyright (c) 2021 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13 from os import walk, listdir, scandir, environ
14 from os.path import isfile, isdir, join, getsize
15
16 # Temporary working directory. It is created and deleted by docs.sh
17 WORKING_DIR = environ.get("WORKING_DIR")
18
19 # Directory with resources to be documented.
20 RESOURCES_DIR = u"resources"
21
22 # Directory with libraries (python, robot) to be documented.
23 LIB_DIR = u"libraries"
24
25 # Directory with tests (func, perf) to be documented.
26 TESTS_DIR = u"tests"
27
28 PY_EXT = u".py"
29 RF_EXT = u".robot"
30
31 PATH_PY_LIBS = join(WORKING_DIR, RESOURCES_DIR, LIB_DIR, u"python")
32 PATH_RF_LIBS = join(WORKING_DIR, RESOURCES_DIR, LIB_DIR, u"robot")
33 PATH_TESTS = join(WORKING_DIR, TESTS_DIR)
34
35 # Sections in rst files
36 rst_toc = u"""
37 .. toctree::
38 """
39
40 rst_py_module = u"""
41 .. automodule:: {}.{}
42     :members:
43     :undoc-members:
44     :show-inheritance:
45 """
46
47 rst_rf_suite_setup = u"""
48 .. robot-settings::
49    :source: {}
50 """
51
52 rst_rf_variables = u"""
53 .. robot-variables::
54    :source: {}
55 """
56
57 rst_rf_keywords = u"""
58 .. robot-keywords::
59    :source: {}
60 """
61
62 rst_rf_tests = u"""
63 .. robot-tests::
64    :source: {}
65 """
66
67
68 def get_files(path, extension):
69     """Generates the list of files to process.
70
71     :param path: Path to files.
72     :param extension: Extension of files to process. If it is the empty string,
73     all files will be processed.
74     :type path: str
75     :type extension: str
76     :returns: List of files to process.
77     :rtype: list
78     """
79
80     file_list = list()
81     for root, dirs, files in walk(path):
82         for filename in files:
83             if extension:
84                 if filename.endswith(extension) and u"__init__" not in filename:
85                     file_list.append(join(root, filename))
86             else:
87                 file_list.append(join(root, filename))
88
89     return file_list
90
91
92 def create_file_name(path, start):
93     """Create the name of rst file.
94
95     Example:
96     tests.perf.rst
97
98     :param path: Path to a module to be documented.
99     :param start: The first directory in path which is used in the file name.
100     :type path: str
101     :type start: str
102     :returns: File name.
103     :rtype: str
104     """
105     dir_list = path.split(u"/")
106     start_index = dir_list.index(start)
107     return u".".join(dir_list[start_index:-1]) + u".rst"
108
109
110 def create_rst_file_names_set(files, start):
111     """Generate a set of unique rst file names.
112
113     :param files: List of all files to be documented with path beginning in the
114     working directory.
115     :param start: The first directory in path which is used in the file name.
116     :type files: list
117     :type start: str
118     :returns: Set of unique rst file names.
119     :rtype: set
120     """
121     file_names = set()
122     for file in files:
123         file_names.add(create_file_name(file, start))
124     return file_names
125
126
127 def add_nested_folders_in_rst_set(file_names, path):
128     """Add RST files from folders where are only folders without tests.
129
130     :param file_names: List of all files to be documented with path beginning
131         in the working directory.
132     :param path: Path where it starts adding missing RST files.
133     :type file_names: list
134     :type path: str
135     """
136
137     # When we split directory tree by "/" we don't need to create RST file in
138     # folders in depth <= 5. It's because the WORKING_DIR folder structure i
139     # as following:
140     # /tmp/tmp-csitXXX/tests/<subject_of_test>/<type_of_test>/<what_is_tested>
141     # That splits to ie:
142     # ['', 'tmp', 'tmp-csitXXX', 'tests', 'vpp', 'device', 'container_memif']
143     # We need to generate RST files for folders after <subject_of_test> which
144     # is in depth > 5
145
146     for directory in fast_scandir(path):
147         dir_list = directory.split(u"/")
148         if len(dir_list) > 5:
149             # cut ['', 'tmp', 'tmp-csitXXX']
150             dir_rst = u".".join(dir_list[3:]) + u".rst"
151             if dir_rst not in file_names and u"__pycache__" not in dir_rst:
152                 file_names.add(dir_rst)
153
154
155 def scan_dir(path):
156     """Create a list of files and directories in the given directory.
157
158     :param path: Path to the directory.
159     :type path: str
160     :returns: List of directories and list of files sorted in alphabetical
161     order.
162     :rtype: tuple of two lists
163     """
164     files = list()
165     dirs = list()
166     items = listdir(path)
167     for item in items:
168         if isfile(join(path, item)) and u"__init__" not in item:
169             files.append(item)
170         elif isdir(join(path, item)):
171             dirs.append(item)
172     return sorted(dirs), sorted(files)
173
174
175 def write_toc(fh, path, dirs):
176     """Write a table of contents to given rst file.
177
178     :param fh: File handler of the rst file.
179     :param path: Path to package.
180     :param dirs: List of directories to be included in ToC.
181     :type fh: BinaryIO
182     :type path: str
183     :type dirs: list
184     """
185     fh.write(rst_toc)
186     for directory in dirs:
187         fh.write(f"    {u'.'.join(path)}.{directory}\n")
188
189
190 def write_module_title(fh, module_name):
191     """Write the module title to the given rst file. The title will be on the
192     second level.
193
194     :param fh: File handler of the rst file.
195     :param module_name: The name of module used for title.
196     :type fh: BinaryIO
197     :type module_name: str
198     """
199     title = f"{module_name} suite"
200     fh.write(f"\n{title}\n{u'-' * len(title)}")
201
202
203 def generate_py_rst_files():
204     """Generate all rst files for all python modules."""
205
206     dirs_ignore_list = [u"__pycache__", ]
207
208     py_libs = get_files(PATH_PY_LIBS, PY_EXT)
209     file_names = create_rst_file_names_set(py_libs, RESOURCES_DIR)
210
211     for file_name in file_names:
212         path = join(WORKING_DIR, *file_name.split(u".")[:-1])
213         dirs, files = scan_dir(path)
214
215         for item in dirs_ignore_list:
216             while True:
217                 try:
218                     dirs.remove(item)
219                 except ValueError:
220                     break
221
222         full_path = join(WORKING_DIR, file_name)
223         with open(full_path, mode="a") as fh:
224             if getsize(full_path) == 0:
225                 package = file_name.split(u".")[-2]
226                 fh.write(f"{package}\n")
227                 fh.write(u"=" * len(f"{package}"))
228             module_path = file_name.split(u".")[:-1]
229             if dirs:
230                 write_toc(fh, module_path, dirs)
231             for file in files:
232                 module_name = file.split(u".")[0]
233                 write_module_title(fh, module_name)
234                 fh.write(rst_py_module.format(
235                     u".".join(module_path), module_name)
236                 )
237
238
239 def generate_rf_rst_files(
240         file_names, incl_tests=True, incl_keywords=True, incl_suite_setup=False,
241         incl_variables=False):
242     """Generate rst files for the given robot modules.
243
244     :param file_names: List of file names to be included in the documentation
245     (rst files).
246     :param incl_tests: If True, tests will be included in the documentation.
247     :param incl_keywords: If True, keywords will be included in the
248     documentation.
249     :param incl_suite_setup: If True, the suite setup will be included in the
250     documentation.
251     :param incl_variables: If True, the variables will be included in the
252     documentation.
253     :type file_names: set
254     :type incl_tests: bool
255     :type incl_keywords: bool
256     :type incl_suite_setup: bool
257     :type incl_variables: bool
258     """
259
260     for file_name in file_names:
261         path = join(WORKING_DIR, *file_name.split(u".")[:-1])
262         dirs, files = scan_dir(path)
263
264         full_path = join(WORKING_DIR, file_name)
265         with open(full_path, mode="a") as fh:
266             if getsize(full_path) == 0:
267                 package = file_name.split(u".")[-2]
268                 fh.write(f"{package}\n")
269                 fh.write(u"=" * len(f"{package}") + u"\n")
270             module_path = file_name.split(u".")[:-1]
271             if dirs:
272                 write_toc(fh, module_path, dirs)
273             for file in files:
274                 module_name = file.split(u".")[0]
275                 write_module_title(fh, module_name)
276                 path = join(join(*module_path), module_name + RF_EXT)
277                 if incl_suite_setup:
278                     fh.write(rst_rf_suite_setup.format(path))
279                 if incl_variables:
280                     fh.write(rst_rf_variables.format(path))
281                 if incl_keywords:
282                     fh.write(rst_rf_keywords.format(path))
283                 if incl_tests:
284                     fh.write(rst_rf_tests.format(path))
285
286
287 def generate_kw_rst_files():
288     """Generate all rst files for all robot modules with keywords in libraries
289     directory (no tests)."""
290
291     rf_libs = get_files(PATH_RF_LIBS, RF_EXT)
292     file_names = create_rst_file_names_set(rf_libs, RESOURCES_DIR)
293
294     generate_rf_rst_files(file_names, incl_tests=False)
295
296
297 def generate_tests_rst_files():
298     """Generate all rst files for all robot modules with tests in tests
299     directory. Include also keywords defined in these modules."""
300
301     tests = get_files(PATH_TESTS, RF_EXT)
302     file_names = create_rst_file_names_set(tests, TESTS_DIR)
303     add_nested_folders_in_rst_set(file_names, PATH_TESTS)
304
305     generate_rf_rst_files(
306         file_names, incl_suite_setup=True, incl_variables=True
307     )
308
309
310 def fast_scandir(dirname):
311     subfolders = [f.path for f in scandir(dirname) if f.is_dir()]
312     for dirname in list(subfolders):
313         subfolders.extend(fast_scandir(dirname))
314     return subfolders
315
316
317 if __name__ == u"__main__":
318
319     # Generate all rst files:
320     generate_py_rst_files()
321     generate_kw_rst_files()
322     generate_tests_rst_files()