Test Failed
Pull Request — master (#832)
by
unknown
03:34
created

scripts.config_generator.content.Content.get()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 2
dl 0
loc 2
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:: content
17
   :platform: Unix
18
   :synopsis: Content class for the configurator
19
20
.. moduleauthor:: Nicola Wadeson <[email protected]>
21
22
"""
23
import re
24
import os
25
import inspect
26
27
from savu.plugins import utils as pu
28
from savu.data.plugin_list import PluginList
29
import scripts.config_generator.parameter_utils as param_u
30
31
from . import mutations
32
33
34
class Content(object):
35
    def __init__(self, filename=None, level="basic"):
36
        self.disp_level = level
37
        self.plugin_list = PluginList()
38
        self.plugin_mutations = mutations.plugin_mutations
39
        self.param_mutations = mutations.param_mutations
40
        self.filename = filename
41
        self._finished = False
42
        self.failed = {}
43
        self.expand_dim = None
44
45
    def set_finished(self, check="y"):
46
        self._finished = True if check.lower() == "y" else False
47
48
    def is_finished(self):
49
        return self._finished
50
51
    def fopen(self, infile, update=False, skip=False):
52
        if os.path.exists(infile):
53
            self.plugin_list._populate_plugin_list(infile, active_pass=True)
54
        else:
55
            raise Exception("INPUT ERROR: The file does not exist.")
56
        self.filename = infile
57
        if update:
58
            self.plugin_mutations = self.check_mutations(
59
                self.plugin_mutations
60
            )
61
            self.param_mutations = self.check_mutations(self.param_mutations)
62
            self._apply_plugin_updates(skip)
63
64
    def check_mutations(self, mut_dict: dict):
65
        plist_version = self._version_to_float(self.plugin_list.version)
66
        # deleting elements while iterating invalidates the iterator
67
        # which raises a RuntimeError in Python 3.
68
        # Instead a copy of the dict is mutated and returned
69
        mut_dict_copy = mut_dict.copy()
70
        for key, subdict in mut_dict.items():
71
            if "up_to_version" in subdict.keys():
72
                up_to_version = self._version_to_float(
73
                    subdict["up_to_version"]
74
                )
75
                if plist_version >= up_to_version:
76
                    del mut_dict_copy[key]
77
        return mut_dict_copy
78
79
    def _version_to_float(self, version):
80
        if version is None:
81
            return 0
82
        if isinstance(version, bytes):
83
            version = version.decode("ascii")
84
        split_vals = version.split(".")
85
        return float(".".join([split_vals[0], "".join(split_vals[1:])]))
86
87
    def display(self, formatter, **kwargs):
88
        # Set current level
89
        if "current_level" not in list(kwargs.keys()):
90
            kwargs["current_level"] = self.disp_level
91
        if (
92
            "disp_level" in list(kwargs.keys())
93
            and kwargs["disp_level"] is True
94
        ):
95
            # Display level
96
            print(f"Level is set at '{kwargs['current_level']}'")
97
        else:
98
            # Display parameter
99
            kwargs["expand_dim"] = self.expand_dim
100
            print("\n" + formatter._get_string(**kwargs) + "\n")
101
102
    def check_file(self, filename):
103
        if not filename:
104
            raise Exception(
105
                "INPUT ERROR: Please specify the output filepath."
106
            )
107
        path = os.path.dirname(filename)
108
        path = path if path else "."
109
        if not os.path.exists(path):
110
            file_error = "INPUT_ERROR: Incorrect filepath."
111
            raise Exception(file_error)
112
113
    def save(self, filename, check="y", template=False):
114
        self.check_plugin_list_exists()
115
        # Check if a loader and saver are present.
116
        self.plugin_list._check_loaders()
117
        if check.lower() == "y":
118
            print(f"Saving file {filename}")
119
            if template:
120
                self.plugin_list.add_template(create=True)
121
            self.plugin_list._save_plugin_list(filename)
122
        else:
123
            print("The process list has NOT been saved.")
124
125
    def clear(self, check="y"):
126
        if check.lower() == "y":
127
            self.expand_dim = None
128
            self.plugin_list.plugin_list = []
129
130
    def check_plugin_list_exists(self):
131
        """ Check if plugin list is populated. """
132
        pos_list = self.get_positions()
133
        if not pos_list:
134
            print("There are no items to access in your list.")
135
            raise Exception("Please add an item to the process list.")
136
137
    def add(self, name, str_pos):
138
        self.check_for_plugin_failure(name)
139
        plugin = pu.plugins[name]()
140
        plugin.get_plugin_tools()._populate_default_parameters()
141
        pos, str_pos = self.convert_pos(str_pos)
142
        self.insert(plugin, pos, str_pos)
143
144
    def refresh(self, str_pos, defaults=False, change=False):
145
        pos = self.find_position(str_pos)
146
        plugin_entry = self.plugin_list.plugin_list[pos]
147
        name = change if change else plugin_entry["name"]
148
        active = plugin_entry["active"]
149
        plugin = pu.plugins[name]()
150
        plugin.get_plugin_tools()._populate_default_parameters()
151
        keep = self.get(pos)["data"] if not defaults else None
152
        self.insert(plugin, pos, str_pos, replace=True)
153
        self.plugin_list.plugin_list[pos]["active"] = active
154
        if keep:
155
            self._update_parameters(plugin, name, keep, str_pos)
156
157
158
    def check_for_plugin_failure(self, name):
159
        """Check if the plugin failed to load
160
161
        :param name: plugin name
162
        """
163
        if (name not in list(pu.plugins.keys())
164
                or self.plugin_in_failed_dict(name)):
165
                if self.plugin_in_failed_dict(name):
166
                    msg = f"IMPORT ERROR: {name} is unavailable due to" \
167
                          f" the following error:\n\t{self.failed[name]}"
168
                    raise Exception(msg)
169
                raise Exception("INPUT ERROR: Unknown plugin %s" % name)
170
171
172
    def plugin_in_failed_dict(self, name):
173
        """Check if plugin in failed dictionary
174
175
        :param name: plugin name
176
        :return: True if plugin name in the list of failed plugins
177
        """
178
        failed_plugin_list = list(self.failed.keys()) if self.failed else []
179
        return True if name in failed_plugin_list else False
180
181
182
    def check_preview_param(self, plugin_pos):
183
        """ Check that the plugin position number is valid and it contains
184
        a preview parameter
185
186
        :param plugin_pos:
187
        :return:
188
        """
189
        pos = self.find_position(plugin_pos)
190
        plugin_entry = self.plugin_list.plugin_list[pos]
191
        parameters = plugin_entry["data"]
192
        if "preview" not in parameters:
193
            raise Exception("You can only use this command with "
194
                            "plugins containing the preview parameter")
195
196
    def set_preview_display(self, expand_off, expand_dim, dim_view,
197
                            plugin_pos):
198
        """Set the dimensions_to_display value to "off" to prevent the
199
        preview parameter being shown in it's expanded form.
200
201
        If dimensions_to_display = "all", then all dimension slices are shown.
202
        If a number is provided to dim_view, that dimension is shown.
203
204
        :param expand_off: True if expand display should be turned off
205
        :param expand_dim: The dimension number to display, or "all"
206
        :param dim_view: True if only a certain dimension should be shown
207
        :param plugin_pos: Plugin position
208
        """
209
        if expand_off is True:
210
            self.expand_dim = None
211
            print(f"Expand diplay has been turned off")
212
        else:
213
            self.check_preview_param(plugin_pos)
214
            dims_to_display = expand_dim if dim_view else "all"
215
            self.expand_dim = dims_to_display
216
217
    def _update_parameters(self, plugin, name, keep, str_pos):
218
        union_params = set(keep).intersection(set(plugin.parameters))
219
        # Names of the parameter names present in both lists
220
        for param in union_params:
221
            self.modify(str_pos, param, keep[param], ref=True)
222
        # add any parameter mutations here
223
        classes = [c.__name__ for c in inspect.getmro(plugin.__class__)]
224
        m_dict = self.param_mutations
225
        keys = [k for k in list(m_dict.keys()) if k in classes]
226
227
        changes = False
228
        for k in keys:
229
            for entry in m_dict[k]["params"]:
230
                if entry["old"] in list(keep.keys()):
231
                    changes = True
232
                    val = keep[entry["old"]]
233
                    if "eval" in list(entry.keys()):
234
                        val = eval(entry["eval"])
235
                    self.modify(str_pos, entry["new"], val, ref=True)
236
        if changes:
237
            mutations.param_change_str(keep, plugin.parameters, name, keys)
238
239
    def _apply_plugin_updates(self, skip=False):
240
        # Update old process lists that start from 0
241
        the_list = self.plugin_list.plugin_list
242
        if "pos" in list(the_list[0].keys()) and the_list[0]["pos"] == "0":
243
            self.increment_positions()
244
245
        missing = []
246
        pos = len(the_list) - 1
247
        notices = mutations.plugin_notices
248
249
        for plugin in the_list[::-1]:
250
            # update old process lists to include 'active' flag
251
            if "active" not in list(plugin.keys()):
252
                plugin["active"] = True
253
254
            while True:
255
                name = the_list[pos]["name"]
256
                if name in notices.keys():
257
                    print(notices[name]["desc"])
258
259
                # if a plugin is missing from all available plugins
260
                # then look for mutations in the plugin name
261
                search = True if name not in pu.plugins.keys() else False
262
                found = self._mutate_plugins(name, pos, search=search)
263
                if search and not found:
264
                    str_pos = self.plugin_list.plugin_list[pos]["pos"]
265
                    missing.append([name, str_pos])
266
                    self.remove(pos)
267
                    pos -= 1
268
                if name == the_list[pos]["name"]:
269
                    break
270
            pos -= 1
271
272
        for name, pos in missing[::-1]:
273
            if skip:
274
                print(f"Skipping plugin {pos}: {name}")
275
            else:
276
                message = (
277
                    f"\nPLUGIN ERROR: The plugin {name} is "
278
                    f"unavailable in this version of Savu. \n Type open"
279
                    f" -s <process_list> to skip the broken plugin."
280
                )
281
                raise Exception(f"Incompatible process list. {message}")
282
283
    def _mutate_plugins(self, name, pos, search=False):
284
        """ Perform plugin mutations. """
285
        # check for case changes in plugin name
286
        if search:
287
            for key in pu.plugins.keys():
288
                if name.lower() == key.lower():
289
                    str_pos = self.plugin_list.plugin_list[pos]["pos"]
290
                    self.refresh(str_pos, change=key)
291
                    return True
292
293
        # check mutations dict
294
        m_dict = self.plugin_mutations
295
        if name in m_dict.keys():
296
            mutate = m_dict[name]
297
            if "replace" in mutate.keys():
298
                if mutate["replace"] in list(pu.plugins.keys()):
299
                    str_pos = self.plugin_list.plugin_list[pos]["pos"]
300
                    self.refresh(str_pos, change=mutate["replace"])
301
                    print(mutate["desc"])
302
                    return True
303
                raise Exception(
304
                    f"Replacement plugin {mutate['replace']} "
305
                    f"unavailable for {name}"
306
                )
307
            elif "remove" in mutate.keys():
308
                self.remove(pos)
309
                print(mutate["desc"])
310
            else:
311
                raise Exception("Unknown mutation type.")
312
        return False
313
314
    def move(self, old, new):
315
        old_pos = self.find_position(old)
316
        entry = self.plugin_list.plugin_list[old_pos]
317
        self.remove(old_pos)
318
        new_pos, new = self.convert_pos(new)
319
        name = entry["name"]
320
        self.insert(pu.plugins[name](), new_pos, new)
321
        self.plugin_list.plugin_list[new_pos] = entry
322
        self.plugin_list.plugin_list[new_pos]["pos"] = new
323
324
    def modify(self, pos_str, param_name, value, default=False, ref=False,
325
               dim=False):
326
        """Modify the plugin at pos_str and the parameter at param_name
327
        The new value will be set if it is valid.
328
329
        :param pos_str: The plugin position
330
        :param param_name: The parameter position/name
331
        :param value: The new parameter value
332
        :param default: True if value should be reverted to the default
333
        :param ref: boolean Refresh the plugin
334
        :param dim: The dimension to be modified
335
336
        returns: A boolean True if the value is a valid input for the
337
          selected parameter
338
        """
339
        pos = self.find_position(pos_str)
340
        plugin_entry = self.plugin_list.plugin_list[pos]
341
        tools = plugin_entry["tools"]
342
        parameters = plugin_entry["data"]
343
        params = plugin_entry["param"]
344
        param_name, value = self.setup_modify(params, param_name, value, ref)
345
        default_str = ["-d", "--default"]
346
        if default or value in default_str:
347
            value = tools.get_default_value(param_name)
348
            self._change_value(param_name, value, tools, parameters)
349
            valid_modification = True
350
        else:
351
            value = self._catch_parameter_tuning_syntax(value, param_name)
352
            valid_modification = self.modify_main(
353
                param_name, value, tools, parameters, dim
354
            )
355
        return valid_modification
356
357
    def _catch_parameter_tuning_syntax(self, value, param_name):
358
        """Check if the new parameter value seems like it is written
359
        in parameter tuning syntax with colons. If it is, then append
360
        a semi colon onto the end.
361
362
        :param value: new parameter value
363
        :param param_name:
364
        :return: modified value with semi colon appended
365
        """
366
        if self._is_multi_parameter_syntax(value) \
367
                and param_name != "preview":
368
            # Assume parameter tuning syntax is being used
369
            value = f"{value};"
370
            print("Parameter tuning syntax applied")
371
        return value
372
373
    def _is_multi_parameter_syntax(self, value):
374
        """If the value contains two colons, is not a dictionary,
375
        and doesnt already contain a semi colon, then assume that
376
        it is using parameter tuning syntax
377
378
        :param value: new parameter value
379
        :return boolean True if parameter tuning syntax is being used
380
        """
381
        isdict = re.findall(r"[\{\}]+", value)
382
        return (isinstance(value, str)
383
                and value.count(":") >= 2
384
                and not isdict
385
                and ";" not in value)
386
387
    def setup_modify(self, params, param_name, value, ref):
388
        """Get the parameter keys in the correct order and find
389
        the parameter name string
390
391
        :param params: The plugin parameters
392
        :param param_name: The parameter position/name
393
        :param value: The new parameter value
394
        :param ref: boolean Refresh the plugin
395
396
        return: param_name str to avoid discrepancy, value
397
        """
398
        if ref:
399
            # For a refresh, refresh all keys, including those with
400
            # dependencies (which have the display off)
401
            keys = params.keys()
402
        else:
403
            # Select the correct group and order of parameters according to that
404
            # on display to the user. This ensures correct parameter is modified.
405
            keys = pu.set_order_by_visibility(params)
406
            value = self.value(value)
407
        param_name = pu.param_to_str(param_name, keys)
408
        return param_name, value
409
410
    def modify_main(self, param_name, value, tools, parameters, dim):
411
        """Check the parameter is within the current parameter list.
412
        Check the new parameter value is valid, modify the parameter
413
        value, update defaults, check if dependent parameters should
414
        be made visible or hidden.
415
416
        :param param_name: The parameter position/name
417
        :param value: The new parameter value
418
        :param tools: The plugin tools
419
        :param parameters: The plugin parameters
420
        :param dim: The dimensions
421
422
        returns: A boolean True if the value is a valid input for the
423
          selected parameter
424
        """
425
        parameter_valid = False
426
        current_parameter_details = tools.param.get(param_name)
427
428
        # If dimensions are provided then alter preview param
429
        if self.preview_dimension_to_modify(dim, param_name):
430
            # Filter the dimension, dim1 or dim1.start
431
            dim, _slice = self._separate_dimension_and_slice(dim)
432
            value = self.modify_preview(
433
                parameters, param_name, value, dim, _slice
434
            )
435
436
        # If found, then the parameter is within the current parameter list
437
        # displayed to the user
438
        if current_parameter_details:
439
            value_check = pu._dumps(value)
440
            parameter_valid, error_str = param_u.is_valid(
441
                param_name, value_check, current_parameter_details
442
            )
443
            if parameter_valid:
444
                self._change_value(param_name, value, tools, parameters)
445
            else:
446
                value = str(value)
447
                display_value = f"{value[0:12]}.." if len(value) > 12 \
448
                                else value
449
                print(f"ERROR: The input value {display_value} "
450
                      f"for {param_name} is not correct.")
451
                print(error_str)
452
        else:
453
            print("Not in parameter keys.")
454
        return parameter_valid
455
456
    def _change_value(self, param_name, value, tools, parameters):
457
        """ Change the parameter "param_name" value inside the parameters list
458
        Update feedback messages for various dependant parameters
459
460
        :param param_name: The parameter position/name
461
        :param value: The new parameter value
462
        :param tools: The plugin tools
463
        :param parameters: The plugin parameters
464
        """
465
        # Save the value
466
        parameters[param_name] = value
467
        tools.warn_dependents(param_name, value)
468
        # Update the list of parameters to hide those dependent on others
469
        tools.check_dependencies(parameters)
470
471
    def check_required_args(self, value, required):
472
        """Check required argument 'value' is present
473
474
        :param value: Argument value
475
        :param required: bool, True if the argument is required
476
        """
477
        if required and (not value):
478
            raise Exception("Please enter a value")
479
480
        if (not required) and value:
481
            raise Exception(f"Unrecognised argument: {value}")
482
483
    def preview_dimension_to_modify(self, dim, param_name):
484
        """Check that the dimension string is only present when the parameter
485
        to modify is the preview parameter
486
487
        :param dim: Dimension string
488
        :param param_name: The parameter name (of the parameter to be modified)
489
        :return: True if dimension string is provided and the parameter to modify
490
        the preview parameter
491
        """
492
        if dim:
493
            if param_name == "preview":
494
                return True
495
            else:
496
                raise Exception(
497
                    "Please only use the dimension syntax when "
498
                    "modifying the preview parameter."
499
                )
500
        return False
501
502
    def modify_dimensions(self, pos_str, dim, check="y"):
503
        """Modify the plugin preview value. Remove or add dimensions
504
        to the preview parameter until the provided dimension number
505
        is reached.
506
507
        :param pos_str: The plugin position
508
        :param dim: The new number of dimensions
509
        :return True if preview is modified
510
        """
511
        pos = self.find_position(pos_str)
512
        plugin_entry = self.plugin_list.plugin_list[pos]
513
        parameters = plugin_entry["data"]
514
        self.check_param_exists(parameters, "preview")
515
        current_prev_list = pu._dumps(parameters["preview"])
516
        if not isinstance(current_prev_list, list):
517
            # Temporarily cover dict instance for preview
518
            print("This command is only possible for preview "
519
                  "values of the type list")
520
            return False
521
        pu.check_valid_dimension(dim, [])
522
        if check.lower() == "y":
523
            while len(current_prev_list) > dim:
524
                current_prev_list.pop()
525
            while len(current_prev_list) < dim:
526
                current_prev_list.append(":")
527
            parameters["preview"] = current_prev_list
528
            return True
529
        return False
530
531
    def check_param_exists(self, parameters, pname):
532
        """Check the parameter is present in the current parameter list
533
534
        :param parameters: Dictionary of parameters
535
        :param pname: Parameter name
536
        :return:
537
        """
538
        if not parameters.get(pname):
539
            raise Exception(
540
                f"The {pname} parameter is not available" f" for this plugin."
541
            )
542
543
    def value(self, value):
544
        if not value.count(";"):
545
            try:
546
                value = eval(value)
547
            except (NameError, SyntaxError):
548
                try:
549
                    value = eval(f"'{value}'")
550
                    # if there is one quotation mark there will be an error
551
                except EOFError:
552
                    raise EOFError(
553
                        "There is an end of line error. Please check your"
554
                        ' input for the character "\'".'
555
                    )
556
                except SyntaxError:
557
                    raise SyntaxError(
558
                        "There is a syntax error. Please check your input."
559
                    )
560
                except:
561
                    raise Exception("Please check your input.")
562
        return value
563
564
    def convert_to_ascii(self, value):
565
        ascii_list = []
566
        for v in value:
567
            ascii_list.append(v.encode("ascii", "ignore"))
568
        return ascii_list
569
570
    def on_and_off(self, str_pos, index):
571
        print(("switching plugin %s %s" % (str_pos, index)))
572
        status = True if index == "ON" else False
573
        pos = self.find_position(str_pos)
574
        self.plugin_list.plugin_list[pos]["active"] = status
575
576
    def convert_pos(self, str_pos):
577
        """ Converts the display position (input) to the equivalent numerical
578
        position and updates the display position if required.
579
580
        :param str_pos: the plugin display position (input) string.
581
        :returns: the equivalent numerical position of str_pos and and updated\
582
            str_pos.
583
        :rtype: (pos, str_pos)
584
        """
585
        pos_list = self.get_split_positions()
586
        num = re.findall(r"\d+", str_pos)[0]
587
        letter = re.findall("[a-z]", str_pos)
588
        entry = [num, letter[0]] if letter else [num]
589
590
        # full value already exists in the list
591
        if entry in pos_list:
592
            index = pos_list.index(entry)
593
            return self.inc_positions(index, pos_list, entry, 1)
594
595
        # only the number exists in the list
596
        num_list = [pos_list[i][0] for i in range(len(pos_list))]
597
        if entry[0] in num_list:
598
            start = num_list.index(entry[0])
599
            if len(entry) == 2:
600
                if len(pos_list[start]) == 2:
601
                    idx = int([i for i in range(len(num_list)) if
602
                               (num_list[i] == entry[0])][-1]) + 1
603
                    entry = [entry[0], str(chr(ord(pos_list[idx - 1][1]) + 1))]
604
                    return idx, ''.join(entry)
605
                if entry[1] == 'a':
606
                    self.plugin_list.plugin_list[start]['pos'] = entry[0] + 'b'
607
                    return start, ''.join(entry)
608
                else:
609
                    self.plugin_list.plugin_list[start]['pos'] = entry[0] + 'a'
610
                    return start + 1, entry[0] + 'b'
611
            return self.inc_positions(start, pos_list, entry, 1)
612
613
        # number not in list
614
        entry[0] = str(int(num_list[-1]) + 1 if num_list else 1)
615
        if len(entry) == 2:
616
            entry[1] = "a"
617
        return len(self.plugin_list.plugin_list), "".join(entry)
618
619
    def increment_positions(self):
620
        """Update old process lists that start plugin numbering from 0 to
621
        start from 1."""
622
        for plugin in self.plugin_list.plugin_list:
623
            str_pos = plugin["pos"]
624
            num = str(int(re.findall(r"\d+", str_pos)[0]) + 1)
625
            letter = re.findall("[a-z]", str_pos)
626
            plugin["pos"] = "".join([num, letter[0]] if letter else [num])
627
628
    def get_positions(self):
629
        """ Get a list of all current plugin entry positions. """
630
        elems = self.plugin_list.plugin_list
631
        pos_list = []
632
        for e in elems:
633
            pos_list.append(e["pos"])
634
        return pos_list
635
636
    def get_param_arg_str(self, pos_str, subelem):
637
        """Get the name of the parameter so that the display lists the
638
        correct item when the parameter order has been updated
639
640
        :param pos_str: The plugin position
641
        :param subelem: The parameter
642
        :return: The plugin.parameter_name argument
643
        """
644
        pos = self.find_position(pos_str)
645
        current_parameter_list = self.plugin_list.plugin_list[pos]["param"]
646
        current_parameter_list_ordered = pu.set_order_by_visibility(
647
            current_parameter_list
648
        )
649
        param_name = pu.param_to_str(subelem, current_parameter_list_ordered)
650
        param_argument = pos_str + "." + param_name
651
        return param_argument
652
653
    def get_split_positions(self):
654
        """ Separate numbers and letters in positions. """
655
        positions = self.get_positions()
656
        split_pos = []
657
        for i in range(len(positions)):
658
            num = re.findall(r"\d+", positions[i])[0]
659
            letter = re.findall("[a-z]", positions[i])
660
            split_pos.append([num, letter[0]] if letter else [num])
661
        return split_pos
662
663
    def find_position(self, pos):
664
        """ Find the numerical index of a position (a string). """
665
        pos_list = self.get_positions()
666
        if not pos_list:
667
            print("There are no items to access in your list.")
668
            raise Exception("Please add an item to the process list.")
669
        else:
670
            if pos not in pos_list:
671
                raise ValueError("Incorrect plugin position.")
672
            return pos_list.index(pos)
673
674
    def inc_positions(self, start, pos_list, entry, inc):
675
        if len(entry) == 1:
676
            self.inc_numbers(start, pos_list, inc)
677
        else:
678
            idx = [
679
                i
680
                for i in range(start, len(pos_list))
681
                if pos_list[i][0] == entry[0]
682
            ]
683
            self.inc_letters(idx, pos_list, inc)
684
        return start, "".join(entry)
685
686
    def inc_numbers(self, start, pos_list, inc):
687
        for i in range(start, len(pos_list)):
688
            pos_list[i][0] = str(int(pos_list[i][0]) + inc)
689
            self.plugin_list.plugin_list[i]["pos"] = "".join(pos_list[i])
690
691
    def inc_letters(self, idx, pos_list, inc):
692
        for i in idx:
693
            pos_list[i][1] = str(chr(ord(pos_list[i][1]) + inc))
694
            self.plugin_list.plugin_list[i]["pos"] = "".join(pos_list[i])
695
696
    def split_plugin_string(self, start, stop, subelem_view=False):
697
        """Find the start and stop number for the plugin range selected.
698
699
        :param start: Plugin starting index (including a subelem value
700
          if permitted)
701
        :param stop: Plugin stopping index
702
        :param subelem_view: False if subelem value not permitted
703
        :return: range_dict containing start stop (and possible subelem)
704
        """
705
        range_dict = {}
706
        if start:
707
            if subelem_view and "." in start:
708
                start, stop, subelem = self._split_subelem(start)
709
                range_dict["subelem"] = subelem
710
            else:
711
                start, stop = self._get_start_stop(start, stop)
712
            range_dict["start"] = start
713
            range_dict["stop"] = stop
714
        return range_dict
715
716
    def _get_start_stop(self, start, stop):
717
        """Find the start and stop number for the plugin range selected """
718
        start = self.find_position(start)
719
        stop = self.find_position(stop) + 1 if stop else start + 1
720
        return start, stop
721
722
    def _split_subelem(self, start, config_disp=True):
723
        """Separate the start string containing the plugin number,
724
        parameter(subelement), dimension and command
725
726
        :param start: The plugin to start at
727
        :param config_disp: True if command and dimension arguments
728
          are not permitted
729
        :return: start plugin, range_dict containing a subelem
730
            if a parameter is specified
731
        """
732
        start, subelem, dim, command = self.separate_plugin_subelem(
733
            start, config_disp
734
        )
735
        start, stop = self._get_start_stop(start, "")
736
        return start, stop, subelem
737
738
    def _check_command_valid(self, plugin_param, config_disp):
739
        """Check the plugin_param string length
740
741
        :param plugin_param: The string containing plugin number, parameter,
742
         and command
743
        :param config_disp: bool, True if command and dimension arguments are
744
          not permitted
745
        """
746
        if config_disp:
747
            if not 1 < len(plugin_param) < 3:
748
                raise ValueError(
749
                    "Use either 'plugin_pos.param_name' or"
750
                    " 'plugin_pos.param_no'"
751
                )
752
        else:
753
            # The modify command is being used
754
            if len(plugin_param) <= 1:
755
                raise ValueError(
756
                    "Please enter the plugin parameter to modify"
757
                    ". Either 'plugin_pos.param_name' or"
758
                    " 'plugin_pos.param_no'"
759
                )
760
            if not 1 < len(plugin_param) < 5:
761
                raise ValueError(
762
                    "Enter 'plugin_pos.param_no.dimension'. "
763
                    "Following the dimension, use start/stop/step"
764
                    " eg. '1.1.dim1.start' "
765
                )
766
767
    def separate_plugin_subelem(self, plugin_param, config_disp):
768
        """Separate the plugin number,parameter (subelement) number
769
        and additional command if present.
770
771
        :param plugin_param: A string supplied by the user input which
772
         contains the plugin element to edit/display. eg "1.1.dim.command"
773
        :param config_disp: bool, True if command and dimension arguments are
774
          not permitted
775
776
        :returns plugin: The number of the plugin element
777
                 subelem: The number of the parameter
778
                 dim: The dimension
779
                 command: The supplied command, eg expand or a dimension
780
                          string
781
        """
782
        plugin_param = plugin_param.split(".")
783
        plugin = plugin_param[0]
784
        start = self.find_position(plugin)
785
        self._check_command_valid(plugin_param, config_disp)
786
        subelem = plugin_param[1]
787
        if len(plugin_param) > 2:
788
            dim = self.dim_str_to_int(plugin_param[2])
789
            command = str(dim)
790
            if len(plugin_param) == 4:
791
                self._check_command_str(plugin_param[3])
792
                command += "." + plugin_param[3]
793
        else:
794
            dim, command = "", ""
795
        return plugin, subelem, dim, command
796
797
    def _check_command_str(self, command_str):
798
        """Check the additional 1.1.dim.command for slice or 'expand'
799
        keywords
800
        """
801
        command_list = ["expand", "start", "stop", "step", "chunk"]
802
        if command_str not in command_list:
803
            raise ValueError(
804
                "Following the dimension, use start/stop/step eg.  "
805
                "'1.1.dim1.start' "
806
            )
807
        return command_str
808
809
    def insert(self, plugin, pos, str_pos, replace=False):
810
        plugin_dict = self.create_plugin_dict(plugin)
811
        plugin_dict["pos"] = str_pos
812
        if replace:
813
            self.plugin_list.plugin_list[pos] = plugin_dict
814
        else:
815
            self.plugin_list.plugin_list.insert(pos, plugin_dict)
816
817
    def create_plugin_dict(self, plugin):
818
        tools = plugin.get_plugin_tools()
819
        plugin_dict = {}
820
        plugin_dict["name"] = plugin.name
821
        plugin_dict["id"] = plugin.__module__
822
        plugin_dict["data"] = plugin.parameters
823
        plugin_dict["active"] = True
824
        plugin_dict["tools"] = tools
825
        plugin_dict["param"] = tools.get_param_definitions()
826
        plugin_dict["doc"] = tools.docstring_info
827
        return plugin_dict
828
829
    def get(self, pos):
830
        return self.plugin_list.plugin_list[pos]
831
832
    def _separate_dimension_and_slice(self, command_str):
833
        """Check the start stop step command
834
835
        param command_str: a string '1.1' containing the dimension
836
            and slice number seperated by a full stop
837
838
        :returns dim, slice
839
        """
840
        if isinstance(command_str, str) and "." in command_str:
841
            # If the slice value is included
842
            slice_dict = {"start": 0, "stop": 1, "step": 2, "chunk": 3}
843
            _slice = slice_dict[command_str.split(".")[1]]
844
            dim = int(command_str.split(".")[0])
845
        else:
846
            dim = int(command_str)
847
            _slice = ""
848
        return dim, _slice
849
850
    def dim_str_to_int(self, dim_str):
851
        """Check the additional 1.1.dim keyword
852
853
        :param dim: A string 'dim1' specifying the dimension
854
855
        :return: dim - An integer dimension value
856
        """
857
        number = "".join(l for l in dim_str if l.isdigit())
858
        letters = "".join(l for l in dim_str if l.isalpha())
859
860
        if letters == "dim" and number.strip():
861
            dim = int(number)
862
        else:
863
            raise ValueError(
864
                "Following the second decimal place, please "
865
                "specify a dimension 1.1.dim1 or 1.preview.dim1"
866
            )
867
        return dim
868
869
    def modify_preview(self, parameters, param_name, value, dim, _slice):
870
        """ Check the entered value is valid and edit preview"""
871
        slice_list = [0, 1, 2, 3]
872
        type_check_value = pu._dumps(value)
873
        current_preview_value = pu._dumps(parameters[param_name])
874
        pu.check_valid_dimension(dim, current_preview_value)
875
        if _slice in slice_list:
876
            # Modify this dimension and slice only
877
            if param_u._preview_dimension_singular(type_check_value):
878
                value = self._modify_preview_dimension_slice(
879
                    value, current_preview_value, dim, _slice
880
                )
881
            else:
882
                raise ValueError(
883
                    "Invalid preview dimension slice value. Please "
884
                    "enter a float, an integer or a string including "
885
                    "only mid and end keywords."
886
                )
887
        else:
888
            # If the entered value is a valid dimension value
889
            if param_u._preview_dimension(type_check_value):
890
                # Modify the whole dimension
891
                value = self._modify_preview_dimension(
892
                    value, current_preview_value, dim
893
                )
894
            else:
895
                raise ValueError(
896
                    "Invalid preview dimension value. Please "
897
                    "enter a float, an integer or slice notation."
898
                )
899
        return value
900
901
    def _modify_preview_dimension_slice(self, value, current_val, dim, _slice):
902
        """Modify the preview dimension slice value at the dimension (dim)
903
        provided
904
905
        :param value: The new value
906
        :param current_value: The current preview parameter value
907
        :param dim: The dimension to modify
908
        :param _slice: The slice value to modify
909
        :return: The modified value
910
        """
911
        if not current_val:
912
            current_val = self._set_empty_list(
913
                dim, self._set_empty_dimension_slice(value, _slice)
914
            )
915
        else:
916
            current_val[dim - 1] = self._modified_slice_notation(
917
                current_val[dim - 1], value, _slice
918
            )
919
        return current_val
920
921
    def _modified_slice_notation(self, old_value, value, _slice):
922
        """Change the current value at the provided slice
923
924
        :param old_value: Previous slice notation
925
        :param value: New value to set
926
        :param _slice: Slice to modify
927
        :return: Changed value (str/int)
928
        """
929
        old_value = self._set_incomplete_slices(str(old_value), _slice)
930
        if pu.is_slice_notation(old_value):
931
            start_stop_split = old_value.split(":")
932
            return self._get_modified_slice(start_stop_split, value, _slice)
933
        elif _slice == 0:
934
            # If there is no slice notation, only allow first slice to
935
            # be modified
936
            return value
937
        else:
938
            raise Exception(
939
                "There is no existing slice notation to modify."
940
            )
941
942
    def _get_modified_slice(self, start_stop_split, value, _slice):
943
        if all(v == "" for v in start_stop_split):
944
            return self._set_empty_dimension_slice(value, _slice)
945
        else:
946
            start_stop_split[_slice] = str(value)
947
            start_stop_split = ":".join(start_stop_split)
948
            return start_stop_split
949
950
    def _modify_preview_dimension(self, value, current_preview, dim):
951
        """Modify the preview list value at the dimension provided (dim)
952
953
        :param value: The new value
954
        :param current_value: The current preview parameter list value
955
        :param dim: The dimension to modify
956
        :return: The modified value
957
        """
958
        if not current_preview:
959
            return self._set_empty_list(dim, value)
960
        current_preview[dim - 1] = value
961
        # Save the altered preview value
962
        return current_preview
963
964
    def _set_empty_dimension_slice(self, value, _slice):
965
        """Set the empty dimension and insert colons to indicate
966
        the correct slice notation.
967
968
        :param value: New value to set
969
        :param _slice: start/stop/step/chunk value
970
        :return: String for the new value
971
        """
972
        return _slice * ":" + str(value)
973
974
    def _set_incomplete_slices(self, old_value, _slice):
975
        """Append default slice values.to the current string value, in order
976
         to allow later slice values to be set
977
978
        :param old_value: Current string value with slice notation
979
        :param _slice: slice notation index to be edited
980
        :return: String with default slice notation entries set
981
        """
982
        while old_value.count(":") < _slice:
983
            old_value += ":"
984
        return old_value
985
986
    def _set_empty_list(self, dim, value):
987
        """If the dimension is 1 then allow the whole empty list
988
        to be set.
989
990
        :param dim: Dimension to be altered
991
        :param value: New value
992
        :return: List containing new value
993
        """
994
        if dim == 1:
995
            return [value]
996
        else:
997
            raise ValueError("You have not set earlier dimensions")
998
999
    def level(self, level):
1000
        """ Set the visibility level of parameters """
1001
        if level:
1002
            self.disp_level = level
1003
            print(f"Level set to '{level}'")
1004
        else:
1005
            print(f"Level is set at '{self.disp_level}'")
1006
1007
    def remove(self, pos):
1008
        if pos >= self.size:
1009
            raise Exception(
1010
                "Cannot remove plugin %s as it does not exist."
1011
                % self.plugin_list.plugin_list[pos]["name"]
1012
            )
1013
        pos_str = self.plugin_list.plugin_list[pos]["pos"]
1014
        self.plugin_list.plugin_list.pop(pos)
1015
        pos_list = self.get_split_positions()
1016
        self.inc_positions(pos, pos_list, pos_str, -1)
1017
1018
    @property
1019
    def size(self):
1020
        return len(self.plugin_list.plugin_list)
1021