Test Failed
Pull Request — master (#888)
by Daniil
03:57
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 not None:
148
            self.plugin_list.remove_iterate_plugin_groups(args.remove)
149
        elif args.set is not None:
150
            # create a dict representing a group of plugins to iterate over
151
            start = args.set[0]
152
            end = args.set[1]
153
            iterations = args.set[2]
154
            # set the plugins to be identified as iterative in the plugins dictionary
155
            for i in range((end-start)+1):
156
                self.plugin_list.plugin_list[i+start-1]['iterative'] = True
157
            self.plugin_list.add_iterate_plugin_group(start, end, iterations)
158
159
    def refresh(self, str_pos, defaults=False, change=False):
160
        pos = self.find_position(str_pos)
161
        plugin_entry = self.plugin_list.plugin_list[pos]
162
        name = change if change else plugin_entry["name"]
163
        active = plugin_entry["active"]
164
        plugin = pu.plugins[name]()
165
        plugin.get_plugin_tools()._populate_default_parameters()
166
        keep = self.get(pos)["data"] if not defaults else None
167
        self.insert(plugin, pos, str_pos, replace=True)
168
        self.plugin_list.plugin_list[pos]["active"] = active
169
        if keep:
170
            self._update_parameters(plugin, name, keep, str_pos)
171
172
    def duplicate(self, dupl_pos, new):
173
        """ Duplicate the plugin at position dupl_pos
174
        and insert it at the new position
175
176
        :param dupl_pos: Position of the plugin to duplicate
177
        :param new: New plugin position
178
        """
179
        pos = self.find_position(dupl_pos)
180
        new_pos, new_pos_str = self.convert_pos(new)
181
        plugin_entry = copy.deepcopy(self.plugin_list.plugin_list[pos])
182
        plugin_entry["pos"] = new_pos_str
183
        self.plugin_list.plugin_list.insert(new_pos, plugin_entry)
184
185
    def check_for_plugin_failure(self, name):
186
        """Check if the plugin failed to load
187
188
        :param name: plugin name
189
        """
190
        if (name not in list(pu.plugins.keys())
191
                or self.plugin_in_failed_dict(name)):
192
                if self.plugin_in_failed_dict(name):
193
                    msg = f"IMPORT ERROR: {name} is unavailable due to" \
194
                          f" the following error:\n\t{self.failed[name]}"
195
                    raise Exception(msg)
196
                raise Exception("INPUT ERROR: Unknown plugin %s" % name)
197
198
    def plugin_in_failed_dict(self, name):
199
        """Check if plugin in failed dictionary
200
201
        :param name: plugin name
202
        :return: True if plugin name in the list of failed plugins
203
        """
204
        failed_plugin_list = list(self.failed.keys()) if self.failed else []
205
        return True if name in failed_plugin_list else False
206
207
    def check_preview_param(self, plugin_pos):
208
        """ Check that the plugin position number is valid and it contains
209
        a preview parameter
210
211
        :param plugin_pos:
212
        :return:
213
        """
214
        pos = self.find_position(plugin_pos)
215
        plugin_entry = self.plugin_list.plugin_list[pos]
216
        parameters = plugin_entry["data"]
217
        if "preview" not in parameters:
218
            raise Exception("You can only use this command with "
219
                            "plugins containing the preview parameter")
220
221
    def set_preview_display(self, expand_off, expand_dim, dim_view,
222
                            plugin_pos):
223
        """Set the dimensions_to_display value to "off" to prevent the
224
        preview parameter being shown in it's expanded form.
225
226
        If dimensions_to_display = "all", then all dimension slices are shown.
227
        If a number is provided to dim_view, that dimension is shown.
228
229
        :param expand_off: True if expand display should be turned off
230
        :param expand_dim: The dimension number to display, or "all"
231
        :param dim_view: True if only a certain dimension should be shown
232
        :param plugin_pos: Plugin position
233
        """
234
        if expand_off is True:
235
            self.expand_dim = None
236
            print(f"Expand diplay has been turned off")
237
        else:
238
            self.check_preview_param(plugin_pos)
239
            dims_to_display = expand_dim if dim_view else "all"
240
            self.expand_dim = dims_to_display
241
242
    def _update_parameters(self, plugin, name, keep, str_pos):
243
        union_params = set(keep).intersection(set(plugin.parameters))
244
        # Names of the parameter names present in both lists
245
        for param in union_params:
246
            self.modify(str_pos, param, keep[param], ref=True)
247
        # add any parameter mutations here
248
        classes = [c.__name__ for c in inspect.getmro(plugin.__class__)]
249
        m_dict = self.param_mutations
250
        keys = [k for k in list(m_dict.keys()) if k in classes]
251
252
        changes = False
253
        for k in keys:
254
            for entry in m_dict[k]["params"]:
255
                if entry["old"] in list(keep.keys()):
256
                    changes = True
257
                    val = keep[entry["old"]]
258
                    if "eval" in list(entry.keys()):
259
                        val = eval(entry["eval"])
260
                    self.modify(str_pos, entry["new"], val, ref=True)
261
        if changes:
262
            mutations.param_change_str(keep, plugin.parameters, name, keys)
263
264
    def _apply_plugin_updates(self, skip=False):
265
        # Update old process lists that start from 0
266
        the_list = self.plugin_list.plugin_list
267
        if "pos" in list(the_list[0].keys()) and the_list[0]["pos"] == "0":
268
            self.increment_positions()
269
270
        missing = []
271
        pos = len(the_list) - 1
272
        notices = mutations.plugin_notices
273
274
        for plugin in the_list[::-1]:
275
            # update old process lists to include 'active' flag
276
            if "active" not in list(plugin.keys()):
277
                plugin["active"] = True
278
279
            while True:
280
                name = the_list[pos]["name"]
281
                if name in notices.keys():
282
                    print(notices[name]["desc"])
283
284
                # if a plugin is missing from all available plugins
285
                # then look for mutations in the plugin name
286
                search = True if name not in pu.plugins.keys() else False
287
                found = self._mutate_plugins(name, pos, search=search)
288
                if search and not found:
289
                    str_pos = self.plugin_list.plugin_list[pos]["pos"]
290
                    missing.append([name, str_pos])
291
                    self.remove(pos)
292
                    pos -= 1
293
                if name == the_list[pos]["name"]:
294
                    break
295
            pos -= 1
296
297
        for name, pos in missing[::-1]:
298
            if skip:
299
                print(f"Skipping plugin {pos}: {name}")
300
            else:
301
                message = (
302
                    f"\nPLUGIN ERROR: The plugin {name} is "
303
                    f"unavailable in this version of Savu. \nThe plugin is "
304
                    f"missing from the position {pos} in the process list. "
305
                    f"\n Type open -s <process_list> to skip the broken "
306
                    f"plugin."
307
                )
308
                raise Exception(f"Incompatible process list. {message}")
309
310
    def _mutate_plugins(self, name, pos, search=False):
311
        """ Perform plugin mutations. """
312
        # check for case changes in plugin name
313
        if search:
314
            for key in pu.plugins.keys():
315
                if name.lower() == key.lower():
316
                    str_pos = self.plugin_list.plugin_list[pos]["pos"]
317
                    self.refresh(str_pos, change=key)
318
                    return True
319
320
        # check mutations dict
321
        m_dict = self.plugin_mutations
322
        if name in m_dict.keys():
323
            mutate = m_dict[name]
324
            if "replace" in mutate.keys():
325
                if mutate["replace"] in list(pu.plugins.keys()):
326
                    str_pos = self.plugin_list.plugin_list[pos]["pos"]
327
                    self.refresh(str_pos, change=mutate["replace"])
328
                    print(mutate["desc"])
329
                    return True
330
                raise Exception(
331
                    f"Replacement plugin {mutate['replace']} "
332
                    f"unavailable for {name}"
333
                )
334
            elif "remove" in mutate.keys():
335
                self.remove(pos)
336
                print(mutate["desc"])
337
            else:
338
                raise Exception("Unknown mutation type.")
339
        return False
340
341
    def move(self, old, new):
342
        old_pos = self.find_position(old)
343
        entry = self.plugin_list.plugin_list[old_pos]
344
        self.remove(old_pos)
345
        new_pos, new = self.convert_pos(new)
346
        name = entry["name"]
347
        self.insert(pu.plugins[name](), new_pos, new)
348
        self.plugin_list.plugin_list[new_pos] = entry
349
        self.plugin_list.plugin_list[new_pos]["pos"] = new
350
        self.check_iterative_loops([old_pos + 1, new_pos + 1], 0)
351
352
    def modify(self, pos_str, param_name, value, default=False, ref=False,
353
               dim=False):
354
        """Modify the plugin at pos_str and the parameter at param_name
355
        The new value will be set if it is valid.
356
357
        :param pos_str: The plugin position
358
        :param param_name: The parameter position/name
359
        :param value: The new parameter value
360
        :param default: True if value should be reverted to the default
361
        :param ref: boolean Refresh the plugin
362
        :param dim: The dimension to be modified
363
364
        returns: A boolean True if the value is a valid input for the
365
          selected parameter
366
        """
367
        pos = self.find_position(pos_str)
368
        plugin_entry = self.plugin_list.plugin_list[pos]
369
        tools = plugin_entry["tools"]
370
        parameters = plugin_entry["data"]
371
        params = plugin_entry["param"]
372
        param_name, value = self.setup_modify(params, param_name, value, ref)
373
        default_str = ["-d", "--default"]
374
        if default or value in default_str:
375
            value = tools.get_default_value(param_name)
376
            self._change_value(param_name, value, tools, parameters)
377
            valid_modification = True
378
        else:
379
            value = self._catch_parameter_tuning_syntax(value, param_name)
380
            valid_modification = self.modify_main(
381
                param_name, value, tools, parameters, dim
382
            )
383
        return valid_modification
384
385
    def _catch_parameter_tuning_syntax(self, value, param_name):
386
        """Check if the new parameter value seems like it is written
387
        in parameter tuning syntax with colons. If it is, then append
388
        a semi colon onto the end.
389
390
        :param value: new parameter value
391
        :param param_name:
392
        :return: modified value with semi colon appended
393
        """
394
        exempt_parameters = ["preview", "indices"]
395
        if self._is_multi_parameter_syntax(value) \
396
                and param_name not in exempt_parameters:
397
            # Assume parameter tuning syntax is being used
398
            value = f"{value};"
399
            print("Parameter tuning syntax applied")
400
        return value
401
402
    def _is_multi_parameter_syntax(self, value):
403
        """If the value contains two colons, is not a dictionary,
404
        and doesnt already contain a semi colon, then assume that
405
        it is using parameter tuning syntax
406
407
        :param value: new parameter value
408
        :return boolean True if parameter tuning syntax is being used
409
        """
410
        isdict = re.findall(r"[\{\}]+", str(value))
411
        return (isinstance(value, str)
412
                and value.count(":") >= 2
413
                and not isdict
414
                and ";" not in value)
415
416
    def setup_modify(self, params, param_name, value, ref):
417
        """Get the parameter keys in the correct order and find
418
        the parameter name string
419
420
        :param params: The plugin parameters
421
        :param param_name: The parameter position/name
422
        :param value: The new parameter value
423
        :param ref: boolean Refresh the plugin
424
425
        return: param_name str to avoid discrepancy, value
426
        """
427
        if ref:
428
            # For a refresh, refresh all keys, including those with
429
            # dependencies (which have the display off)
430
            keys = params.keys()
431
        else:
432
            # Select the correct group and order of parameters according to that
433
            # on display to the user. This ensures correct parameter is modified.
434
            keys = pu.set_order_by_visibility(params)
435
            value = self.value(value)
436
        param_name = pu.param_to_str(param_name, keys)
437
        return param_name, value
438
439
    def modify_main(self, param_name, value, tools, parameters, dim):
440
        """Check the parameter is within the current parameter list.
441
        Check the new parameter value is valid, modify the parameter
442
        value, update defaults, check if dependent parameters should
443
        be made visible or hidden.
444
445
        :param param_name: The parameter position/name
446
        :param value: The new parameter value
447
        :param tools: The plugin tools
448
        :param parameters: The plugin parameters
449
        :param dim: The dimensions
450
451
        returns: A boolean True if the value is a valid input for the
452
          selected parameter
453
        """
454
        parameter_valid = False
455
        current_parameter_details = tools.param.get(param_name)
456
457
        # If dimensions are provided then alter preview param
458
        if self.preview_dimension_to_modify(dim, param_name):
459
            # Filter the dimension, dim1 or dim1.start
460
            dim, _slice = self._separate_dimension_and_slice(dim)
461
            value = self.modify_preview(
462
                parameters, param_name, value, dim, _slice
463
            )
464
465
        # If found, then the parameter is within the current parameter list
466
        # displayed to the user
467
        if current_parameter_details:
468
            value_check = pu._dumps(value)
469
            parameter_valid, error_str = param_u.is_valid(
470
                param_name, value_check, current_parameter_details
471
            )
472
            if parameter_valid:
473
                self._change_value(param_name, value, tools, parameters)
474
            else:
475
                value = str(value)
476
                display_value = f"{value[0:12]}.." if len(value) > 12 \
477
                                else value
478
                print(f"ERROR: The input value {display_value} "
479
                      f"for {param_name} is not correct.")
480
                print(error_str)
481
        else:
482
            print("Not in parameter keys.")
483
        return parameter_valid
484
485
    def _change_value(self, param_name, value, tools, parameters):
486
        """ Change the parameter "param_name" value inside the parameters list
487
        Update feedback messages for various dependant parameters
488
489
        :param param_name: The parameter position/name
490
        :param value: The new parameter value
491
        :param tools: The plugin tools
492
        :param parameters: The plugin parameters
493
        """
494
        # Save the value
495
        parameters[param_name] = value
496
        tools.warn_dependents(param_name, value)
497
        # Update the list of parameters to hide those dependent on others
498
        tools.check_dependencies(parameters)
499
500
    def check_required_args(self, value, required):
501
        """Check required argument 'value' is present
502
503
        :param value: Argument value
504
        :param required: bool, True if the argument is required
505
        """
506
        if required and (not value):
507
            raise Exception("Please enter a value")
508
509
        if (not required) and value:
510
            raise Exception(f"Unrecognised argument: {value}")
511
512
    def preview_dimension_to_modify(self, dim, param_name):
513
        """Check that the dimension string is only present when the parameter
514
        to modify is the preview parameter
515
516
        :param dim: Dimension string
517
        :param param_name: The parameter name (of the parameter to be modified)
518
        :return: True if dimension string is provided and the parameter to modify
519
        the preview parameter
520
        """
521
        if dim:
522
            if param_name == "preview":
523
                return True
524
            else:
525
                raise Exception(
526
                    "Please only use the dimension syntax when "
527
                    "modifying the preview parameter."
528
                )
529
        return False
530
531
    def modify_dimensions(self, pos_str, dim, check="y"):
532
        """Modify the plugin preview value. Remove or add dimensions
533
        to the preview parameter until the provided dimension number
534
        is reached.
535
536
        :param pos_str: The plugin position
537
        :param dim: The new number of dimensions
538
        :return True if preview is modified
539
        """
540
        pos = self.find_position(pos_str)
541
        plugin_entry = self.plugin_list.plugin_list[pos]
542
        parameters = plugin_entry["data"]
543
        self.check_param_exists(parameters, "preview")
544
        current_prev_list = pu._dumps(parameters["preview"])
545
        if not isinstance(current_prev_list, list):
546
            # Temporarily cover dict instance for preview
547
            print("This command is only possible for preview "
548
                  "values of the type list")
549
            return False
550
        pu.check_valid_dimension(dim, [])
551
        if check.lower() == "y":
552
            while len(current_prev_list) > dim:
553
                current_prev_list.pop()
554
            while len(current_prev_list) < dim:
555
                current_prev_list.append(":")
556
            parameters["preview"] = current_prev_list
557
            return True
558
        return False
559
560
    def check_param_exists(self, parameters, pname):
561
        """Check the parameter is present in the current parameter list
562
563
        :param parameters: Dictionary of parameters
564
        :param pname: Parameter name
565
        :return:
566
        """
567
        if not parameters.get(pname):
568
            raise Exception(
569
                f"The {pname} parameter is not available" f" for this plugin."
570
            )
571
572
573
    def plugin_to_num(self, plugin_val, pl_index):
574
        """Check the plugin is within the process list and
575
        return the number in the list.
576
577
        :param plugin_val: The dictionary of parameters
578
        :param pl_index: The plugin index (for use when there are multiple
579
           plugins of same name)
580
        :return: A plugin index number of a certain plugin in the process list
581
        """
582
        if plugin_val.isdigit():
583
            return plugin_val
584
        pl_names = [pl["name"] for pl in self.plugin_list.plugin_list]
585
        if plugin_val in pl_names:
586
            # Find the plugin number
587
            pl_indexes = [i for i, p in enumerate(pl_names) if p == plugin_val]
588
            # Subtract one to access correct list index. Add one to access
589
            # correct plugin position
590
            return str(pl_indexes[pl_index-1] +1)
591
        else:
592
            raise Exception("This plugin is not present in this process list.")
593
594
595
    def value(self, value):
596
        if not value.count(";"):
597
            try:
598
                value = eval(value)
599
            except (NameError, SyntaxError):
600
                try:
601
                    value = eval(f"'{value}'")
602
                    # if there is one quotation mark there will be an error
603
                except EOFError:
604
                    raise EOFError(
605
                        "There is an end of line error. Please check your"
606
                        ' input for the character "\'".'
607
                    )
608
                except SyntaxError:
609
                    raise SyntaxError(
610
                        "There is a syntax error. Please check your input."
611
                    )
612
                except:
613
                    raise Exception("Please check your input.")
614
        return value
615
616
    def convert_to_ascii(self, value):
617
        ascii_list = []
618
        for v in value:
619
            ascii_list.append(v.encode("ascii", "ignore"))
620
        return ascii_list
621
622
    def on_and_off(self, str_pos, index):
623
        print(("switching plugin %s %s" % (str_pos, index)))
624
        status = True if index == "ON" else False
625
        pos = self.find_position(str_pos)
626
        self.plugin_list.plugin_list[pos]["active"] = status
627
628
    def convert_pos(self, str_pos):
629
        """ Converts the display position (input) to the equivalent numerical
630
        position and updates the display position if required.
631
632
        :param str_pos: the plugin display position (input) string.
633
        :returns: the equivalent numerical position of str_pos and and updated\
634
            str_pos.
635
        :rtype: (pos, str_pos)
636
        """
637
        pos_list = self.get_split_positions()
638
        num = re.findall(r"\d+", str_pos)[0]
639
        letter = re.findall("[a-z]", str_pos)
640
        entry = [num, letter[0]] if letter else [num]
641
642
        # full value already exists in the list
643
        if entry in pos_list:
644
            index = pos_list.index(entry)
645
            return self.inc_positions(index, pos_list, entry, 1)
646
647
        # only the number exists in the list
648
        num_list = [pos_list[i][0] for i in range(len(pos_list))]
649
        if entry[0] in num_list:
650
            start = num_list.index(entry[0])
651
            if len(entry) == 2:
652
                if len(pos_list[start]) == 2:
653
                    idx = int([i for i in range(len(num_list)) if
654
                               (num_list[i] == entry[0])][-1]) + 1
655
                    entry = [entry[0], str(chr(ord(pos_list[idx - 1][1]) + 1))]
656
                    return idx, ''.join(entry)
657
                if entry[1] == 'a':
658
                    self.plugin_list.plugin_list[start]['pos'] = entry[0] + 'b'
659
                    return start, ''.join(entry)
660
                else:
661
                    self.plugin_list.plugin_list[start]['pos'] = entry[0] + 'a'
662
                    return start + 1, entry[0] + 'b'
663
            return self.inc_positions(start, pos_list, entry, 1)
664
665
        # number not in list
666
        entry[0] = str(int(num_list[-1]) + 1 if num_list else 1)
667
        if len(entry) == 2:
668
            entry[1] = "a"
669
        return len(self.plugin_list.plugin_list), "".join(entry)
670
671
    def increment_positions(self):
672
        """Update old process lists that start plugin numbering from 0 to
673
        start from 1."""
674
        for plugin in self.plugin_list.plugin_list:
675
            str_pos = plugin["pos"]
676
            num = str(int(re.findall(r"\d+", str_pos)[0]) + 1)
677
            letter = re.findall("[a-z]", str_pos)
678
            plugin["pos"] = "".join([num, letter[0]] if letter else [num])
679
680
    def get_positions(self):
681
        """ Get a list of all current plugin entry positions. """
682
        elems = self.plugin_list.plugin_list
683
        pos_list = []
684
        for e in elems:
685
            pos_list.append(e["pos"])
686
        return pos_list
687
688
    def get_param_arg_str(self, pos_str, subelem):
689
        """Get the name of the parameter so that the display lists the
690
        correct item when the parameter order has been updated
691
692
        :param pos_str: The plugin position
693
        :param subelem: The parameter
694
        :return: The plugin.parameter_name argument
695
        """
696
        pos = self.find_position(pos_str)
697
        current_parameter_list = self.plugin_list.plugin_list[pos]["param"]
698
        current_parameter_list_ordered = pu.set_order_by_visibility(
699
            current_parameter_list
700
        )
701
        param_name = pu.param_to_str(subelem, current_parameter_list_ordered)
702
        param_argument = pos_str + "." + param_name
703
        return param_argument
704
705
    def get_split_positions(self):
706
        """ Separate numbers and letters in positions. """
707
        positions = self.get_positions()
708
        split_pos = []
709
        for i in range(len(positions)):
710
            num = re.findall(r"\d+", positions[i])[0]
711
            letter = re.findall("[a-z]", positions[i])
712
            split_pos.append([num, letter[0]] if letter else [num])
713
        return split_pos
714
715
    def find_position(self, pos):
716
        """ Find the numerical index of a position (a string). """
717
        pos_list = self.get_positions()
718
        if not pos_list:
719
            print("There are no items to access in your list.")
720
            raise Exception("Please add an item to the process list.")
721
        else:
722
            if pos not in pos_list:
723
                raise ValueError("Incorrect plugin position.")
724
            return pos_list.index(pos)
725
726
    def inc_positions(self, start, pos_list, entry, inc):
727
        if len(entry) == 1:
728
            self.inc_numbers(start, pos_list, inc)
729
        else:
730
            idx = [
731
                i
732
                for i in range(start, len(pos_list))
733
                if pos_list[i][0] == entry[0]
734
            ]
735
            self.inc_letters(idx, pos_list, inc)
736
        return start, "".join(entry)
737
738
    def inc_numbers(self, start, pos_list, inc):
739
        for i in range(start, len(pos_list)):
740
            pos_list[i][0] = str(int(pos_list[i][0]) + inc)
741
            self.plugin_list.plugin_list[i]["pos"] = "".join(pos_list[i])
742
743
    def inc_letters(self, idx, pos_list, inc):
744
        for i in idx:
745
            pos_list[i][1] = str(chr(ord(pos_list[i][1]) + inc))
746
            self.plugin_list.plugin_list[i]["pos"] = "".join(pos_list[i])
747
748
    def split_plugin_string(self, start, stop, subelem_view=False):
749
        """Find the start and stop number for the plugin range selected.
750
751
        :param start: Plugin starting index (including a subelem value
752
          if permitted)
753
        :param stop: Plugin stopping index
754
        :param subelem_view: False if subelem value not permitted
755
        :return: range_dict containing start stop (and possible subelem)
756
        """
757
        range_dict = {}
758
        if start:
759
            if subelem_view and "." in start:
760
                start, stop, subelem = self._split_subelem(start)
761
                range_dict["subelem"] = subelem
762
            else:
763
                start, stop = self._get_start_stop(start, stop)
764
            range_dict["start"] = start
765
            range_dict["stop"] = stop
766
        return range_dict
767
768
    def _get_start_stop(self, start, stop):
769
        """Find the start and stop number for the plugin range selected """
770
        start = self.find_position(start)
771
        stop = self.find_position(stop) + 1 if stop else start + 1
772
        return start, stop
773
774
    def _split_subelem(self, start, config_disp=True):
775
        """Separate the start string containing the plugin number,
776
        parameter(subelement), dimension and command
777
778
        :param start: The plugin to start at
779
        :param config_disp: True if command and dimension arguments
780
          are not permitted
781
        :return: start plugin, range_dict containing a subelem
782
            if a parameter is specified
783
        """
784
        start, subelem, dim, command = self.separate_plugin_subelem(
785
            start, config_disp
786
        )
787
        start, stop = self._get_start_stop(start, "")
788
        return start, stop, subelem
789
790
    def _check_command_valid(self, plugin_param, config_disp):
791
        """Check the plugin_param string length
792
793
        :param plugin_param: The string containing plugin number, parameter,
794
         and command
795
        :param config_disp: bool, True if command and dimension arguments are
796
          not permitted
797
        """
798
        if config_disp:
799
            if not 1 < len(plugin_param) < 3:
800
                raise ValueError(
801
                    "Use either 'plugin_pos.param_name' or"
802
                    " 'plugin_pos.param_no'"
803
                )
804
        else:
805
            # The modify command is being used
806
            if len(plugin_param) <= 1:
807
                raise ValueError(
808
                    "Please enter the plugin parameter to modify"
809
                    ". Either 'plugin_pos.param_name' or"
810
                    " 'plugin_pos.param_no'"
811
                )
812
            if not 1 < len(plugin_param) < 5:
813
                raise ValueError(
814
                    "Enter 'plugin_pos.param_no.dimension'. "
815
                    "Following the dimension, use start/stop/step"
816
                    " eg. '1.1.dim1.start' "
817
                )
818
819
    def separate_plugin_subelem(self, plugin_param, config_disp):
820
        """Separate the plugin number,parameter (subelement) number
821
        and additional command if present.
822
823
        :param plugin_param: A string supplied by the user input which
824
         contains the plugin element to edit/display. eg "1.1.dim.command"
825
        :param config_disp: bool, True if command and dimension arguments are
826
          not permitted
827
828
        :returns plugin: The number of the plugin element
829
                 subelem: The number of the parameter
830
                 dim: The dimension
831
                 command: The supplied command, eg expand or a dimension
832
                          string
833
        """
834
        plugin_param = plugin_param.split(".")
835
        plugin = plugin_param[0]
836
        # change str plugin name to a number
837
        start = self.find_position(plugin)
838
        self._check_command_valid(plugin_param, config_disp)
839
        subelem = plugin_param[1]
840
        if len(plugin_param) > 2:
841
            dim = self.dim_str_to_int(plugin_param[2])
842
            command = str(dim)
843
            if len(plugin_param) == 4:
844
                self._check_command_str(plugin_param[3])
845
                command += "." + plugin_param[3]
846
        else:
847
            dim, command = "", ""
848
        return plugin, subelem, dim, command
849
850
    def _check_command_str(self, command_str):
851
        """Check the additional 1.1.dim.command for slice or 'expand'
852
        keywords
853
        """
854
        command_list = ["expand", "start", "stop", "step", "chunk"]
855
        if command_str not in command_list:
856
            raise ValueError(
857
                "Following the dimension, use start/stop/step eg.  "
858
                "'1.1.dim1.start' "
859
            )
860
        return command_str
861
862
    def insert(self, plugin, pos, str_pos, replace=False):
863
        plugin_dict = self.create_plugin_dict(plugin)
864
        plugin_dict["pos"] = str_pos
865
        if replace:
866
            self.plugin_list.plugin_list[pos] = plugin_dict
867
        else:
868
            self.plugin_list.plugin_list.insert(pos, plugin_dict)
869
870
    def create_plugin_dict(self, plugin):
871
        tools = plugin.get_plugin_tools()
872
        plugin_dict = {}
873
        plugin_dict["name"] = plugin.name
874
        plugin_dict["id"] = plugin.__module__
875
        plugin_dict["data"] = plugin.parameters
876
        plugin_dict["active"] = True
877
        plugin_dict["iterative"] = False
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()