Test Failed
Push — master ( e26e75...729ab1 )
by Nicola
01:26 queued 17s
created

create_plugin_doc   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 400
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 233
dl 0
loc 400
rs 8.8798
c 0
b 0
f 0
wmc 44

12 Functions

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

How to fix   Complexity   

Complexity

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