Test Failed
Pull Request — master (#888)
by Daniil
03:51
created

Content.check_iterative_loops()   D

Complexity

Conditions 12

Size

Total Lines 49
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 31
nop 3
dl 0
loc 49
rs 4.8
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like scripts.config_generator.content.Content.check_iterative_loops() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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