PluginCitations._set_plugin_citations()   A
last analyzed

Complexity

Conditions 4

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nop 2
dl 0
loc 10
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:: plugin_tools
17
   :platform: Unix
18
   :synopsis: Plugin tools
19
20
.. moduleauthor:: Jessica Verschoyle <[email protected]>
21
22
"""
23
import os
24
import copy
25
import json
26
import logging
27
28
from colorama import Fore
29
from collections import OrderedDict
30
31
import savu.plugins.utils as pu
32
from savu.data.meta_data import MetaData
33
import savu.plugins.docstring_parser as doc
34
import scripts.config_generator.parameter_utils as param_u
35
from savu.data.plugin_list import CitationInformation
36
37
logger = logging.getLogger("documentationLog")
38
39
40
class PluginParameters(object):
41
    """Save the parameters for the plugin and base classes to a
42
    dictionary. The parameters are in yaml format inside the
43
    define_parameter function. These are read and checked for problems.
44
    """
45
46
    def __init__(self):
47
        super(PluginParameters, self).__init__()
48
        self.param = MetaData(ordered=True)
49
        self.docstring_info = {}
50
        self.parameters = {}
51
        self.multi_params_dict = {}
52
        self.extra_dims = []        
53
54
    def populate_parameters(self, tools_list):
55
        """ Set parameter definitions and default parameter values """
56
        # set the parameter definitions
57
        # populates the dictionary returned by self.get_param_definitions()
58
        list(map(lambda tool_class: 
59
                 self._set_parameter_definitions(tool_class), tools_list))
60
        # set the default parameter values
61
        # populates the dictionary returned by self.get_param_values()
62
        self._populate_default_parameters()
63
64
    def initialise(self, params):
65
        # Override default parameter values with plugin list entries
66
        self.set_plugin_list_parameters(copy.deepcopy(params))
67
        self._get_plugin().set_parameters(self.parameters)
68
69
    def _populate_default_parameters(self):
70
        """
71
        This method should populate all the required parameters with
72
        default values. It is used for checking to see if parameter
73
        values are appropriate
74
        """
75
        p_defs = self.get_param_definitions()
76
        self.set_docstring(self.get_doc())
77
        self.parameters = \
78
            OrderedDict([(k, v['default']) for k, v in p_defs.items()])
79
        # parameters holds current values, this is edited outside of the
80
        # tools class so default and dependency display values are updated here
81
        self.update_dependent_defaults()
82
        self.check_dependencies(self.parameters)
83
        self._get_plugin().set_parameters(self.parameters)
84
85
    def set_docstring(self, doc_str):
86
        self.docstring_info['info'] = doc_str.get('verbose')
87
        self.docstring_info['warn'] = doc_str.get('warn')
88
        self.docstring_info['documentation_link'] = doc_str.get('documentation_link')
89
        self.docstring_info['synopsis'] = doc.find_synopsis(self._get_plugin())
90
91
    def _set_parameters_this_instance(self, indices):
92
        """ Determines the parameters for this instance of the plugin, in the
93
        case of parameter tuning.
94
95
        param np.ndarray indices: the index of the current value in the
96
            parameter tuning list.
97
        """
98
        dims = set(self.multi_params_dict.keys())
99
        count = 0
100
        for dim in dims:
101
            info = self.multi_params_dict[dim]
102
            name = info['label'].split('_param')[0]
103
            self.parameters[name] = info['values'][indices[count]]
104
            count += 1
105
106
    def _set_parameter_definitions(self, tool_class):
107
        """Load the parameters for each base class, c, check the
108
        dataset visibility, check data types, set dictionary values.
109
        """
110
        param_info_dict = self._load_param_from_doc(tool_class)
111
        if param_info_dict:
112
            for p_name, p_value in param_info_dict.items():
113
                if p_name in self.param.get_dictionary():
114
                    for k,v in p_value.items():
115
                        self.param[p_name][k] = v
116
                else:
117
                    self.param.set(p_name, p_value)
118
        self._check_param_defs(tool_class)
119
120
    def _check_param_defs(self, tool_class):
121
        """Check the parameter definitions for errors
122
123
        :param tool_class: tool_class to use for error message
124
        """
125
        pdefs = self.param.get_dictionary()
126
        # Remove ignored parameters
127
        self._remove_ignored_params(pdefs)
128
        # Check that unknown keys aren't present
129
        self._check_all_keys(pdefs, tool_class)
130
        # Check if the required keys are included
131
        self._check_required_keys(pdefs, tool_class)
132
        # Check that option values are valid
133
        self._check_options(pdefs, tool_class)
134
        # Check that the visibility is valid
135
        self._check_visibility(pdefs, tool_class)
136
        # Check that the dtype is valid
137
        self._check_dtype(pdefs, tool_class)
138
        # Use a display option to apply to dependent parameters later.
139
        self._set_display(pdefs)
140
        for k,v in pdefs.items():
141
            # Change empty OrderedDict to dict (due to yaml loader)
142
            if isinstance(v['default'], OrderedDict):
143
                v['default'] = json.loads(json.dumps(v['default']))
144
            # Change the string to an integer, float, list, str, dict
145
            if not self.default_dependency_dict_exists(v):
146
                v['default'] = pu._dumps(v['default'])
147
148
    def _load_param_from_doc(self, tool_class):
149
        """Find the parameter information from the method docstring.
150
        This is provided in a yaml format.
151
        """
152
        # *** TO DO turn the dtype entry into a string
153
154
        param_info_dict = None
155
        if hasattr(tool_class, "define_parameters"):
156
            yaml_text = tool_class.define_parameters.__doc__
157
            # If yaml_text is not None and not empty or consisting of spaces
158
            if yaml_text and yaml_text.strip():
159
                # When loading yaml parameter information, convert dtype value to a string
160
                docsting = doc.change_dtype_to_str(yaml_text)
161
                param_info_dict = doc.load_yaml_doc(docsting)
162
                if param_info_dict:
163
                    if not isinstance(param_info_dict, OrderedDict):
164
                        error_msg = (
165
                            f"The parameters have not been read "
166
                            f"in correctly for {tool_class.__name__}"
167
                        )
168
                        raise Exception(error_msg)
169
170
        return param_info_dict
171
172
    def get_default_value(self, mod_param):
173
        """If the '--default' argument is passed, then get the original
174
        default value. If the default contains a dictionary, then search
175
        for the correct value
176
        """
177
        param_info_dict = self.param.get_dictionary()
178
        if self.default_dependency_dict_exists(param_info_dict[mod_param]):
179
            mod_value = self.get_dependent_default(param_info_dict[mod_param])
180
        else:
181
            mod_value = param_info_dict[mod_param]["default"]
182
        return mod_value
183
184
    def _check_required_keys(self, param_info_dict, tool_class):
185
        """Check the four keys ['dtype', 'description', 'visibility',
186
        'default'] are included inside the dictionary given for each
187
        parameter.
188
        """
189
        required_keys = ["dtype", "description", "visibility", "default"]
190
        missing_keys = False
191
        missing_key_dict = {}
192
193
        for p_key, p in param_info_dict.items():
194
            all_keys = p.keys()
195
            if p.get("visibility"):
196
                if p.get("visibility") == "hidden":
197
                    # For hidden keys, only require a default value key
198
                    required_keys = ["default"]
199
            else:
200
                required_keys = ["visibility"]
201
202
            if not all(d in all_keys for d in required_keys):
203
                missing_key_dict[p_key] = set(required_keys) - set(all_keys)
204
                missing_keys = True
205
206
        if missing_keys:
207
            self._print_incorrect_key_str(missing_key_dict, "missing required", tool_class)
208
209
    def _check_all_keys(self, param_info_dict, tool_class):
210
        allowed_keys = ["dtype", "description", "visibility", "default",
211
                        "options", "display", "example", "dependency",
212
                        "warning"]
213
        unknown_keys = False
214
        unknown_keys_dict = {}
215
216
        for p_key, p in param_info_dict.items():
217
            all_keys = p.keys()
218
            if any(d not in allowed_keys for d in all_keys):
219
                unknown_keys_dict[p_key] = set(all_keys) - set(allowed_keys)
220
                unknown_keys = True
221
222
        if unknown_keys:
223
            self._print_incorrect_key_str(unknown_keys_dict, "unknown",
224
                                        tool_class)
225
226
    def _print_incorrect_key_str(self, incorrect_key_dict, key_str, tool_class):
227
        """Create and print an error message and log message when certain keys are
228
        invalid or missing
229
230
        :param missing_key_dict: Dictionary of parameter option keys which are invalid/missing
231
        :param key_str: Error message string for this
232
        :param tool_class:
233
        :return:
234
        """
235
        print(
236
            f"{tool_class.__name__} has {key_str} keys."
237
        )
238
        for param, incorrect_values in incorrect_key_dict.items():
239
            print(f"The {key_str} keys for '{param}' are:")
240
            print(*incorrect_values, sep=", ")
241
        logger.error(f"ERROR: {key_str} keys inside {tool_class.__name__}")
242
        raise Exception(f"Please edit {tool_class.__name__}")
243
244
    def _check_dtype(self, param_info_dict, tool_class):
245
        """
246
        Make sure that the dtype input is valid and that the default value is
247
        compatible
248
        """
249
        plugin_error_str = f"There was an error with {tool_class.__name__}"
250
        for p_key, p_dict in param_info_dict.items():
251
            dtype = p_dict.get("dtype")
252
            if dtype:
253
                dtype = dtype.replace(" ", "")
254
                try:
255
                    pvalid, error_str = param_u.is_valid_dtype(dtype)
256
                    if not pvalid:
257
                        raise Exception("Invalid parameter definition %s:\n %s"
258
                                        % (p_key, error_str))
259
                except IndexError:
260
                    print(plugin_error_str)
261
                if not self.default_dependency_dict_exists(p_dict):
262
                    default_value = pu._dumps(p_dict["default"])
263
                    pvalid, error_str = param_u.is_valid(p_key, default_value,
264
                                                 p_dict, check=True)
265
                    if not pvalid:
266
                        raise Exception(f"{plugin_error_str}: {error_str}")
267
268
    def _check_visibility(self, param_info_dict, tool_class):
269
        """Make sure that the visibility choice is valid"""
270
        visibility_levels = [
271
            "basic",
272
            "intermediate",
273
            "advanced",
274
            "datasets",
275
            "hidden",
276
        ]
277
        visibility_valid = True
278
        for p_key, p in param_info_dict.items():
279
            # Check dataset visibility level is correct
280
            self._check_data_keys(p_key, p)
281
            # Check that the data types are valid choices
282
            if p["visibility"] not in visibility_levels:
283
                print(
284
                    f"Inside {tool_class.__name__} the {p_key}"
285
                    f" parameter is assigned an invalid visibility "
286
                    f"level '{p['visibility']}'"
287
                )
288
                print("Valid choices are:")
289
                print(*visibility_levels, sep=", ")
290
                visibility_valid = False
291
292
        if not visibility_valid:
293
            raise Exception(
294
                f"Please change the file for {tool_class.__name__}"
295
            )
296
297
    def _check_data_keys(self, p_key, p):
298
        """Make sure that the visibility of dataset parameters is 'datasets'
299
        so that the display order is unchanged.
300
        """
301
        datasets = ["in_datasets", "out_datasets"]
302
        exceptions = ["hidden"]
303
        if p_key in datasets:
304
            if p["visibility"] != "datasets" \
305
                    and p["visibility"] not in exceptions:
306
                p["visibility"] = "datasets"
307
308
    def _check_options(self, param_info_dict, tool_class):
309
        """Make sure that option verbose descriptions match the actual
310
        options
311
        """
312
        options_valid = True
313
        for p_key, p in param_info_dict.items():
314
            desc = param_info_dict[p_key].get("description")
315
            # desc not present for hidden keys
316
            if desc and isinstance(desc, dict):
317
                options = param_info_dict[p_key].get("options")
318
                option_desc = desc.get("options")
319
                if options and option_desc:
320
                    # Check that there is not an invalid option description
321
                    # inside the option list.
322
                    invalid_option = [
323
                        opt for opt in option_desc if opt not in options
324
                    ]
325
                    if invalid_option:
326
                        options_valid = False
327
                        break
328
329
        if options_valid is False:
330
            raise Exception(
331
                f"Please check the parameter options for {tool_class.__name__}"
332
            )
333
334
    def _remove_ignored_params(self, param_info_dict):
335
        """Remove any parameters with visibility = ignore"""
336
        p_dict_copy = param_info_dict.copy()
337
        for p_key, p in p_dict_copy.items():
338
            visibility = param_info_dict[p_key].get("visibility")
339
            if visibility == "ignore":
340
                del param_info_dict[p_key]
341
342
    def _set_display(self, param_info_dict):
343
        """Initially, set all of the parameters to display 'on'
344
        This is later altered when dependent parameters need to be shown
345
        or hidden
346
        """
347
        for k, v in param_info_dict.items():
348
            v["display"] = "on"
349
350
    def update_dependent_defaults(self):
351
        """
352
        Fix default values for parameters that have a dependency on the value 
353
        of another parameter, and are in dictionary form.
354
        """
355
        for name, pdict in self.get_param_definitions().items():
356
            if self.default_dependency_dict_exists(pdict):
357
                self.parameters[name] = self.get_dependent_default(pdict)
358
359
    def default_dependency_dict_exists(self, pdict):
360
        """ Check that the parameter default value is in a format with
361
        the parent parameter string and the dependent value
362
        e.g. default:
363
                algorithm: FGP
364
        and not an actual default value to be set
365
        e.g. default: {'2':5}
366
367
        :param pdict: The parameter definition dictionary
368
        :return: True if the default dictionary contains the
369
                correct format
370
        """
371
        if pdict["default"] and isinstance(pdict["default"], dict):
372
            if "dict" not in pdict["dtype"]:
373
                return True
374
            else:
375
                parent_name = list(pdict['default'].keys())[0]
376
                if parent_name in self.get_param_definitions():
377
                    return True
378
        return False
379
380
    def does_exist(self, key, ddict):
381
        if not key in ddict:
382
            raise Exception("The dependency %s does not exist" % key)
383
        return ddict[key]
384
385
    def get_dependent_default(self, child):
386
        """
387
        Recursive function to replace a dictionary of default parameters with
388
        a single value.
389
390
        Parameters
391
        ----------
392
        child : dict
393
            The parameter definition dictionary of the dependent parameter.
394
395
        Returns1
396
        -------
397
        value
398
            The correct default value based on the current value of the
399
            dependency, or parent, parameter.
400
401
        """
402
        pdefs = self.get_param_definitions()
403
        parent_name = list(child['default'].keys())[0]
404
        parent = self.does_exist(parent_name, pdefs)
405
406
        # if the parent default is a dictionary then apply the function
407
        # recursively
408
        if isinstance(parent['default'], dict):
409
            self.parameters[parent_name] = \
410
                self.get_dependent_default(parent['default'])
411
        return child['default'][parent_name][self.parameters[parent_name]]
412
413
    def warn_dependents(self, mod_param, mod_value): 
414
        """
415
        Find dependents of a modified parameter # complete the docstring
416
        """
417
        # find dependents
418
        for name, pdict in self.get_param_definitions().items():
419
            if self.default_dependency_dict_exists(pdict):
420
                default = pdict['default']
421
                parent_name = list(default.keys())[0]
422
                if parent_name == mod_param:
423
                    if mod_value in default[parent_name].keys():
424
                        value = default[parent_name][mod_value]
425
                        desc = pdict['description']
426
427
428
    def make_recommendation(self, child_name, desc, parent_name, value): 
429
        if isinstance(desc, dict):
430
            desc["range"] = f"The recommended value with the chosen " \
431
                            f"{parent_name} would be {value}"
432
        recommendation = f"It's recommended that you update {child_name}"\
433
                         f" to {value}"
434
        print(recommendation)
435
436
        
437
    def check_dependencies(self, parameters):
438
        """Determine which parameter values are dependent on a parent
439
        value and whether they should be hidden or shown
440
        """
441
        param_info_dict = self.param.get_dictionary()
442
        dep_list = {
443
            k: v["dependency"]
444
            for k, v in param_info_dict.items()
445
            if "dependency" in v
446
        }
447
        for p_name, dependency in dep_list.items():
448
            if isinstance(dependency, OrderedDict):
449
                # There is a dictionary of dependency values
450
                parent_param_name = list(dependency.keys())[0]
451
                # The choices which must be in the parent value
452
                parent_choice_list = dependency[parent_param_name]
453
454
                if parent_param_name in parameters:
455
                    """Check that the parameter is in the current plug in
456
                    This is relevant for base classes which have several
457
                    dependent classes
458
                    """
459
                    parent_value = parameters[parent_param_name]
460
461
                    if str(parent_value) in parent_choice_list:
462
                        param_info_dict[p_name]["display"] = "on"
463
                    else:
464
                        param_info_dict[p_name]["display"] = "off"
465
            else:
466
                if dependency in parameters:
467
                    parent_value = parameters[dependency]
468
                    if parent_value is None or str(parent_value) == "None":
469
                        param_info_dict[p_name]["display"] = "off"
470
                    else:
471
                        param_info_dict[p_name]["display"] = "on"
472
473
474
    def set_plugin_list_parameters(self, input_parameters):
475
        """
476
        This method is called after the plugin has been created by the
477
        pipeline framework.  It replaces ``self.parameters``
478
        default values with those given in the input process list. It
479
        checks for multi parameter strings, eg. 57;68;56;
480
481
        :param dict input_parameters: A dictionary of the input parameters
482
        for this plugin, or None if no customisation is required.
483
        """        
484
        for key in input_parameters.keys():
485
            if key in self.parameters.keys():
486
                new_value = input_parameters[key]
487
                self.__check_multi_params(
488
                    self.parameters, new_value, key
489
                )
490
            else:
491
                error = (
492
                    f"Parameter '{key}' is not valid for plugin "
493
                    f"{self.plugin_class.name}. \nTry opening and re-saving "
494
                    f"the process list in the configurator to auto remove "
495
                    f"\nobsolete parameters."
496
                )
497
                raise ValueError(error)
498
499
    def __check_multi_params(self, parameters, value, key):
500
        """
501
        Convert parameter value to a list if it uses parameter tuning
502
        and set associated parameters, so the framework knows the new size
503
        of the data and which plugins to re-run.
504
505
        :param parameters: Dictionary of parameters and current values
506
        :param value: Value to set parameter to
507
        :param key: Parameter name
508
        :return:
509
        """
510
        if param_u.is_multi_param(key, value):
511
            value, error_str = pu.convert_multi_params(key, value)
512
            if not error_str:
513
                parameters[key] = value
514
                label = key + "_params." + type(value[0]).__name__
515
                self.alter_multi_params_dict(
516
                    len(self.get_multi_params_dict()),
517
                    {"label": label, "values": value},
518
                )
519
                self.append_extra_dims(len(value))
520
        else:
521
            parameters[key] = value
522
523
    def get_expand_dict(self, preview, expand_dim):
524
        """Create dict for expand syntax
525
526
        :param preview: Preview parameter value
527
        :param expand_dim: Number of dimensions to return dict for
528
        :return: dict
529
        """
530
        expand_dict = {}
531
        preview_val = pu._dumps(preview)
532
        if not preview_val:
533
            # In the case that there is an empty dict, display the default
534
            preview_val = []
535
        if isinstance( preview_val, dict):
536
            for key, prev_list in preview_val.items():
537
                expand_dict[key] = self.get_expand_dict(prev_list, expand_dim)
538
            return expand_dict
539
        elif isinstance(preview_val, list):
540
            if expand_dim == "all":
541
                expand_dict = \
542
                    self._output_all_dimensions(preview_val,
543
                         self._get_dimensions(preview_val))
544
            else:
545
                pu.check_valid_dimension(expand_dim, preview_val)
546
                dim_key = f"dim{expand_dim}"
547
                expand_dict[dim_key] = \
548
                    self._dim_slice_output(preview_val, expand_dim)
549
            return expand_dict
550
        else:
551
            raise ValueError("This preview value was not a recognised list "
552
                             "or dictionary. This expand command currenty "
553
                             "only works with those two data types.")
554
555
    def _get_dimensions(self, preview_list):
556
        """
557
        :param preview_list: The preview parameter list
558
        :return: Dimensions to display
559
        """
560
        return 1 if not preview_list else len(preview_list)
561
562
    def _output_all_dimensions(self, preview_list, dims):
563
        """Compile output string lines for all dimensions
564
565
        :param preview_list: The preview parameter list
566
        :param dims: Number of dimensions to display
567
        :return: dict
568
        """
569
        prev_dict = {}
570
        for dim in range(1, dims + 1):
571
            dim_key = f"dim{dim}"
572
            prev_dict[dim_key] = self._dim_slice_output(preview_list, dim)
573
        return prev_dict
574
575
    def _dim_slice_output(self, preview_list, dim):
576
        """If there are multiple values in list format
577
        Only save the values for the dimensions chosen
578
579
        :param preview_list: The preview parameter list
580
        :param dim: dimension to return the slice notation dictionary for
581
        :return slice notation dictionary
582
        """
583
        if not preview_list:
584
            # If empty
585
            preview_display_value = ":"
586
        else:
587
            preview_display_value = preview_list[dim - 1]
588
        prev_val = self._set_all_syntax(preview_display_value)
589
        return self._get_slice_notation_dict(prev_val)
590
591
    def _get_slice_notation_dict(self, val):
592
        """Create a dict for slice notation information,
593
        start:stop:step (and chunk if provided)
594
595
        :param val: The list value in slice notation
596
        :return: dictionary of slice notation
597
        """
598
        import itertools
599
600
        basic_slice_keys = ["start", "stop", "step"]
601
        all_slice_keys = [*basic_slice_keys, "chunk"]
602
        slice_dict = {}
603
604
        if pu.is_slice_notation(val):
605
            val_list = val.split(":")
606
            if len(val_list) < 3:
607
                # Make sure the start stop step slice keys are always shown,
608
                # even when blank
609
                val_list.append("")
610
            for slice_name, v in zip(all_slice_keys, val_list):
611
                # Only print up to the shortest list.
612
                # (Only show the chunk value if it is in val_list)
613
                slice_dict[slice_name] = v
614
        else:
615
            val_list = [val]
616
            for slice_name, v in itertools.zip_longest(
617
                    basic_slice_keys, val_list, fillvalue=""
618
            ):
619
                slice_dict[slice_name] = v
620
        return slice_dict
621
622
    def _set_all_syntax(self, val, replacement_str=""):
623
        """Remove additional spaces from val, replace colon when 'all'
624
        data is selected
625
626
        :param val: Slice notation value
627
        :param replacement_str: String to replace ':' with
628
        :return:
629
        """
630
        if isinstance(val, str):
631
            if pu.is_slice_notation(val):
632
                if val == ":":
633
                    val = replacement_str
634
                else:
635
                    val = val.strip()
636
            else:
637
                val = val.strip()
638
        return val
639
640
    def get_multi_params_dict(self):
641
        """ Get the multi parameter dictionary. """
642
        return self.multi_params_dict
643
644
    def alter_multi_params_dict(self, key, value):
645
        self.multi_params_dict[key] = value
646
647
    def get_extra_dims(self):
648
        """ Get the extra dimensions. """
649
        return self.extra_dims
650
651
    def set_extra_dims(self, value):
652
        self.extra_dims = value
653
654
    def append_extra_dims(self, value):
655
        self.extra_dims.append(value)
656
657
    def define_parameters(self):
658
        pass
659
660
    """
661
    @dataclass
662
    class Parameter:
663
        ''' Descriptor of Parameter Information for plugins
664
        '''
665
        visibility: int
666
        datatype: specific_type
667
        description: str
668
        default: int
669
        Options: Optional[[str]]
670
        dependency: Optional[]
671
672
        def _get_param(self):
673
            param_dict = {}
674
            param_dict['visibility'] = self.visibility
675
            param_dict['type'] = self.dtype
676
            param_dict['description'] = self.description
677
            # and the remaining keys
678
            return param_dict
679
    """
680
681
682
class PluginCitations(object):
683
    """Get this citation dictionary so get_dictionary of the metadata type
684
    should return a dictionary of all the citation info as taken from
685
    docstring
686
    """
687
688
    def __init__(self):
689
        super(PluginCitations, self).__init__()
690
        self.cite = MetaData(ordered=True)
691
692
    def set_cite(self, tools_list):
693
        """Set the citations for each of the tools classes
694
        :param tools_list: List containing tool classes of parent plugins
695
        """
696
        list(
697
            map(
698
                lambda tool_class: self._set_plugin_citations(tool_class),
699
                tools_list
700
            )
701
        )
702
703
    def _set_plugin_citations(self, tool_class):
704
        """ Load the parameters for each base class and set values"""
705
        citations = self._load_cite_from_doc(tool_class)
706
        if citations:
707
            for citation in citations.values():
708
                if self._citation_keys_valid(citation, tool_class):
709
                    new_citation = CitationInformation(**citation)
710
                    self.cite.set(new_citation.name, new_citation)
711
                else:
712
                    print(f"The citation for {tool_class.__name__} "
713
                          f"was not saved.")
714
715
    def _citation_keys_valid(self, new_citation, tool_class):
716
        """Check that required citation keys are present. Return false if
717
        required keys are missing
718
        """
719
        required_keys = ["description"]
720
        # Inside the fresnel filter there is only a description
721
        citation_keys = [k for k in new_citation.keys()]
722
        # Check that all of the required keys are contained inside the
723
        # citation definition
724
        check_keys = all(item in citation_keys for item in required_keys)
725
        citation_keys_valid = False if check_keys is False else True
726
727
        all_keys = [
728
            "short_name_article",
729
            "description",
730
            "bibtex",
731
            "endnote",
732
            "doi",
733
            "dependency",
734
        ]
735
        # Keys which are not used
736
        additional_keys = [k for k in citation_keys if k not in all_keys]
737
        if additional_keys:
738
            print(f"Please only use the following keys inside the citation"
739
                  f" definition for {tool_class.__name__}:")
740
            print(*all_keys, sep=", ")
741
            print("The incorrect keys used:", additional_keys)
742
743
        return citation_keys_valid
744
745
    def _load_cite_from_doc(self, tool_class):
746
        """Find the citation information from the method docstring.
747
        This is provided in a yaml format.
748
749
        :param tool_class: Tool to retrieve citation docstring from
750
        :return: All citations from this tool class
751
        """
752
        all_c = OrderedDict()
753
        # Seperate the citation methods. __dict__ returns instance attributes.
754
        citation_methods = {key: value
755
                            for key, value in tool_class.__dict__.items()
756
                            if key.startswith('citation')}
757
        for c_method_name, c_method in citation_methods.items():
758
            yaml_text = c_method.__doc__
759
            if yaml_text is not None:
760
                yaml_text = self.seperate_description(yaml_text)
761
                current_citation = doc.load_yaml_doc(yaml_text)
762
                if not isinstance(current_citation, OrderedDict):
763
                    print(f"The citation information has not been read in "
764
                          f"correctly for {tool_class.__name__}.")
765
                else:
766
                    all_c[c_method_name] = current_citation
767
        return all_c
768
769
    def seperate_description(self, yaml_text):
770
        """Change the format of the docstring to retain new lines for the
771
        endnote and bibtex and create a key for the description so that
772
        it be read as a yaml file
773
774
        :param yaml_text:
775
        :return: Reformatted yaml text
776
        """
777
        description = doc.remove_new_lines(yaml_text.partition("bibtex:")[0])
778
        desc_str = "        description:" + description
779
780
        bibtex_text = \
781
            yaml_text.partition("bibtex:")[2].partition("endnote:")[0]
782
        end_text = \
783
            yaml_text.partition("bibtex:")[2].partition("endnote:")[2]
784
785
        if bibtex_text and end_text:
786
            final_str = desc_str + '\n        bibtex: |' + bibtex_text \
787
                      + 'endnote: |' + end_text
788
        elif end_text:
789
            final_str = desc_str + '\n        endnote: |' + end_text
790
        elif bibtex_text:
791
            final_str = desc_str + '\n        bibtex: |' + bibtex_text
792
        else:
793
            final_str = desc_str
794
795
        return final_str
796
797
798
class PluginDocumentation(object):
799
    """Get this documentation dictionary so get_dictionary of
800
    the metadata type should return a dictionary of all the
801
    documentation details taken from docstring
802
    """
803
804
    def __init__(self):
805
        super(PluginDocumentation, self).__init__()
806
        self.doc = MetaData()
807
808
    def set_doc(self, tools_list):
809
        # Use the tools class at the 'top'
810
        doc_lines = tools_list[-1].__doc__.splitlines()
811
        doc_lines = [line.strip() for line in doc_lines if line]
812
        docstring = " ".join(doc_lines)
813
        self.doc.set("verbose", docstring)
814
        self.doc.set("warn", self.set_warn(tools_list))
815
        self.set_doc_link()
816
817
    def set_warn(self, tools_list):
818
        """Remove new lines and save config warnings for the child tools
819
        class only.
820
        """
821
        config_str = tools_list[-1].config_warn.__doc__
822
        if config_str and "\n\n" in config_str:
823
            # Separate multiple warnings with two new lines \n\n
824
            config_warn_list = [doc.remove_new_lines(l)
825
                                for l in config_str.split("\n\n")]
826
            config_str = '\n'.join(config_warn_list)
827
        return config_str
828
829
    def set_doc_link(self):
830
        """If there is a restructured text documentation file inside the
831
        doc/source/plugin_guides/plugins folder, then save the link to the page.
832
833
        """
834
        # determine Savu base path
835
        savu_base_path = os.path.join(
836
            os.path.dirname(os.path.realpath(__file__)), '../../')
837
838
        # Locate documentation file
839
        doc_folder = savu_base_path + "doc/source/plugin_guides"
840
        module_path = \
841
            self.plugin_class.__module__.replace(".", "/").replace("savu", "")
842
        file_ = module_path + "_doc"
843
        file_name = file_ + ".rst"
844
        file_path = doc_folder + file_name
845
        sphinx_link = f"https://savu.readthedocs.io/en/latest/" \
846
                      f"plugin_guides{file_}.html"
847
        if os.path.isfile(file_path):
848
            self.doc.set("documentation_link", sphinx_link)
849
850
    def config_warn(self):
851
        pass
852
853
854
class PluginTools(PluginParameters, PluginCitations, PluginDocumentation):
855
    """Holds all of the parameter, citation and documentation information
856
    for one plugin class - cls"""
857
858
    def __init__(self, cls):
859
        super(PluginTools, self).__init__()
860
        self.plugin_class = cls
861
        self.tools_list = self._find_tools()
862
        self._set_tools_data()
863
864
    def _get_plugin(self):
865
        return self.plugin_class
866
867
    def _find_tools(self):
868
        """Using the method resolution order, find base class tools.
869
        Add tools information in reverse order, to build up on base parameters
870
        """
871
        tool_list = []
872
        for tool_class in reversed(self.plugin_class.__class__.__mro__[:-1]):
873
            plugin_tools_id = tool_class.__module__ + "_tools"
874
            p_tools = pu.get_tools_class(plugin_tools_id)
875
            if p_tools:
876
                tool_list.append(p_tools)
877
        return tool_list
878
879
    def _set_tools_data(self):
880
        """Populate the parameters, citations and documentation
881
        with information from all of the tools classes
882
        """
883
        self.populate_parameters(self.tools_list)
884
        self.set_cite(self.tools_list)
885
        self.set_doc(self.tools_list)
886
887
    def get_param_definitions(self):
888
        """
889
        Returns
890
        -------
891
        dict
892
            Original parameter definitions read from tools file.
893
        """
894
        return self.param.get_dictionary()
895
896
    def get_param_values(self):
897
        """
898
        Returns
899
        -------
900
        dict
901
            Plugin parameter values for this instance.
902
903
        """
904
        return self.parameters
905
906
    def get_citations(self):
907
        return self.cite.get_dictionary()
908
909
    def get_doc(self):
910
        return self.doc.get_dictionary()
911