Test Failed
Pull Request — master (#897)
by Daniil
04:04
created

Content._separate_dimension_and_slice()   A

Complexity

Conditions 3

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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