Test Setup Failed
Push — master ( 614633...164ec0 )
by Nicola
03:53 queued 17s
created

create_plugin_doc.create_disp_format()   C

Complexity

Conditions 10

Size

Total Lines 35
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 25
nop 3
dl 0
loc 35
rs 5.9999
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like create_plugin_doc.create_disp_format() 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
from collections import OrderedDict
30
31
import savu.plugins.utils as pu
32
import scripts.config_generator.savu_config as sc
33
34
35
def add_package_entry(f, files_present, output, module_name):
36
    """Create a contents page for the files and directories contained
37
    in 'files'. Create links to all the plugin classes which load without
38
    errors
39
    """
40
    if files_present:
41
        # If files are present in this directory then, depending on the
42
        # number of nested directories, determine which section heading
43
        # and title to apply
44
        title = module_name.split(".")
45
46
        if len(title) > 1:
47
            f.write(set_heading(title[len(title) - 1], len(title) - 1))
48
49
        # For directory contents
50
        f.write("\n.. toctree::\n")
51
        # Contents display level is set to have plugin names only
52
        f.write("   :maxdepth: 1 \n\n")
53
54
        for fi in files_present:
55
            # TODO At the moment if a directory contains files, and none of
56
            #  their classes load correctly, the content will be blank
57
            mod_path = module_name + "." + fi.split(".py")[0]
58
            file_path = get_path_format(mod_path, output)
59
            py_module_name = "savu." + str(mod_path)
60
            try:
61
                # If the plugin class exists, put it's name into the contents
62
                plugin_class = pu.load_class(py_module_name)
63
                name = convert_title(fi.split(".py")[0])
64
                f.write(f"   {name} <{output}/{file_path}>\n")
65
            except Exception:
66
                pass
67
        f.write("\n\n")
68
69
70
def set_underline(level: int, length: int) -> str:
71
    """Create an underline string of a certain length
72
73
    :param level: The underline level specifying the symbol to use
74
    :param length: The string length
75
    :return: Underline string
76
    """
77
    underline_symbol = ["", "#", "*", "-", "^", '"', "="]
78
    symbol_str = underline_symbol[level] * length
79
    return f"\n{symbol_str}\n"
80
81
82
def set_heading(title: str, level: int) -> str:
83
    """Return the plugin heading string
84
85
    :param title: Plugin title
86
    :param level: Heading underline level
87
    :return: Heading string
88
    """
89
    plugin_type = convert_title(title)
90
    return f"{plugin_type}{set_underline(level, 56)}"
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
    plugin_guide_path = "plugin_guides/"
108
    for fi in files:
109
        mod_path = module_name + "." + fi.split(".py")[0]
110
        file_path = mod_path.replace(".", "/")
111
        py_module_name = "savu." + str(mod_path)
112
        try:
113
            plugin_class = pu.load_class(py_module_name)()
114
        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...
115
            p_name = py_module_name.split('plugins.')[1]
116
            print(f"Cannot load {p_name}: {er}")
117
            plugin_class = None
118
119
        if plugin_class:
120
            tools = plugin_class.get_plugin_tools()
121
            tools._populate_default_parameters()
122
            try:
123
                plugin_tools = plugin_class.tools.tools_list
124
                if plugin_tools:
125
                    # Create rst additional documentation directory
126
                    # and file and image directory
127
                    create_documentation_directory(
128
                        savu_base_path, plugin_guide_path, fi
129
                    )
130
                    # Create an empty rst file inside this directory where
131
                    # the plugin tools documentation will be stored
132
                    full_file_path = f"{savu_base_path}doc/source/reference/{output}/{file_path}.rst"
133
                    pu.create_dir(full_file_path)
134
                    with open(full_file_path, "w+") as new_rst_file:
135
                        # Populate this file
136
                        populate_plugin_doc_files(
137
                            new_rst_file,
138
                            plugin_tools,
139
                            file_path,
140
                            plugin_class,
141
                            savu_base_path,
142
                            plugin_guide_path,
143
                        )
144
            except:
145
                print(f"Tools file missing for {py_module_name}")
146
147
148
def convert_title(original_title):
149
    """Remove underscores from string"""
150
    new_title = original_title.replace("_", " ").title()
151
    return new_title
152
153
154
def populate_plugin_doc_files(new_rst_file, tool_class_list, file_path,
155
                     plugin_class, savu_base_path, plugin_guide_path):
156
    """Create the restructured text file containing parameter, citation
157
    and documentation information for the plugin_class
158
159
    :param new_rst_file: The new restructured text file which will hold all
160
                            of the plugin details for this plugin class
161
    :param tool_class_list: The list of base tool classes for this plugin
162
    :param file_path: Path to the plugin file
163
    :param plugin_class: Plugin class
164
    :param savu_base_path: Savu file path
165
    """
166
167
    title = file_path.split("/")
168
    # Depending on the number of nested directories, determine which section
169
    # heading and title to apply
170
    new_rst_file.write(set_heading(title[-1], 1))
171
172
    plugin_data = plugin_class.tools.get_param_definitions()
173
    plugin_citations = plugin_class.tools.get_citations()
174
    plugin_docstring = plugin_class.tools.get_doc()
175
176
    tool_class = tool_class_list[-1]
177
    docstring_info = plugin_docstring.get("verbose")
178
179
    if docstring_info:
180
        new_rst_file.write(f"\nDescription{set_underline(3,26)}")
181
        new_rst_file.write("\n")
182
        new_rst_file.write(docstring_info)
183
        new_rst_file.write("\n")
184
185
        # Locate documentation file
186
        doc_folder = savu_base_path + "doc/source/"
187
        file_str = f"{doc_folder}{plugin_guide_path}{file_path}_doc.rst"
188
        inner_file_str = f"/../../../{plugin_guide_path}{file_path}_doc.rst"
189
        if os.path.isfile(file_str):
190
            # If there is a documentation file
191
            new_rst_file.write("\n")
192
            new_rst_file.write(".. toctree::")
193
            new_rst_file.write(f"\n    Plugin documention and guidelines"
194
                               f" on use <{inner_file_str}>")
195
            new_rst_file.write("\n")
196
197
    if tool_class.define_parameters.__doc__:
198
        # Check define parameters exists
199
        new_rst_file.write(f"\nParameter definitions{set_underline(3,26)}")
200
        new_rst_file.write("\n.. code-block:: yaml")
201
        new_rst_file.write("\n")
202
203
        if plugin_data:
204
            # Go through all plugin parameters
205
            for p_name, p_dict in plugin_data.items():
206
                new_rst_file.write("\n"
207
                       + pu.indent_multi_line_str
208
                           (get_parameter_info(p_name, p_dict), 2))
209
210
        # Key to explain parameters
211
        new_rst_file.write(f"\nKey{set_underline(4,10)}")
212
        new_rst_file.write("\n")
213
        new_rst_file.write(
214
            ".. literalinclude:: "
215
            "/../source/files_and_images/"
216
            + plugin_guide_path
217
            + "short_parameter_key.yaml"
218
        )
219
        new_rst_file.write("\n    :language: yaml\n")
220
221
    if plugin_citations:
222
        # If documentation information is present, then display it
223
        new_rst_file.write(f"\nCitations{set_underline(3,26)}")
224
225
        write_citations_to_file(new_rst_file, plugin_citations)
226
227
228
def get_parameter_info(p_name, parameter):
229
    exclude_keys = ["display"]
230
    parameter_info = p_name + ":\n"
231
    try:
232
        keys_display = {k:v for k,v in parameter.items() if k not in exclude_keys}
233
        parameter_info = create_disp_format(keys_display, parameter_info)
234
    except Exception as e:
235
        print(str(e))
236
    return parameter_info
237
238
239
def create_disp_format(in_dict, disp_string, indent_level=1):
240
    """ Create specific documentation display string in yaml format
241
242
    :param dict: dictionary to display
243
    :param disp_string: input string to append to
244
    :return: final display string
245
    """
246
    for k, v in in_dict.items():
247
        list_display = isinstance(v, list) and indent_level > 1
248
        if isinstance(v, dict):
249
            indent_level += 1
250
            str_dict = create_disp_format(v, "", indent_level)
251
            indent_level -= 1
252
            str_val= f"{k}: \n{str_dict}"
253
        elif list_display:
254
            indent_level += 1
255
            list_str = ''
256
            for item in v:
257
                list_str += pu.indent(f"{item}\n", indent_level)
258
            indent_level -= 1
259
            str_val = f"{k}: \n{list_str}"
260
        elif isinstance(v, str):
261
            # Check if the string contains characters which may need
262
            # to be surrounded by quotes
263
            v = v.strip()
264
            str_val = f'{k}: {v}' if no_yaml_char(v) else f'{k}: "{v}"'
265
        elif isinstance(v, type(None)):
266
            str_val = f"{k}: None"
267
        else:
268
            str_val = f'{k}: "{v}"'
269
        if not isinstance(v, dict) and not list_display:
270
            # Don't append a new line for dictionary entries
271
            str_val += "\n"
272
        disp_string += pu.indent(str_val, indent_level)
273
    return disp_string
274
275
276
def no_yaml_char(s):
277
    """Check for characters which prevent the yaml syntax highlighter
278
    from being applied. For example [] and ? and '
279
    """
280
    return bool(re.match(r"^[a-zA-Z0-9()%|#\"/._,+\-=: {}<>]*$", s))
281
282
283
def write_citations_to_file(new_rst_file, plugin_citations):
284
    """Create the citation text format """
285
    for name, citation in plugin_citations.items():
286
        new_rst_file.write(
287
            f"\n{name.lstrip()}{set_underline(4, 182).rstrip()}"
288
        )
289
        if citation.dependency:
290
            # If the citation is dependent upon a certain parameter value
291
            # being chosen
292
            for (
293
                citation_dependent_parameter,
294
                citation_dependent_value,
295
            ) in citation.dependency.items():
296
                new_rst_file.write(
297
                    "\n(Please use this citation if "
298
                    "you are using the "
299
                    + citation_dependent_value
300
                    + " "
301
                    + citation_dependent_parameter
302
                    + ")\n"
303
                )
304
        bibtex = citation.bibtex
305
        endnote = citation.endnote
306
        # Where new lines are, append an indentation
307
        if bibtex:
308
            new_rst_file.write(f"\nBibtex{set_underline(5,42)}")
309
            new_rst_file.write("\n.. code-block:: none")
310
            new_rst_file.write("\n\n")
311
            new_rst_file.write(pu.indent_multi_line_str(bibtex, True))
312
            new_rst_file.write("\n")
313
314
        if endnote:
315
            new_rst_file.write(f"\nEndnote{set_underline(5,42)}")
316
            new_rst_file.write("\n.. code-block:: none")
317
            new_rst_file.write("\n\n")
318
            new_rst_file.write(pu.indent_multi_line_str(endnote, True))
319
            new_rst_file.write("\n")
320
321
    new_rst_file.write("\n")
322
323
324
def create_plugin_template_downloads(savu_base_path):
325
    """Inside plugin_examples/plugin_templates/general
326
    If the file begins with 'plugin_template' then select it
327
    Read the lines of the files docstring and set as a descriptor
328
    """
329
    doc_template_file = (
330
        savu_base_path + "doc/source/dev_guides/dev_plugin_templates.rst"
331
    )
332
    # Populate dictionary with template class and template class docstring
333
    docstring_text = create_template_class_dict(savu_base_path)
334
    if docstring_text:
335
        with open(doc_template_file, "w") as doc_template:
336
            doc_template.write(".. _plugin_templates:\n")
337
            doc_template.write("\n")
338
            doc_template.write(f"Plugin templates {set_underline(6,23)}")
339
            doc_template.write("\n")
340
341
            doc_name = "plugin_template1_with_detailed_notes"
342
            detailed_template = docstring_text.get(doc_name)
343
344
            if detailed_template:
345
                docstring_text.pop(doc_name)
346
                title = convert_title(doc_name)
347
                title, number = filter_template_numbers(title)
348
                # Create the restructured text page for the plugin template
349
                # python code
350
                generate_template_files(doc_name, title)
351
                inner_file_str = (
352
                    "../../../" + "plugin_examples/plugin_templates/general"
353
                )
354
                doc_template.write(f"{title}{set_underline(3,66)}")
355
                doc_template.write(
356
                    "\nA template to create a simple plugin "
357
                    "that takes one dataset as input and returns "
358
                    "a similar dataset as output"
359
                )
360
                doc_template.write("\n")
361
                doc_template.write(
362
                    """
363
.. list-table::  
364
   :widths: 10
365
   
366
   * - :ref:`"""
367
                    + doc_name
368
                    + """`
369
370
"""
371
                )
372
            doc_template.write(f"Further Examples{set_underline(3,66)}")
373
            # Begin the table layout
374
            doc_template.write(
375
                """
376
.. list-table::  
377
   :widths: 10 90
378
   :header-rows: 1
379
380
   * - Link
381
     - Description"""
382
            )
383
384
            for doc_name, doc_str in docstring_text.items():
385
                title = convert_title(doc_name)
386
                title, number = filter_template_numbers(title)
387
                desc_str = doc_str["desc"]
388
                # Create a link to the restructured text page view of the python
389
                # code for the template
390
                doc_template.write("\n   * - :ref:`" + doc_name + "`")
391
                # The template description from the docstring
392
                doc_template.write("\n     - " + desc_str)
393
                doc_template.write("\n")
394
                # Create the restructured text page for the plugin template
395
                # python code
396
                generate_template_files(doc_name, title)
397
398
            doc_template.write("\n")
399
400
401
def generate_template_files(doc_name, title):
402
    """Create a restructured text file which will include the python
403
     code for the plugin template 'doc_name'
404
405
    :param doc_name: The name of the template file
406
    :param title:
407
    :return:
408
    """
409
    inner_file_str = (
410
        "../../../../" + "plugin_examples/plugin_templates/general"
411
    )
412
    template_file_path = (
413
        savu_base_path
0 ignored issues
show
introduced by
The variable savu_base_path does not seem to be defined in case __name__ == "__main__" on line 576 is False. Are you sure this can never be the case?
Loading history...
414
        + "doc/source/dev_guides/templates/"
415
        + doc_name
416
        + ".rst"
417
    )
418
    with open(template_file_path, "w") as template_file:
419
        # Add the orphan instruction as this file is not inside a toctree
420
        template_file.write(":orphan:\n")
421
        template_file.write("\n.. _" + doc_name + ":\n")
422
        template_file.write("\n")
423
        template_file.write(f"{title}{set_underline(4, 39)}")
424
        template_file.write("\n")
425
        template_file.write(
426
            f":download:`Download {title}<{inner_file_str}"
427
            f"/{doc_name}.py>`\n\n"
428
        )
429
        template_file.write("\n")
430
        template_file.write(
431
            f":download:`Download {title} Tools<{inner_file_str}"
432
            f"/{doc_name}_tools.py>`\n\n"
433
        )
434
        template_file.write("\n")
435
        template_file.write(
436
            ".. literalinclude:: "
437
            "/../../plugin_examples/plugin_templates/general/"
438
            + doc_name
439
            + ".py"
440
        )
441
        template_file.write("\n    :language: python\n")
442
        template_file.write("\n")
443
        template_file.write(
444
            ".. literalinclude:: "
445
            "/../../plugin_examples/plugin_templates/general/"
446
            + doc_name
447
            + "_tools.py"
448
        )
449
        template_file.write("\n    :language: python\n")
450
451
452
def filter_template_numbers(name_string):
453
    """
454
    :param name_string: The name of the template
455
    :return: A string with the template number seperated by a space
456
    """
457
    number = "".join(l for l in name_string if l.isdigit())
458
    letters = "".join(l for l in name_string if l.isalpha())
459
    split_uppercase = [l for l in re.split("([A-Z][^A-Z]*)", letters) if l]
460
    title = " ".join(split_uppercase)
461
    name = title + " " + number
462
    return name, number
463
464
465
def create_template_class_dict(savu_base_path):
466
    """Iterate through the plugin example folder and store the class
467
    and it's class docstring into a dictionary docstring_text
468
469
    :param savu_base_path:
470
    :return: docstring_text dictionary of class and docstring
471
    """
472
    docstring_text = {}
473
    plugin_ex_path = (
474
        savu_base_path + "plugin_examples/plugin_templates/general"
475
    )
476
477
    for t_root, t_dirs, template_files \
478
            in os.walk(plugin_ex_path, topdown=True):
479
        template_files[:] = [fi for fi in template_files
480
                             if fi.split(".")[-1] == "py"
481
                             and "tools" not in fi]
482
        if "__" not in t_root:
483
            pkg_path = t_root.split("Savu/")[1]
484
            module_name = pkg_path.replace("/", ".")
485
486
        for fi in template_files:
487
            file_name = fi.split(".py")[0]
488
            cls_module = module_name + "." + file_name
0 ignored issues
show
introduced by
The variable module_name does not seem to be defined for all execution paths.
Loading history...
489
            try:
490
                cls_loaded = pu.load_class(cls_module)()
491
            except AttributeError as e:
492
                cls_loaded = None
493
494
            if cls_loaded:
495
                tools = cls_loaded.get_plugin_tools()
496
                doc = tools.get_doc() if tools else ""
497
                desc = doc.get("verbose") if isinstance(doc,dict) else ""
498
                title = convert_title(file_name)
499
                name, number = filter_template_numbers(title)
500
                docstring_text[file_name] = {
501
                    "desc": desc,
502
                    "number": int(number),
503
                }
504
505
    # Order templates by number
506
    docstring_text = OrderedDict(
507
        sorted(docstring_text.items(), key=lambda i: i[1]["number"])
508
    )
509
510
    return docstring_text
511
512
513
def create_savu_config_documentation(savu_base_path):
514
    """Look at the available commands inside savu_config
515
    Create a rst text file for each.
516
    """
517
    command_file_path = (savu_base_path
518
                         + "doc/source/reference/savu_config_commands.rst")
519
    with open(command_file_path, "w") as command_file:
520
        savu_command_test_start = """
521
Savu Config Commands
522
**********************
523
524
The links on this page provide help for each command.
525
If you are using the command line please type ``-h`` or ``--help``.
526
527
.. code-block:: bash
528
529
   savu_config --help
530
531
"""
532
        # Write contents
533
        command_file.write(savu_command_test_start)
534
        for command in sc.commands:
535
            command_file.write("\n")
536
            command_file.write("* :ref:`" + command + "`")
537
            command_file.write("\n")
538
539
        # Document commands
540
        for command in sc.commands:
541
            command_file.write("\n")
542
            command_file.write(".. _" + command + ":")
543
            command_file.write("\n\n" + command)
544
            command_file.write(set_underline(3,16))
545
            command_file.write("\n.. cssclass:: argstyle\n")
546
            command_file.write("\n    .. argparse::")
547
            command_file.write("\n            :module: scripts.config_generator.arg_parsers")
548
            command_file.write("\n            :func: _" + command + "_arg_parser")
549
            command_file.write("\n            :prog: " + command)
550
            command_file.write("\n")
551
            command_file.write("\n")
552
553
554
def create_documentation_directory(savu_base_path,
555
                                   plugin_guide_path,
556
                                   plugin_file):
557
    """ Create plugin directory inside documentation and
558
    documentation file and image folders
559
    """
560
    # Create directory inside
561
    doc_path = savu_base_path + "doc/source/"
562
    doc_image_path = (
563
        savu_base_path
564
        + "doc/source/files_and_images/"
565
        + plugin_guide_path
566
        + "plugins/"
567
    )
568
569
    # find the directories to create
570
    doc_dir = doc_path + plugin_guide_path + plugin_file
571
    image_dir = doc_image_path + plugin_file
572
    pu.create_dir(doc_dir)
573
    pu.create_dir(image_dir)
574
575
576
if __name__ == "__main__":
577
    out_folder, rst_file, api_type = sys.argv[1:]
578
579
    # determine Savu base path
580
    main_dir = \
581
        os.path.dirname(os.path.realpath(__file__)).split("/Savu/")[0]
582
    savu_base_path = f"{main_dir}/Savu/"
583
584
    base_path = savu_base_path + "savu/plugins"
585
    # create entries in the autosummary for each package
586
587
    exclude_file = [
588
        "__init__.py",
589
        "docstring_parser.py",
590
        "plugin.py",
591
        "plugin_datasets.py",
592
        "plugin_datasets_notes.py",
593
        "utils.py",
594
        "plugin_tools.py",
595
    ]
596
    exclude_dir = ["driver",
597
                   "utils",
598
                   "unregistered",
599
                   "under_revision",
600
                   "templates",
601
                   ]
602
603
    # Create template download page
604
    create_plugin_template_downloads(savu_base_path)
605
606
    # Create savu_config command rst files
607
    # create_config_documentation(savu_base_path)
608
    create_savu_config_documentation(savu_base_path)
609
610
    # Only document the plugin python files
611
    # Create the directory if it does not exist
612
    pu.create_dir(f"{savu_base_path}doc/source/reference/{out_folder}")
613
614
    # open the autosummary file
615
    with open(f"{savu_base_path}doc/source/reference/{rst_file}", "w") as f:
616
617
        document_title = convert_title(out_folder)
618
        f.write(".. _" + out_folder + ":\n")
619
        f.write(f"{set_underline(2,22)}{document_title} "
620
                f"{set_underline(2,22)}\n")
621
622
        for root, dirs, files in os.walk(base_path, topdown=True):
623
            tools_files = [fi for fi in files if "tools" in fi]
624
            template_files = [fi for fi in files if "template" in fi]
625
            base_files = [fi for fi in files if fi.startswith("base")]
626
            driver_files = [fi for fi in files if "driver" in fi]
627
            exclude_files = [
628
                exclude_file,
629
                tools_files,
630
                base_files,
631
                driver_files,
632
                template_files
633
            ]
634
            dirs[:] = [d for d in dirs if d not in exclude_dir]
635
            files[:] = [fi for fi in files if fi not in chain(*exclude_files)]
636
            # Exclude the tools files fron html view sidebar
637
            if "__" not in root:
638
                pkg_path = root.split("Savu/")[1]
639
                module_name = pkg_path.replace("/", ".")
640
                module_name = module_name.replace("savu.", "")
641
                if "plugins" in module_name:
642
                    add_package_entry(f, files, out_folder, module_name)
643
                    if out_folder == "plugin_documentation":
644
                        create_plugin_documentation(
645
                            files, out_folder, module_name, savu_base_path
646
                        )
647