Test Failed
Pull Request — master (#777)
by
unknown
03:28
created

PluginParameters._get_expand_dict()   B

Complexity

Conditions 6

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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