Test Failed
Push — master ( 9ba79c...17f3e3 )
by Yousef
01:54 queued 19s
created

Content.inc_letters()   A

Complexity

Conditions 2

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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