Test Failed
Push — master ( 26b3f2...a95b01 )
by Daniil
01:45 queued 20s
created

Content.modify_global()   A

Complexity

Conditions 2

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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