doc.create_plugin_doc.no_yaml_char()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
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
The variable ModuleNotFoundError does not seem to be defined.
Loading history...
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
introduced by
The variable savu_base_path does not seem to be defined in case __name__ == "__main__" on line 494 is False. Are you sure this can never be the case?
Loading history...
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