Test Failed
Pull Request — master (#853)
by
unknown
04:30
created

doc.create_plugin_doc   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 448
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 258
dl 0
loc 448
rs 5.5199
c 0
b 0
f 0
wmc 56

15 Functions

Rating   Name   Duplication   Size   Complexity  
B _create_api_content() 0 31 7
A _select_relevant_files() 0 36 3
C create_disp_format() 0 35 10
B populate_plugin_doc_files() 0 72 7
B write_citations_to_file() 0 39 6
B create_plugin_documentation() 0 40 7
A _write_to_contents() 0 10 1
A create_documentation_directory() 0 16 1
B add_package_entry() 0 34 6
A no_yaml_char() 0 5 1
A convert_title() 0 5 1
A get_path_format() 0 11 2
A get_parameter_info() 0 9 2
A set_heading() 0 9 1
A set_underline() 0 10 1

How to fix   Complexity   

Complexity

Complex classes like doc.create_plugin_doc often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
44
        if len(title) >= 1:
45
            f.write(set_heading(title[len(title) - 1], len(title) - 1))
46
47
        # For directory contents
48
        f.write("\n.. toctree::\n")
49
        # Contents display level is set to have plugin names only
50
        f.write("   :maxdepth: 1 \n\n")
51
52
        for fi in files_present:
53
            mod_path = module_name + "." + fi.split(".py")[0]
54
            if "plugin" in output:
55
                try:
56
                    # If the plugin class exists, put it's name into the contents
57
                    plugin_class = pu.load_class(mod_path)
58
                    file_path = get_path_format(mod_path.replace("savu.", ""),
59
                                                output)
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 set_heading(title: str, level: int) -> str:
94
    """Return the plugin heading string
95
96
    :param title: Plugin title
97
    :param level: Heading underline level
98
    :return: Heading string
99
    """
100
    plugin_type = convert_title(title)
101
    return f"{plugin_type}{set_underline(level, 56)}"
102
103
104
def get_path_format(mod_path, output):
105
    """Use the module path '.' file name for api documentation
106
    Use the file path '/' file name for plugin documentation
107
108
    :param mod_path: module path for file
109
    :param output: the type of file output required eg. api or plugin
110
    :return: string in correct path format
111
    """
112
    if output == "plugin_documentation":
113
        mod_path = mod_path.replace(".", "/")
114
    return mod_path
115
116
117
def create_plugin_documentation(files, output, module_name, savu_base_path):
118
    plugin_guide_path = "plugin_guides/"
119
    for fi in files:
120
        py_module_name = module_name + "." + fi.split(".py")[0]
121
        mod_path = py_module_name.replace("savu.", "")
122
        file_path = get_path_format(mod_path, output)
123
        try:
124
            plugin_class = pu.load_class(py_module_name)()
125
        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...
126
            p_name = py_module_name.split('plugins.')[1]
127
            print(f"Cannot load {p_name}: {er}")
128
            plugin_class = None
129
130
        if plugin_class:
131
            tools = plugin_class.get_plugin_tools()
132
            tools._populate_default_parameters()
133
            try:
134
                plugin_tools = plugin_class.tools.tools_list
135
                if plugin_tools:
136
                    # Create rst additional documentation directory
137
                    # and file and image directory
138
                    create_documentation_directory(
139
                        savu_base_path, plugin_guide_path, fi
140
                    )
141
                    # Create an empty rst file inside this directory where
142
                    # the plugin tools documentation will be stored
143
                    full_file_path = f"{savu_base_path}doc/source/reference/{output}/{file_path}.rst"
144
                    pu.create_dir(full_file_path)
145
                    with open(full_file_path, "w+") as new_rst_file:
146
                        # Populate this file
147
                        populate_plugin_doc_files(
148
                            new_rst_file,
149
                            plugin_tools,
150
                            file_path,
151
                            plugin_class,
152
                            savu_base_path,
153
                            plugin_guide_path,
154
                        )
155
            except:
156
                print(f"Tools file missing for {py_module_name}")
157
158
159
def convert_title(original_title):
160
    """Remove underscores from string"""
161
    new_title = original_title.replace("_", " ").title()
162
    new_title = new_title.replace("Api", "API")
163
    return new_title
164
165
166
def populate_plugin_doc_files(new_rst_file, tool_class_list, file_path,
167
                     plugin_class, savu_base_path, plugin_guide_path):
168
    """Create the restructured text file containing parameter, citation
169
    and documentation information for the plugin_class
170
171
    :param new_rst_file: The new restructured text file which will hold all
172
                            of the plugin details for this plugin class
173
    :param tool_class_list: The list of base tool classes for this plugin
174
    :param file_path: Path to the plugin file
175
    :param plugin_class: Plugin class
176
    :param savu_base_path: Savu file path
177
    """
178
179
    title = file_path.split("/")
180
    # Depending on the number of nested directories, determine which section
181
    # heading and title to apply
182
    new_rst_file.write(set_heading(title[-1], 1))
183
184
    plugin_data = plugin_class.tools.get_param_definitions()
185
    plugin_citations = plugin_class.tools.get_citations()
186
    plugin_docstring = plugin_class.tools.get_doc()
187
188
    tool_class = tool_class_list[-1]
189
    docstring_info = plugin_docstring.get("verbose")
190
191
    if docstring_info:
192
        new_rst_file.write(f"\nDescription{set_underline(3,26)}")
193
        new_rst_file.write("\n")
194
        new_rst_file.write(docstring_info)
195
        new_rst_file.write("\n")
196
197
        # Locate documentation file
198
        doc_folder = savu_base_path + "doc/source/"
199
        file_str = f"{doc_folder}{plugin_guide_path}{file_path}_doc.rst"
200
        inner_file_str = f"/../../../{plugin_guide_path}{file_path}_doc.rst"
201
        if os.path.isfile(file_str):
202
            # If there is a documentation file
203
            new_rst_file.write("\n")
204
            new_rst_file.write(".. toctree::")
205
            new_rst_file.write(f"\n    Plugin documention and guidelines"
206
                               f" on use <{inner_file_str}>")
207
            new_rst_file.write("\n")
208
209
    if tool_class.define_parameters.__doc__:
210
        # Check define parameters exists
211
        new_rst_file.write(f"\nParameter definitions{set_underline(3,26)}")
212
        new_rst_file.write("\n.. code-block:: yaml")
213
        new_rst_file.write("\n")
214
215
        if plugin_data:
216
            # Go through all plugin parameters
217
            for p_name, p_dict in plugin_data.items():
218
                new_rst_file.write("\n"
219
                       + pu.indent_multi_line_str
220
                           (get_parameter_info(p_name, p_dict), 2))
221
222
        # Key to explain parameters
223
        new_rst_file.write(f"\nKey{set_underline(4,10)}")
224
        new_rst_file.write("\n")
225
        new_rst_file.write(
226
            ".. literalinclude:: "
227
            "/../source/files_and_images/"
228
            + plugin_guide_path
229
            + "short_parameter_key.yaml"
230
        )
231
        new_rst_file.write("\n    :language: yaml\n")
232
233
    if plugin_citations:
234
        # If documentation information is present, then display it
235
        new_rst_file.write(f"\nCitations{set_underline(3,26)}")
236
237
        write_citations_to_file(new_rst_file, plugin_citations)
238
239
240
def get_parameter_info(p_name, parameter):
241
    exclude_keys = ["display"]
242
    parameter_info = p_name + ":\n"
243
    try:
244
        keys_display = {k:v for k,v in parameter.items() if k not in exclude_keys}
245
        parameter_info = create_disp_format(keys_display, parameter_info)
246
    except Exception as e:
247
        print(str(e))
248
    return parameter_info
249
250
251
def create_disp_format(in_dict, disp_string, indent_level=1):
252
    """ Create specific documentation display string in yaml format
253
254
    :param dict: dictionary to display
255
    :param disp_string: input string to append to
256
    :return: final display string
257
    """
258
    for k, v in in_dict.items():
259
        list_display = isinstance(v, list) and indent_level > 1
260
        if isinstance(v, dict):
261
            indent_level += 1
262
            str_dict = create_disp_format(v, "", indent_level)
263
            indent_level -= 1
264
            str_val= f"{k}: \n{str_dict}"
265
        elif list_display:
266
            indent_level += 1
267
            list_str = ''
268
            for item in v:
269
                list_str += pu.indent(f"{item}\n", indent_level)
270
            indent_level -= 1
271
            str_val = f"{k}: \n{list_str}"
272
        elif isinstance(v, str):
273
            # Check if the string contains characters which may need
274
            # to be surrounded by quotes
275
            v = v.strip()
276
            str_val = f'{k}: {v}' if no_yaml_char(v) else f'{k}: "{v}"'
277
        elif isinstance(v, type(None)):
278
            str_val = f"{k}: None"
279
        else:
280
            str_val = f'{k}: "{v}"'
281
        if not isinstance(v, dict) and not list_display:
282
            # Don't append a new line for dictionary entries
283
            str_val += "\n"
284
        disp_string += pu.indent(str_val, indent_level)
285
    return disp_string
286
287
288
def no_yaml_char(s):
289
    """Check for characters which prevent the yaml syntax highlighter
290
    from being applied. For example [] and ? and '
291
    """
292
    return bool(re.match(r"^[a-zA-Z0-9()%|#\"/._,+\-=: {}<>]*$", s))
293
294
295
def write_citations_to_file(new_rst_file, plugin_citations):
296
    """Create the citation text format """
297
    for name, citation in plugin_citations.items():
298
        new_rst_file.write(
299
            f"\n{name.lstrip()}{set_underline(4, 182).rstrip()}"
300
        )
301
        if citation.dependency:
302
            # If the citation is dependent upon a certain parameter value
303
            # being chosen
304
            for (
305
                citation_dependent_parameter,
306
                citation_dependent_value,
307
            ) in citation.dependency.items():
308
                new_rst_file.write(
309
                    "\n(Please use this citation if "
310
                    "you are using the "
311
                    + citation_dependent_value
312
                    + " "
313
                    + citation_dependent_parameter
314
                    + ")\n"
315
                )
316
        bibtex = citation.bibtex
317
        endnote = citation.endnote
318
        # Where new lines are, append an indentation
319
        if bibtex:
320
            new_rst_file.write(f"\nBibtex{set_underline(5,42)}")
321
            new_rst_file.write("\n.. code-block:: none")
322
            new_rst_file.write("\n\n")
323
            new_rst_file.write(pu.indent_multi_line_str(bibtex, True))
324
            new_rst_file.write("\n")
325
326
        if endnote:
327
            new_rst_file.write(f"\nEndnote{set_underline(5,42)}")
328
            new_rst_file.write("\n.. code-block:: none")
329
            new_rst_file.write("\n\n")
330
            new_rst_file.write(pu.indent_multi_line_str(endnote, True))
331
            new_rst_file.write("\n")
332
333
    new_rst_file.write("\n")
334
335
336
def create_documentation_directory(savu_base_path,
337
                                   plugin_guide_path,
338
                                   plugin_file):
339
    """ Create plugin directory inside documentation and
340
    documentation file and image folders
341
    """
342
    # Create directory inside
343
    doc_path = f"{savu_base_path}doc/source/"
344
    doc_image_path = f"{savu_base_path}doc/source/files_and_images/" \
345
                     f"{plugin_guide_path}plugins/"
346
347
    # find the directories to create
348
    doc_dir = doc_path + plugin_guide_path + plugin_file
349
    image_dir = doc_image_path + plugin_file
350
    pu.create_dir(doc_dir)
351
    pu.create_dir(image_dir)
352
353
354
def _select_relevant_files(api_type):
355
    """ Select the folder related to the api_type
356
    Exclude certain files and directories based on api_type
357
358
    :param api_type: framework or plugin api
359
    :return: The base file path for the api files to document
360
       The list of files to exclude from api
361
    """
362
    if api_type == 'framework':
363
        base_path = savu_base_path + "savu"
364
        exclude_dir = ["__pycache__",
365
                       "test",
366
                       "plugins"]
367
        exclude_file = ["__init__.py", "win_readline.py"]
368
    elif api_type == 'plugin':
369
        base_path = savu_base_path + "savu/plugins"
370
        exclude_file = [
371
            "__init__.py",
372
            "docstring_parser.py",
373
            "plugin.py",
374
            "plugin_datasets.py",
375
            "plugin_datasets_notes.py",
376
            "utils.py",
377
            "plugin_tools.py",
378
        ]
379
        exclude_dir = ["driver",
380
                       "utils",
381
                       "unregistered",
382
                       "under_revision",
383
                       "templates",
384
                       "__pycache__",
385
                       "test"
386
                       ]
387
    else:
388
        raise Exception('Unknown API type', api_type)
389
    return base_path, exclude_file, exclude_dir
390
391
392
def _create_api_content(savu_base_path, out_folder, api_type,
393
                        base_path, exclude_file, exclude_dir, f):
394
    """Populate API contents pages"""
395
    for root, dirs, files in os.walk(base_path, topdown=True):
396
        tools_files = [fi for fi in files if "tools" in fi]
397
        template_files = [fi for fi in files if "template" in fi]
398
        base_files = [fi for fi in files if fi.startswith("base")]
399
        driver_files = [fi for fi in files if "driver" in fi]
400
        exclude_files = [
401
            exclude_file,
402
            tools_files,
403
            base_files,
404
            driver_files,
405
            template_files
406
        ]
407
        dirs[:] = [d for d in dirs if d not in exclude_dir]
408
        files[:] = [fi for fi in files if fi not in chain(*exclude_files)]
409
        files[:] = [fi for fi in files if fi.split('.')[-1] == 'py']
410
411
        # Exclude the tools files from html view sidebar
412
        if "__" not in root:
413
            pkg_path = root.split("Savu/")[1]
414
            module_name = pkg_path.replace("/", ".")
415
            if "plugins" in module_name and api_type == 'plugin':
416
                add_package_entry(f, files, out_folder, module_name)
417
                if out_folder == "plugin_documentation":
418
                    create_plugin_documentation(
419
                        files, out_folder, module_name, savu_base_path
420
                    )
421
            elif api_type == 'framework':
422
                add_package_entry(f, files, out_folder, module_name)
423
424
425
if __name__ == "__main__":
426
    out_folder, rst_file, api_type = sys.argv[1:]
427
428
    # determine Savu base path
429
    main_dir = \
430
        os.path.dirname(os.path.realpath(__file__)).split("/Savu/")[0]
431
    savu_base_path = f"{main_dir}/Savu/"
432
433
    base_path, exclude_file, exclude_dir = _select_relevant_files(api_type)
434
435
    # Create the directory if it does not exist
436
    pu.create_dir(f"{savu_base_path}doc/source/reference/{out_folder}")
437
438
    # open the autosummary file
439
    with open(f"{savu_base_path}doc/source/reference/{rst_file}", "w") as f:
440
441
        document_title = convert_title(out_folder)
442
        f.write(".. _" + out_folder + ":\n")
443
        f.write(f"{set_underline(2,22)}{document_title} "
444
                f"{set_underline(2,22)}\n")
445
446
        _create_api_content(savu_base_path, out_folder, api_type, base_path,
447
                            exclude_file, exclude_dir, f)
448