1 | # Copyright 2014 Diamond Light Source Ltd. |
||
2 | # |
||
3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
||
4 | # you may not use this file except in compliance with the License. |
||
5 | # You may obtain a copy of the License at |
||
6 | # |
||
7 | # http://www.apache.org/licenses/LICENSE-2.0 |
||
8 | # |
||
9 | # Unless required by applicable law or agreed to in writing, software |
||
10 | # distributed under the License is distributed on an "AS IS" BASIS, |
||
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||
12 | # See the License for the specific language governing permissions and |
||
13 | # limitations under the License. |
||
14 | |||
15 | """ |
||
16 | .. module:: create_plugin_doc |
||
17 | :platform: Unix |
||
18 | :synopsis: A module to automatically create plugin documentation |
||
19 | |||
20 | .. moduleauthor:: Nicola Wadeson <[email protected]> |
||
21 | |||
22 | """ |
||
23 | |||
24 | import os |
||
25 | import re |
||
26 | import sys |
||
27 | |||
28 | from itertools import chain |
||
29 | |||
30 | import savu.plugins.utils as pu |
||
31 | |||
32 | |||
33 | def add_package_entry(f, files_present, output, module_name): |
||
34 | """Create a contents page for the files and directories contained |
||
35 | in 'files'. Create links to all the plugin classes which load without |
||
36 | errors |
||
37 | """ |
||
38 | if files_present: |
||
39 | # If files are present in this directory then, depending on the |
||
40 | # number of nested directories, determine which section heading |
||
41 | # and title to apply |
||
42 | title = module_name.replace("savu.", "").split(".") |
||
43 | if len(title) >= 1: |
||
44 | f.write(convert_title(title[len(title) - 1])) |
||
45 | f.write(set_underline(len(title) - 1, 56)) |
||
46 | # For directory contents |
||
47 | f.write("\n.. toctree::\n") |
||
48 | # Contents display level is set to have plugin names only |
||
49 | f.write(" :maxdepth: 1 \n\n") |
||
50 | |||
51 | for fi in files_present: |
||
52 | mod_path = module_name + "." + fi.split(".py")[0] |
||
53 | if "plugin" in output: |
||
54 | try: |
||
55 | # If the plugin class exists, put it's name into the contents |
||
56 | plugin_class = pu.load_class(mod_path) |
||
57 | file_path = get_path_format( |
||
58 | mod_path.replace("savu.", ""), output |
||
59 | ) |
||
60 | _write_to_contents(f, fi, output, file_path) |
||
61 | except ValueError: |
||
62 | pass |
||
63 | else: |
||
64 | file_path = get_path_format(mod_path, output) |
||
65 | _write_to_contents(f, fi, output, file_path) |
||
66 | f.write("\n\n") |
||
67 | |||
68 | |||
69 | def _write_to_contents(f, fi, output, file_path): |
||
70 | """Add the rst file name to the contents page |
||
71 | |||
72 | :param f: Contents file to write to |
||
73 | :param fi: file name |
||
74 | :param output: output directory at which rst files are located |
||
75 | :param file_path: path to file to include in contents page |
||
76 | """ |
||
77 | name = convert_title(fi.split(".py")[0]) |
||
78 | f.write(f" {name} <{output}/{file_path}>\n") |
||
79 | |||
80 | |||
81 | def set_underline(level: int, length: int) -> str: |
||
82 | """Create an underline string of a certain length |
||
83 | |||
84 | :param level: The underline level specifying the symbol to use |
||
85 | :param length: The string length |
||
86 | :return: Underline string |
||
87 | """ |
||
88 | underline_symbol = ["`", "#", "*", "-", "^", '"', "="] |
||
89 | symbol_str = underline_symbol[level] * length |
||
90 | return f"\n{symbol_str}\n" |
||
91 | |||
92 | |||
93 | def get_path_format(mod_path, output): |
||
94 | """Use the module path '.' file name for api documentation |
||
95 | Use the file path '/' file name for plugin documentation |
||
96 | |||
97 | :param mod_path: module path for file |
||
98 | :param output: the type of file output required eg. api or plugin |
||
99 | :return: string in correct path format |
||
100 | """ |
||
101 | if output == "plugin_documentation": |
||
102 | mod_path = mod_path.replace(".", "/") |
||
103 | return mod_path |
||
104 | |||
105 | |||
106 | def create_plugin_documentation(files, output, module_name, savu_base_path): |
||
107 | """Create a plugin rst file to explain its parameters, api etc |
||
108 | |||
109 | :param files: List of files in plugin directory |
||
110 | :param output: output directory |
||
111 | :param module_name: Name of plugin module |
||
112 | :param savu_base_path: |
||
113 | """ |
||
114 | plugin_guide_path = "plugin_guides/" |
||
115 | for fi in files: |
||
116 | py_module_name = module_name + "." + fi.split(".py")[0] |
||
117 | mod_path = py_module_name.replace("savu.", "") |
||
118 | file_path = get_path_format(mod_path, output) |
||
119 | try: |
||
120 | plugin_class = pu.load_class(py_module_name)() |
||
121 | except (ModuleNotFoundError, AttributeError) as er: |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
122 | p_name = py_module_name.split("plugins.")[1] |
||
123 | print(f"Cannot load {p_name}: {er}") |
||
124 | plugin_class = None |
||
125 | |||
126 | if plugin_class: |
||
127 | tools = plugin_class.get_plugin_tools() |
||
128 | tools._populate_default_parameters() |
||
129 | try: |
||
130 | plugin_tools = plugin_class.tools.tools_list |
||
131 | if plugin_tools: |
||
132 | # Create rst additional documentation directory |
||
133 | # and file and image directory |
||
134 | create_documentation_directory( |
||
135 | savu_base_path, plugin_guide_path, fi |
||
136 | ) |
||
137 | # Create an empty rst file inside this directory where |
||
138 | # the plugin tools documentation will be stored |
||
139 | full_file_path = f"{savu_base_path}doc/source/reference/"\ |
||
140 | f"{output}/{file_path}.rst" |
||
141 | pu.create_dir(full_file_path) |
||
142 | with open(full_file_path, "w+") as new_rst_file: |
||
143 | # Populate this file |
||
144 | populate_plugin_doc_files( |
||
145 | new_rst_file, |
||
146 | plugin_tools, |
||
147 | file_path, |
||
148 | plugin_class, |
||
149 | savu_base_path, |
||
150 | plugin_guide_path, |
||
151 | ) |
||
152 | except: |
||
153 | print(f"Tools file missing for {py_module_name}") |
||
154 | |||
155 | |||
156 | def convert_title(original_title): |
||
157 | """Remove underscores from string""" |
||
158 | new_title = original_title.replace("_", " ").title() |
||
159 | new_title = new_title.replace("Api", "API") |
||
160 | return new_title |
||
161 | |||
162 | |||
163 | def populate_plugin_doc_files( |
||
164 | new_rst_file, |
||
165 | tool_class_list, |
||
166 | file_path, |
||
167 | plugin_class, |
||
168 | savu_base_path, |
||
169 | plugin_guide_path, |
||
170 | ): |
||
171 | """Create the restructured text file containing parameter, citation |
||
172 | and documentation information for the plugin_class |
||
173 | |||
174 | :param new_rst_file: The new restructured text file which will hold all |
||
175 | of the plugin details for this plugin class |
||
176 | :param tool_class_list: The list of base tool classes for this plugin |
||
177 | :param file_path: Path to the plugin file |
||
178 | :param plugin_class: Plugin class |
||
179 | :param savu_base_path: Savu file path |
||
180 | :param plugin_guide_path: Plugin guides file path |
||
181 | """ |
||
182 | |||
183 | title = file_path.split("/") |
||
184 | mod_path_length = len(title) |
||
185 | title = convert_title(title[-1]) |
||
186 | # Depending on the number of nested directories, determine which section |
||
187 | # heading and title to apply |
||
188 | new_rst_file.write(f'{{% extends "plugin_template.rst" %}}\n') |
||
189 | new_rst_file.write(f"\n{{% block title %}}{title}{{% endblock %}}\n") |
||
190 | |||
191 | plugin_data = plugin_class.tools.get_param_definitions() |
||
192 | plugin_citations = plugin_class.tools.get_citations() |
||
193 | plugin_docstring = plugin_class.tools.get_doc() |
||
194 | |||
195 | tool_class = tool_class_list[-1] |
||
196 | docstring_info = plugin_docstring.get("verbose") |
||
197 | |||
198 | write_plugin_desc_to_file( |
||
199 | new_rst_file, docstring_info, plugin_guide_path, file_path |
||
200 | ) |
||
201 | write_parameters_to_file(new_rst_file, tool_class, plugin_data) |
||
202 | write_citations_to_file(new_rst_file, plugin_citations) |
||
203 | write_api_link_to_file(new_rst_file, file_path, mod_path_length) |
||
204 | |||
205 | |||
206 | def write_plugin_desc_to_file( |
||
207 | f, docstring_info, plugin_guide_path, file_path |
||
208 | ): |
||
209 | """Write the description to the plugin api |
||
210 | |||
211 | :param f: File to write to |
||
212 | :param docstring_info: Docstring content for a brief summary |
||
213 | :param plugin_guide_path: File path to the plugin guides (in depth) |
||
214 | :param file_path: File path of the plugin file |
||
215 | """ |
||
216 | if docstring_info: |
||
217 | f.write("\n{% block description %}\n") |
||
218 | f.write(docstring_info) |
||
219 | |||
220 | # Locate documentation file |
||
221 | doc_folder = savu_base_path + "doc/source/" |
||
0 ignored issues
–
show
|
|||
222 | file_str = f"{doc_folder}{plugin_guide_path}{file_path}_doc.rst" |
||
223 | inner_file_str = f"/../../../{plugin_guide_path}{file_path}_doc.rst" |
||
224 | if os.path.isfile(file_str): |
||
225 | # If there is a documentation file |
||
226 | f.write("\n") |
||
227 | f.write("\n.. toctree::") |
||
228 | f.write( |
||
229 | f"\n Plugin documention and guidelines" |
||
230 | f" on use <{inner_file_str}>" |
||
231 | ) |
||
232 | f.write("\n") |
||
233 | f.write("\n{% endblock %}\n") |
||
234 | |||
235 | |||
236 | def write_parameters_to_file(f, tool_class, plugin_data): |
||
237 | """Write the parameters to the plugin api |
||
238 | |||
239 | :param f: File to write to |
||
240 | :param tool_class: Tools class for plugin |
||
241 | :param plugin_data: Plugin data dict |
||
242 | """ |
||
243 | if tool_class.define_parameters.__doc__: |
||
244 | # Check define parameters exists |
||
245 | |||
246 | if plugin_data: |
||
247 | # Go through all plugin parameters |
||
248 | f.write("\n{% block parameter_yaml %}\n") |
||
249 | for p_name, p_dict in plugin_data.items(): |
||
250 | f.write( |
||
251 | "\n" |
||
252 | + pu.indent_multi_line_str( |
||
253 | get_parameter_info(p_name, p_dict), 2 |
||
254 | ) |
||
255 | ) |
||
256 | f.write("\n{% endblock %}\n") |
||
257 | |||
258 | |||
259 | def write_api_link_to_file(f, file_path, mod_path_length): |
||
260 | """Write the template block to link to the plugin api |
||
261 | |||
262 | :param f: File to write to |
||
263 | :param file_path: File path of the plugin file |
||
264 | :param mod_path_length: Module/path length to set the api file |
||
265 | directory correctly |
||
266 | """ |
||
267 | mod = file_path.replace("/", ".") |
||
268 | plugin_dir = get_path_to_directory(mod_path_length) |
||
269 | plugin_api = f"{plugin_dir}plugin_api/" |
||
270 | f.write("\n{% block plugin_file %}") |
||
271 | f.write(f"{plugin_api}{mod}.rst") |
||
272 | f.write("{% endblock %}\n") |
||
273 | |||
274 | |||
275 | def get_path_to_directory(mod_path_length): |
||
276 | """ Find the backward navigation to the main directory |
||
277 | :param mod_path_length: length of the plugin module/plugin path |
||
278 | :return:str to savu directory from plugin rst file |
||
279 | """ |
||
280 | count = 0 |
||
281 | plugin_dir = "" |
||
282 | while count < mod_path_length: |
||
283 | plugin_dir = f"../{plugin_dir}" |
||
284 | count += 1 |
||
285 | return plugin_dir |
||
286 | |||
287 | |||
288 | def get_parameter_info(p_name, parameter): |
||
289 | exclude_keys = ["display"] |
||
290 | parameter_info = p_name + ":\n" |
||
291 | try: |
||
292 | keys_display = { |
||
293 | k: v for k, v in parameter.items() if k not in exclude_keys |
||
294 | } |
||
295 | parameter_info = create_disp_format(keys_display, parameter_info) |
||
296 | except Exception as e: |
||
297 | print(str(e)) |
||
298 | return parameter_info |
||
299 | |||
300 | |||
301 | def create_disp_format(in_dict, disp_string, indent_level=1): |
||
302 | """Create specific documentation display string in yaml format |
||
303 | |||
304 | :param dict: dictionary to display |
||
305 | :param disp_string: input string to append to |
||
306 | :return: final display string |
||
307 | """ |
||
308 | for k, v in in_dict.items(): |
||
309 | list_display = isinstance(v, list) and indent_level > 1 |
||
310 | if isinstance(v, dict): |
||
311 | indent_level += 1 |
||
312 | str_dict = create_disp_format(v, "", indent_level) |
||
313 | indent_level -= 1 |
||
314 | str_val = f"{k}: \n{str_dict}" |
||
315 | elif list_display: |
||
316 | indent_level += 1 |
||
317 | list_str = "" |
||
318 | for item in v: |
||
319 | list_str += pu.indent(f"{item}\n", indent_level) |
||
320 | indent_level -= 1 |
||
321 | str_val = f"{k}: \n{list_str}" |
||
322 | elif isinstance(v, str): |
||
323 | # Check if the string contains characters which may need |
||
324 | # to be surrounded by quotes |
||
325 | v = v.strip() |
||
326 | str_val = f"{k}: {v}" if no_yaml_char(v) else f'{k}: "{v}"' |
||
327 | elif isinstance(v, type(None)): |
||
328 | str_val = f"{k}: None" |
||
329 | else: |
||
330 | str_val = f'{k}: "{v}"' |
||
331 | if not isinstance(v, dict) and not list_display: |
||
332 | # Don't append a new line for dictionary entries |
||
333 | str_val += "\n" |
||
334 | disp_string += pu.indent(str_val, indent_level) |
||
335 | return disp_string |
||
336 | |||
337 | |||
338 | def no_yaml_char(s): |
||
339 | """Check for characters which prevent the yaml syntax highlighter |
||
340 | from being applied. For example [] and ? and ' |
||
341 | """ |
||
342 | return bool(re.match(r"^[a-zA-Z0-9()%|#\"/._,+\-=: {}<>]*$", s)) |
||
343 | |||
344 | |||
345 | def write_citations_to_file(f, plugin_citations): |
||
346 | """Write the citations block to the plugin rst file |
||
347 | |||
348 | :param f: File to write to |
||
349 | :param plugin_citations: Plugin citations |
||
350 | """ |
||
351 | if plugin_citations: |
||
352 | # If documentation information is present, then display it |
||
353 | f.write("\n{% block plugin_citations %}\n") |
||
354 | citation_str = get_citation_str(plugin_citations) |
||
355 | f.write(pu.indent_multi_line_str(citation_str, 2)) |
||
356 | f.write("\n{% endblock %}\n") |
||
357 | else: |
||
358 | f.write("\n{% block plugin_citations %}\n") |
||
359 | f.write(pu.indent("No citations")) |
||
360 | f.write("\n{% endblock %}\n") |
||
361 | |||
362 | |||
363 | def get_citation_str(plugin_citations): |
||
364 | """Create the citation text format """ |
||
365 | cite_str = "" |
||
366 | for name, citation in plugin_citations.items(): |
||
367 | str_val = f"\n**{name.lstrip()}**\n" |
||
368 | |||
369 | if citation.dependency: |
||
370 | # If the citation is dependent upon a certain parameter value |
||
371 | # being chosen |
||
372 | for ( |
||
373 | citation_dependent_parameter, |
||
374 | citation_dependent_value, |
||
375 | ) in citation.dependency.items(): |
||
376 | str_val += f"\n(Please use this citation if you are using the {citation_dependent_value} {citation_dependent_parameter}\n" |
||
377 | |||
378 | bibtex = citation.bibtex |
||
379 | endnote = citation.endnote |
||
380 | # Where new lines are, append an indentation |
||
381 | if bibtex: |
||
382 | str_val += "\n**Bibtex**\n" |
||
383 | str_val += "\n.. code-block:: none" |
||
384 | str_val += "\n\n" |
||
385 | str_val += pu.indent_multi_line_str(bibtex, True) |
||
386 | str_val += "\n" |
||
387 | |||
388 | if endnote: |
||
389 | str_val += "\n**Endnote**\n" |
||
390 | str_val += "\n.. code-block:: none" |
||
391 | str_val += "\n\n" |
||
392 | str_val += pu.indent_multi_line_str(endnote, True) |
||
393 | str_val += "\n" |
||
394 | |||
395 | cite_str += f"{str_val}\n" |
||
396 | return cite_str |
||
397 | |||
398 | |||
399 | def create_documentation_directory(savu_base_path, |
||
400 | plugin_guide_path, |
||
401 | plugin_file): |
||
402 | """ Create plugin directory inside documentation and |
||
403 | documentation file and image folders |
||
404 | """ |
||
405 | # Create directory inside |
||
406 | doc_path = f"{savu_base_path}doc/source/" |
||
407 | doc_image_path = ( |
||
408 | f"{savu_base_path}doc/source/files_and_images/" |
||
409 | f"{plugin_guide_path}plugins/" |
||
410 | ) |
||
411 | |||
412 | # find the directories to create |
||
413 | doc_dir = doc_path + plugin_guide_path + plugin_file |
||
414 | image_dir = doc_image_path + plugin_file |
||
415 | pu.create_dir(doc_dir) |
||
416 | pu.create_dir(image_dir) |
||
417 | |||
418 | |||
419 | def _select_relevant_files(api_type): |
||
420 | """Select the folder related to the api_type |
||
421 | Exclude certain files and directories based on api_type |
||
422 | |||
423 | :param api_type: framework or plugin api |
||
424 | """ |
||
425 | if api_type == "framework": |
||
426 | base_path = savu_base_path + "savu" |
||
427 | exclude_dir = ["__pycache__", "test", "plugins"] |
||
428 | exclude_file = ["__init__.py", "win_readline.py"] |
||
429 | elif api_type == "plugin": |
||
430 | base_path = savu_base_path + "savu/plugins" |
||
431 | exclude_file = [ |
||
432 | "__init__.py", |
||
433 | "docstring_parser.py", |
||
434 | "plugin.py", |
||
435 | "plugin_datasets.py", |
||
436 | "plugin_datasets_notes.py", |
||
437 | "utils.py", |
||
438 | "plugin_tools.py", |
||
439 | ] |
||
440 | exclude_dir = [ |
||
441 | "driver", |
||
442 | "utils", |
||
443 | "unregistered", |
||
444 | "under_revision", |
||
445 | "templates", |
||
446 | "__pycache__", |
||
447 | "test", |
||
448 | ] |
||
449 | else: |
||
450 | raise Exception("Unknown API type", api_type) |
||
451 | return base_path, exclude_file, exclude_dir |
||
452 | |||
453 | |||
454 | def _create_api_content( |
||
455 | savu_base_path, |
||
456 | out_folder, |
||
457 | api_type, |
||
458 | base_path, |
||
459 | exclude_file, |
||
460 | exclude_dir, |
||
461 | f, |
||
462 | ): |
||
463 | """Populate API contents pages""" |
||
464 | for root, dirs, files in os.walk(base_path, topdown=True): |
||
465 | tools_files = [fi for fi in files if "tools" in fi] |
||
466 | template_files = [fi for fi in files if "template" in fi] |
||
467 | base_files = [fi for fi in files if fi.startswith("base")] |
||
468 | driver_files = [fi for fi in files if "driver" in fi] |
||
469 | exclude_files = [ |
||
470 | exclude_file, |
||
471 | tools_files, |
||
472 | base_files, |
||
473 | driver_files, |
||
474 | template_files, |
||
475 | ] |
||
476 | dirs[:] = [d for d in dirs if d not in exclude_dir] |
||
477 | files[:] = [fi for fi in files if fi not in chain(*exclude_files)] |
||
478 | files[:] = [fi for fi in files if fi.split(".")[-1] == "py"] |
||
479 | |||
480 | # Exclude the tools files from html view sidebar |
||
481 | if "__" not in root: |
||
482 | pkg_path = root.split("Savu/")[1] |
||
483 | module_name = pkg_path.replace("/", ".") |
||
484 | if "plugins" in module_name and api_type == "plugin": |
||
485 | add_package_entry(f, files, out_folder, module_name) |
||
486 | if out_folder == "plugin_documentation": |
||
487 | create_plugin_documentation( |
||
488 | files, out_folder, module_name, savu_base_path |
||
489 | ) |
||
490 | elif api_type == "framework": |
||
491 | add_package_entry(f, files, out_folder, module_name) |
||
492 | |||
493 | |||
494 | if __name__ == "__main__": |
||
495 | out_folder, rst_file, api_type = sys.argv[1:] |
||
496 | |||
497 | # determine Savu base path |
||
498 | main_dir = os.path.dirname(os.path.realpath(__file__)).split("/Savu/")[0] |
||
499 | savu_base_path = f"{main_dir}/Savu/" |
||
500 | |||
501 | base_path, exclude_file, exclude_dir = _select_relevant_files(api_type) |
||
502 | |||
503 | # Create the directory if it does not exist |
||
504 | pu.create_dir(f"{savu_base_path}doc/source/reference/{out_folder}") |
||
505 | |||
506 | # open the autosummary file |
||
507 | with open(f"{savu_base_path}doc/source/reference/{rst_file}", "w") as f: |
||
508 | |||
509 | document_title = convert_title(out_folder) |
||
510 | f.write(".. _" + out_folder + ":\n") |
||
511 | f.write( |
||
512 | f"{set_underline(2,22)}{document_title} " |
||
513 | f"{set_underline(2,22)}\n" |
||
514 | ) |
||
515 | |||
516 | _create_api_content( |
||
517 | savu_base_path, |
||
518 | out_folder, |
||
519 | api_type, |
||
520 | base_path, |
||
521 | exclude_file, |
||
522 | exclude_dir, |
||
523 | f, |
||
524 | ) |
||
525 |