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