Test Failed
Push — master ( a95b01...df7418 )
by
unknown
01:40 queued 20s
created

Content.on_and_off()   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nop 3
dl 0
loc 5
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 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
        n_pos = self.find_position(new)
356
        entry = self.plugin_list.plugin_list[old_pos]
357
        self.remove(old)
358
        new_pos, new_pos_str = self.convert_pos(new)
359
        name = entry["name"]
360
        self.insert(pu.plugins[name](), new_pos, new_pos_str)
361
        self.plugin_list.plugin_list[new_pos] = entry
362
        self.plugin_list.plugin_list[new_pos]["pos"] = new_pos_str
363
        self.check_iterative_loops([old_pos + 1, new_pos + 1], 0)
364
365
    def replace(self, old, new_plugin):
366
        self.check_for_plugin_failure(new_plugin)
367
        self.remove(old)
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 split_pos_to_list(self, str_pos):
671
        """Split the position string into a list of numbers and letters
672
        :param str_pos: the plugin display position string.
673
        :returns: a list with the position number and letter
674
        """
675
        num = re.findall(r"\d+", str_pos)[0]
676
        letter = re.findall("[a-z]", str_pos)
677
        entry = [num, letter[0]] if letter else [num]
678
        return entry
679
680
    def convert_pos(self, str_pos):
681
        """ Converts the display position (input) to the equivalent numerical
682
        position and updates the display position if required.
683
684
        :param str_pos: the plugin display position (input) string.
685
        :returns: the equivalent numerical position of str_pos and and updated\
686
            str_pos.
687
        :rtype: (pos, str_pos)
688
        """
689
        pos_list = self.get_split_positions()
690
        entry = self.split_pos_to_list(str_pos)
691
692
        # full value already exists in the list
693
        if entry in pos_list:
694
            index = pos_list.index(entry)
695
            return self.inc_positions(index, pos_list, entry, 1)
696
697
        # only the number exists in the list
698
        num_list = [pos_list[i][0] for i in range(len(pos_list))]
699
        if entry[0] in num_list:
700
            start = num_list.index(entry[0])
701
            if len(entry) == 2:
702
                if len(pos_list[start]) == 2:
703
                    idx = int([i for i in range(len(num_list)) if
704
                               (num_list[i] == entry[0])][-1]) + 1
705
                    entry = [entry[0], str(chr(ord(pos_list[idx - 1][1]) + 1))]
706
                    return idx, ''.join(entry)
707
                if entry[1] == 'a':
708
                    self.plugin_list.plugin_list[start]['pos'] = entry[0] + 'b'
709
                    return start, ''.join(entry)
710
                else:
711
                    self.plugin_list.plugin_list[start]['pos'] = entry[0] + 'a'
712
                    return start + 1, entry[0] + 'b'
713
            return self.inc_positions(start, pos_list, entry, 1)
714
715
        # number not in list
716
        entry[0] = str(int(num_list[-1]) + 1 if num_list else 1)
717
        if len(entry) == 2:
718
            entry[1] = "a"
719
        return len(self.plugin_list.plugin_list), "".join(entry)
720
721
    def increment_positions(self):
722
        """Update old process lists that start plugin numbering from 0 to
723
        start from 1."""
724
        for plugin in self.plugin_list.plugin_list:
725
            str_pos = plugin["pos"]
726
            num = str(int(re.findall(r"\d+", str_pos)[0]) + 1)
727
            letter = re.findall("[a-z]", str_pos)
728
            plugin["pos"] = "".join([num, letter[0]] if letter else [num])
729
730
    def get_positions(self):
731
        """ Get a list of all current plugin entry positions. """
732
        elems = self.plugin_list.plugin_list
733
        pos_list = []
734
        for e in elems:
735
            pos_list.append(e["pos"])
736
        return pos_list
737
738
    def get_param_arg_str(self, pos_str, subelem):
739
        """Get the name of the parameter so that the display lists the
740
        correct item when the parameter order has been updated
741
742
        :param pos_str: The plugin position
743
        :param subelem: The parameter
744
        :return: The plugin.parameter_name argument
745
        """
746
        pos = self.find_position(pos_str)
747
        current_parameter_list = self.plugin_list.plugin_list[pos]["param"]
748
        current_parameter_list_ordered = pu.set_order_by_visibility(
749
            current_parameter_list
750
        )
751
        param_name = pu.param_to_str(subelem, current_parameter_list_ordered)
752
        param_argument = pos_str + "." + param_name
753
        return param_argument
754
755
    def get_split_positions(self):
756
        """ Separate numbers and letters in positions. """
757
        positions = self.get_positions()
758
        split_pos = []
759
        for i in range(len(positions)):
760
            num = re.findall(r"\d+", positions[i])[0]
761
            letter = re.findall("[a-z]", positions[i])
762
            split_pos.append([num, letter[0]] if letter else [num])
763
        return split_pos
764
765
    def find_position(self, pos):
766
        """ Find the numerical index of a position (a string). """
767
        pos_list = self.get_positions()
768
        if not pos_list:
769
            print("There are no items to access in your list.")
770
            raise Exception("Please add an item to the process list.")
771
        else:
772
            if pos not in pos_list:
773
                raise ValueError("Incorrect plugin position.")
774
            return pos_list.index(pos)
775
776
    def inc_positions(self, start, pos_list, entry, inc):
777
        """ Increment plugin positions following index start, by provided inc
778
        :param start: Plugin starting index
779
        :param pos_list: plugin position list
780
        :param entry: entry list
781
        :param inc: increment value
782
        """
783
        if len(entry) == 1:
784
            self.inc_numbers(start, pos_list, inc)
785
        else:
786
            idx = [
787
                i
788
                for i in range(start, len(pos_list))
789
                if pos_list[i][0] == entry[0]
790
            ]
791
            self.inc_letters(idx, pos_list, inc)
792
        return start, "".join(entry)
793
794
    def inc_numbers(self, start, pos_list, inc):
795
        for i in range(start, len(pos_list)):
796
            pos_list[i][0] = str(int(pos_list[i][0]) + inc)
797
            self.plugin_list.plugin_list[i]["pos"] = "".join(pos_list[i])
798
799
    def inc_letters(self, idx, pos_list, inc):
800
        for i in idx:
801
            pos_list[i][1] = str(chr(ord(pos_list[i][1]) + inc))
802
            self.plugin_list.plugin_list[i]["pos"] = "".join(pos_list[i])
803
804
    def split_plugin_string(self, start, stop, subelem_view=False):
805
        """Find the start and stop number for the plugin range selected.
806
807
        :param start: Plugin starting index (including a subelem value
808
          if permitted)
809
        :param stop: Plugin stopping index
810
        :param subelem_view: False if subelem value not permitted
811
        :return: range_dict containing start stop (and possible subelem)
812
        """
813
        range_dict = {}
814
        if start:
815
            if subelem_view and "." in start:
816
                start, stop, subelem = self._split_subelem(start)
817
                range_dict["subelem"] = subelem
818
            else:
819
                start, stop = self._get_start_stop(start, stop)
820
            range_dict["start"] = start
821
            range_dict["stop"] = stop
822
        return range_dict
823
824
    def _get_start_stop(self, start, stop):
825
        """Find the start and stop number for the plugin range selected """
826
        start = self.find_position(start)
827
        stop = self.find_position(stop) + 1 if stop else start + 1
828
        return start, stop
829
830
    def _split_subelem(self, start, config_disp=True):
831
        """Separate the start string containing the plugin number,
832
        parameter(subelement), dimension and command
833
834
        :param start: The plugin to start at
835
        :param config_disp: True if command and dimension arguments
836
          are not permitted
837
        :return: start plugin, range_dict containing a subelem
838
            if a parameter is specified
839
        """
840
        start, subelem, dim, command = self.separate_plugin_subelem(
841
            start, config_disp
842
        )
843
        start, stop = self._get_start_stop(start, "")
844
        return start, stop, subelem
845
846
    def _check_command_valid(self, plugin_param, config_disp):
847
        """Check the plugin_param string length
848
849
        :param plugin_param: The string containing plugin number, parameter,
850
         and command
851
        :param config_disp: bool, True if command and dimension arguments are
852
          not permitted
853
        """
854
        if config_disp:
855
            if not 1 < len(plugin_param) < 3:
856
                raise ValueError(
857
                    "Use either 'plugin_pos.param_name' or"
858
                    " 'plugin_pos.param_no'"
859
                )
860
        else:
861
            # The modify command is being used
862
            if len(plugin_param) <= 1:
863
                raise ValueError(
864
                    "Please enter the plugin parameter to modify"
865
                    ". Either 'plugin_pos.param_name' or"
866
                    " 'plugin_pos.param_no'"
867
                )
868
            if not 1 < len(plugin_param) < 5:
869
                raise ValueError(
870
                    "Enter 'plugin_pos.param_no.dimension'. "
871
                    "Following the dimension, use start/stop/step"
872
                    " eg. '1.1.dim1.start' "
873
                )
874
875
    def separate_plugin_subelem(self, plugin_param, config_disp):
876
        """Separate the plugin number,parameter (subelement) number
877
        and additional command if present.
878
879
        :param plugin_param: A string supplied by the user input which
880
         contains the plugin element to edit/display. eg "1.1.dim.command"
881
        :param config_disp: bool, True if command and dimension arguments are
882
          not permitted
883
884
        :returns plugin: The number of the plugin element
885
                 subelem: The number of the parameter
886
                 dim: The dimension
887
                 command: The supplied command, eg expand or a dimension
888
                          string
889
        """
890
        plugin_param = plugin_param.split(".")
891
        plugin = plugin_param[0]
892
        # change str plugin name to a number
893
        start = self.find_position(plugin)
894
        self._check_command_valid(plugin_param, config_disp)
895
        subelem = plugin_param[1]
896
        if len(plugin_param) > 2:
897
            dim = self.dim_str_to_int(plugin_param[2])
898
            command = str(dim)
899
            if len(plugin_param) == 4:
900
                self._check_command_str(plugin_param[3])
901
                command += "." + plugin_param[3]
902
        else:
903
            dim, command = "", ""
904
        return plugin, subelem, dim, command
905
906
    def _check_command_str(self, command_str):
907
        """Check the additional 1.1.dim.command for slice or 'expand'
908
        keywords
909
        """
910
        command_list = ["expand", "start", "stop", "step", "chunk"]
911
        if command_str not in command_list:
912
            raise ValueError(
913
                "Following the dimension, use start/stop/step eg.  "
914
                "'1.1.dim1.start' "
915
            )
916
        return command_str
917
918
    def insert(self, plugin, pos, str_pos, replace=False):
919
        plugin_dict = self.create_plugin_dict(plugin)
920
        plugin_dict["pos"] = str_pos
921
        if replace:
922
            self.plugin_list.plugin_list[pos] = plugin_dict
923
        else:
924
            self.plugin_list.plugin_list.insert(pos, plugin_dict)
925
926
    def create_plugin_dict(self, plugin):
927
        tools = plugin.get_plugin_tools()
928
        plugin_dict = {}
929
        plugin_dict["name"] = plugin.name
930
        plugin_dict["id"] = plugin.__module__
931
        plugin_dict["data"] = plugin.parameters
932
        plugin_dict["active"] = True
933
        plugin_dict["tools"] = tools
934
        plugin_dict["param"] = tools.get_param_definitions()
935
        plugin_dict["doc"] = tools.docstring_info
936
        return plugin_dict
937
938
    def get(self, pos):
939
        return self.plugin_list.plugin_list[pos]
940
941
    def _separate_dimension_and_slice(self, command_str):
942
        """Check the start stop step command
943
944
        param command_str: a string '1.1' containing the dimension
945
            and slice number seperated by a full stop
946
947
        :returns dim, slice
948
        """
949
        if isinstance(command_str, str) and "." in command_str:
950
            # If the slice value is included
951
            slice_dict = {"start": 0, "stop": 1, "step": 2, "chunk": 3}
952
            _slice = slice_dict[command_str.split(".")[1]]
953
            dim = int(command_str.split(".")[0])
954
        else:
955
            dim = int(command_str)
956
            _slice = ""
957
        return dim, _slice
958
959
    def dim_str_to_int(self, dim_str):
960
        """Check the additional 1.1.dim keyword
961
962
        :param dim: A string 'dim1' specifying the dimension
963
964
        :return: dim - An integer dimension value
965
        """
966
        number = "".join(l for l in dim_str if l.isdigit())
967
        letters = "".join(l for l in dim_str if l.isalpha())
968
969
        if letters == "dim" and number.strip():
970
            dim = int(number)
971
        else:
972
            raise ValueError(
973
                "Following the second decimal place, please "
974
                "specify a dimension 1.1.dim1 or 1.preview.dim1"
975
            )
976
        return dim
977
978
    def modify_preview(self, parameters, param_name, value, dim, _slice):
979
        """ Check the entered value is valid and edit preview"""
980
        slice_list = [0, 1, 2, 3]
981
        type_check_value = pu._dumps(value)
982
        current_preview_value = pu._dumps(parameters[param_name])
983
        pu.check_valid_dimension(dim, current_preview_value)
984
        if _slice in slice_list:
985
            # Modify this dimension and slice only
986
            if param_u._preview_dimension_singular(type_check_value):
987
                value = self._modify_preview_dimension_slice(
988
                    value, current_preview_value, dim, _slice
989
                )
990
            else:
991
                raise ValueError(
992
                    "Invalid preview dimension slice value. Please "
993
                    "enter a float, an integer or a string including "
994
                    "only start, mid and end keywords."
995
                )
996
        else:
997
            # If the entered value is a valid dimension value
998
            if param_u._preview_dimension(type_check_value):
999
                # Modify the whole dimension
1000
                value = self._modify_preview_dimension(
1001
                    value, current_preview_value, dim
1002
                )
1003
            else:
1004
                raise ValueError(
1005
                    "Invalid preview dimension value. Please "
1006
                    "enter a float, an integer or slice notation."
1007
                )
1008
        return value
1009
1010
    def _modify_preview_dimension_slice(self, value, current_val, dim, _slice):
1011
        """Modify the preview dimension slice value at the dimension (dim)
1012
        provided
1013
1014
        :param value: The new value
1015
        :param current_value: The current preview parameter value
1016
        :param dim: The dimension to modify
1017
        :param _slice: The slice value to modify
1018
        :return: The modified value
1019
        """
1020
        if not current_val:
1021
            current_val = self._set_empty_list(
1022
                dim, self._set_empty_dimension_slice(value, _slice)
1023
            )
1024
        else:
1025
            current_val[dim - 1] = self._modified_slice_notation(
1026
                current_val[dim - 1], value, _slice
1027
            )
1028
        return current_val
1029
1030
    def _modified_slice_notation(self, old_value, value, _slice):
1031
        """Change the current value at the provided slice
1032
1033
        :param old_value: Previous slice notation
1034
        :param value: New value to set
1035
        :param _slice: Slice to modify
1036
        :return: Changed value (str/int)
1037
        """
1038
        old_value = self._set_incomplete_slices(str(old_value), _slice)
1039
        if pu.is_slice_notation(old_value):
1040
            start_stop_split = old_value.split(":")
1041
            return self._get_modified_slice(start_stop_split, value, _slice)
1042
        elif _slice == 0:
1043
            # If there is no slice notation, only allow first slice to
1044
            # be modified
1045
            return value
1046
        else:
1047
            raise Exception(
1048
                "There is no existing slice notation to modify."
1049
            )
1050
1051
    def _get_modified_slice(self, start_stop_split, value, _slice):
1052
        if all(v == "" for v in start_stop_split):
1053
            return self._set_empty_dimension_slice(value, _slice)
1054
        else:
1055
            start_stop_split[_slice] = str(value)
1056
            start_stop_split = ":".join(start_stop_split)
1057
            return start_stop_split
1058
1059
    def _modify_preview_dimension(self, value, current_preview, dim):
1060
        """Modify the preview list value at the dimension provided (dim)
1061
1062
        :param value: The new value
1063
        :param current_value: The current preview parameter list value
1064
        :param dim: The dimension to modify
1065
        :return: The modified value
1066
        """
1067
        if not current_preview:
1068
            return self._set_empty_list(dim, value)
1069
        current_preview[dim - 1] = value
1070
        # Save the altered preview value
1071
        return current_preview
1072
1073
    def _set_empty_dimension_slice(self, value, _slice):
1074
        """Set the empty dimension and insert colons to indicate
1075
        the correct slice notation.
1076
1077
        :param value: New value to set
1078
        :param _slice: start/stop/step/chunk value
1079
        :return: String for the new value
1080
        """
1081
        return _slice * ":" + str(value)
1082
1083
    def _set_incomplete_slices(self, old_value, _slice):
1084
        """Append default slice values.to the current string value, in order
1085
         to allow later slice values to be set
1086
1087
        :param old_value: Current string value with slice notation
1088
        :param _slice: slice notation index to be edited
1089
        :return: String with default slice notation entries set
1090
        """
1091
        while old_value.count(":") < _slice:
1092
            old_value += ":"
1093
        return old_value
1094
1095
    def _set_empty_list(self, dim, value):
1096
        """If the dimension is 1 then allow the whole empty list
1097
        to be set.
1098
1099
        :param dim: Dimension to be altered
1100
        :param value: New value
1101
        :return: List containing new value
1102
        """
1103
        if dim == 1:
1104
            return [value]
1105
        else:
1106
            raise ValueError("You have not set earlier dimensions")
1107
1108
    def level(self, level):
1109
        """ Set the visibility level of parameters """
1110
        if level:
1111
            self.disp_level = level
1112
            print(f"Level set to '{level}'")
1113
        else:
1114
            print(f"Level is set at '{self.disp_level}'")
1115
1116
    def remove(self, str_pos):
1117
        pos = self.find_position(str_pos)
1118
        entry = self.split_pos_to_list(str_pos)
1119
        self.plugin_list.plugin_list.pop(pos)
1120
        pos_list = self.get_split_positions()
1121
        self.inc_positions(pos, pos_list, entry, -1)
1122
1123
    def check_iterative_loops(self, positions, direction):
1124
        """
1125
        When a plugin is added, removed, or moved, check if any iterative loops
1126
        should be removed or shifted
1127
        """
1128
        def moved_plugin(old_pos, new_pos):
1129
            is_in_loop = self.plugin_list.check_pos_in_iterative_loop(old_pos)
1130
            if is_in_loop and old_pos != new_pos:
1131
                self.plugin_list.remove_associated_iterate_group_dict(
1132
                    old_pos, -1)
1133
1134
            if_will_be_in_loop = \
1135
                self.plugin_list.check_pos_in_iterative_loop(new_pos)
1136
            if if_will_be_in_loop and old_pos != new_pos:
1137
                self.plugin_list.remove_associated_iterate_group_dict(
1138
                    new_pos, -1)
1139
1140
            # shift any relevant loops
1141
            if new_pos < old_pos:
1142
                self.plugin_list.shift_range_iterative_loops(
1143
                    [new_pos, old_pos], 1)
1144
            elif new_pos > old_pos:
1145
                self.plugin_list.shift_range_iterative_loops(
1146
                    [old_pos, new_pos], -1)
1147
1148
        def added_removed_plugin(pos, direction):
1149
            is_in_loop = self.plugin_list.check_pos_in_iterative_loop(pos)
1150
            if is_in_loop:
1151
                # delete the associated loop
1152
                self.plugin_list.remove_associated_iterate_group_dict(pos,
1153
                    direction)
1154
1155
            # check if there are any iterative loops in the process list
1156
            do_loops_exist = len(self.plugin_list.iterate_plugin_groups) > 0
1157
            if do_loops_exist:
1158
                if direction == -1:
1159
                    # shift the start+end of all loops after the plugin down by
1160
                    # 1
1161
                    self.plugin_list.shift_subsequent_iterative_loops(pos, -1)
1162
                elif direction == 1:
1163
                    # shift the start+end of all loops after the plugin up by 1
1164
                    self.plugin_list.shift_subsequent_iterative_loops(pos, 1)
1165
1166
        if direction == 0:
1167
            # a plugin has been moved
1168
            moved_plugin(positions[0], positions[1])
1169
        else:
1170
            # a plugin has been added or removed
1171
            added_removed_plugin(positions[0], direction)
1172
1173
    @property
1174
    def size(self):
1175
        return len(self.plugin_list.plugin_list)
1176
1177
    def display_iterative_loops(self):
1178
        self.plugin_list.print_iterative_loops()
1179