Test Failed
Pull Request — master (#892)
by
unknown
04:01
created

Content.plugin_to_num()   A

Complexity

Conditions 3

Size

Total Lines 20
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nop 3
dl 0
loc 20
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
442
        # If dimensions are provided then alter preview param
443
        if self.preview_dimension_to_modify(dim, param_name):
444
            # Filter the dimension, dim1 or dim1.start
445
            dim, _slice = self._separate_dimension_and_slice(dim)
446
            value = self.modify_preview(
447
                parameters, param_name, value, dim, _slice
448
            )
449
450
        # If found, then the parameter is within the current parameter list
451
        # displayed to the user
452
        if current_parameter_details:
453
            value_check = pu._dumps(value)
454
            parameter_valid, error_str = param_u.is_valid(
455
                param_name, value_check, current_parameter_details
456
            )
457
            if parameter_valid:
458
                self._change_value(param_name, value, tools, parameters)
459
            else:
460
                value = str(value)
461
                display_value = f"{value[0:12]}.." if len(value) > 12 \
462
                                else value
463
                print(f"ERROR: The input value {display_value} "
464
                      f"for {param_name} is not correct.")
465
                print(error_str)
466
        else:
467
            print("Not in parameter keys.")
468
        return parameter_valid
469
470
    def _change_value(self, param_name, value, tools, parameters):
471
        """ Change the parameter "param_name" value inside the parameters list
472
        Update feedback messages for various dependant parameters
473
474
        :param param_name: The parameter position/name
475
        :param value: The new parameter value
476
        :param tools: The plugin tools
477
        :param parameters: The plugin parameters
478
        """
479
        # Save the value
480
        parameters[param_name] = value
481
        tools.warn_dependents(param_name, value)
482
        # Update the list of parameters to hide those dependent on others
483
        tools.check_dependencies(parameters)
484
485
    def check_required_args(self, value, required):
486
        """Check required argument 'value' is present
487
488
        :param value: Argument value
489
        :param required: bool, True if the argument is required
490
        """
491
        if required and (not value):
492
            raise Exception("Please enter a value")
493
494
        if (not required) and value:
495
            raise Exception(f"Unrecognised argument: {value}")
496
497
    def preview_dimension_to_modify(self, dim, param_name):
498
        """Check that the dimension string is only present when the parameter
499
        to modify is the preview parameter
500
501
        :param dim: Dimension string
502
        :param param_name: The parameter name (of the parameter to be modified)
503
        :return: True if dimension string is provided and the parameter to modify
504
        the preview parameter
505
        """
506
        if dim:
507
            if param_name == "preview":
508
                return True
509
            else:
510
                raise Exception(
511
                    "Please only use the dimension syntax when "
512
                    "modifying the preview parameter."
513
                )
514
        return False
515
516
    def modify_dimensions(self, pos_str, dim, check="y"):
517
        """Modify the plugin preview value. Remove or add dimensions
518
        to the preview parameter until the provided dimension number
519
        is reached.
520
521
        :param pos_str: The plugin position
522
        :param dim: The new number of dimensions
523
        :return True if preview is modified
524
        """
525
        pos = self.find_position(pos_str)
526
        plugin_entry = self.plugin_list.plugin_list[pos]
527
        parameters = plugin_entry["data"]
528
        self.check_param_exists(parameters, "preview")
529
        current_prev_list = pu._dumps(parameters["preview"])
530
        if not isinstance(current_prev_list, list):
531
            # Temporarily cover dict instance for preview
532
            print("This command is only possible for preview "
533
                  "values of the type list")
534
            return False
535
        pu.check_valid_dimension(dim, [])
536
        if check.lower() == "y":
537
            while len(current_prev_list) > dim:
538
                current_prev_list.pop()
539
            while len(current_prev_list) < dim:
540
                current_prev_list.append(":")
541
            parameters["preview"] = current_prev_list
542
            return True
543
        return False
544
545
    def check_param_exists(self, parameters, pname):
546
        """Check the parameter is present in the current parameter list
547
548
        :param parameters: Dictionary of parameters
549
        :param pname: Parameter name
550
        :return:
551
        """
552
        if not parameters.get(pname):
553
            raise Exception(
554
                f"The {pname} parameter is not available" f" for this plugin."
555
            )
556
557
558
    def plugin_to_num(self, plugin_val, pl_index):
559
        """Check the plugin is within the process list and
560
        return the number in the list.
561
562
        :param plugin_val: The dictionary of parameters
563
        :param pl_index: The plugin index (for use when there are multiple
564
           plugins of same name)
565
        :return: A plugin index number of a certain plugin in the process list
566
        """
567
        if plugin_val.isdigit():
568
            return plugin_val
569
        pl_names = [pl["name"] for pl in self.plugin_list.plugin_list]
570
        if plugin_val in pl_names:
571
            # Find the plugin number
572
            pl_indexes = [i for i, p in enumerate(pl_names) if p == plugin_val]
573
            # Subtract one to access correct list index. Add one to access
574
            # correct plugin position
575
            return str(pl_indexes[pl_index-1] +1)
576
        else:
577
            raise Exception("This plugin is not present in this process list.")
578
579
580
    def value(self, value):
581
        if not value.count(";"):
582
            try:
583
                value = eval(value)
584
            except (NameError, SyntaxError):
585
                try:
586
                    value = eval(f"'{value}'")
587
                    # if there is one quotation mark there will be an error
588
                except EOFError:
589
                    raise EOFError(
590
                        "There is an end of line error. Please check your"
591
                        ' input for the character "\'".'
592
                    )
593
                except SyntaxError:
594
                    raise SyntaxError(
595
                        "There is a syntax error. Please check your input."
596
                    )
597
                except:
598
                    raise Exception("Please check your input.")
599
        return value
600
601
    def convert_to_ascii(self, value):
602
        ascii_list = []
603
        for v in value:
604
            ascii_list.append(v.encode("ascii", "ignore"))
605
        return ascii_list
606
607
    def on_and_off(self, str_pos, index):
608
        print(("switching plugin %s %s" % (str_pos, index)))
609
        status = True if index == "ON" else False
610
        pos = self.find_position(str_pos)
611
        self.plugin_list.plugin_list[pos]["active"] = status
612
613
    def convert_pos(self, str_pos):
614
        """ Converts the display position (input) to the equivalent numerical
615
        position and updates the display position if required.
616
617
        :param str_pos: the plugin display position (input) string.
618
        :returns: the equivalent numerical position of str_pos and and updated\
619
            str_pos.
620
        :rtype: (pos, str_pos)
621
        """
622
        pos_list = self.get_split_positions()
623
        num = re.findall(r"\d+", str_pos)[0]
624
        letter = re.findall("[a-z]", str_pos)
625
        entry = [num, letter[0]] if letter else [num]
626
627
        # full value already exists in the list
628
        if entry in pos_list:
629
            index = pos_list.index(entry)
630
            return self.inc_positions(index, pos_list, entry, 1)
631
632
        # only the number exists in the list
633
        num_list = [pos_list[i][0] for i in range(len(pos_list))]
634
        if entry[0] in num_list:
635
            start = num_list.index(entry[0])
636
            if len(entry) == 2:
637
                if len(pos_list[start]) == 2:
638
                    idx = int([i for i in range(len(num_list)) if
639
                               (num_list[i] == entry[0])][-1]) + 1
640
                    entry = [entry[0], str(chr(ord(pos_list[idx - 1][1]) + 1))]
641
                    return idx, ''.join(entry)
642
                if entry[1] == 'a':
643
                    self.plugin_list.plugin_list[start]['pos'] = entry[0] + 'b'
644
                    return start, ''.join(entry)
645
                else:
646
                    self.plugin_list.plugin_list[start]['pos'] = entry[0] + 'a'
647
                    return start + 1, entry[0] + 'b'
648
            return self.inc_positions(start, pos_list, entry, 1)
649
650
        # number not in list
651
        entry[0] = str(int(num_list[-1]) + 1 if num_list else 1)
652
        if len(entry) == 2:
653
            entry[1] = "a"
654
        return len(self.plugin_list.plugin_list), "".join(entry)
655
656
    def increment_positions(self):
657
        """Update old process lists that start plugin numbering from 0 to
658
        start from 1."""
659
        for plugin in self.plugin_list.plugin_list:
660
            str_pos = plugin["pos"]
661
            num = str(int(re.findall(r"\d+", str_pos)[0]) + 1)
662
            letter = re.findall("[a-z]", str_pos)
663
            plugin["pos"] = "".join([num, letter[0]] if letter else [num])
664
665
    def get_positions(self):
666
        """ Get a list of all current plugin entry positions. """
667
        elems = self.plugin_list.plugin_list
668
        pos_list = []
669
        for e in elems:
670
            pos_list.append(e["pos"])
671
        return pos_list
672
673
    def get_param_arg_str(self, pos_str, subelem):
674
        """Get the name of the parameter so that the display lists the
675
        correct item when the parameter order has been updated
676
677
        :param pos_str: The plugin position
678
        :param subelem: The parameter
679
        :return: The plugin.parameter_name argument
680
        """
681
        pos = self.find_position(pos_str)
682
        current_parameter_list = self.plugin_list.plugin_list[pos]["param"]
683
        current_parameter_list_ordered = pu.set_order_by_visibility(
684
            current_parameter_list
685
        )
686
        param_name = pu.param_to_str(subelem, current_parameter_list_ordered)
687
        param_argument = pos_str + "." + param_name
688
        return param_argument
689
690
    def get_split_positions(self):
691
        """ Separate numbers and letters in positions. """
692
        positions = self.get_positions()
693
        split_pos = []
694
        for i in range(len(positions)):
695
            num = re.findall(r"\d+", positions[i])[0]
696
            letter = re.findall("[a-z]", positions[i])
697
            split_pos.append([num, letter[0]] if letter else [num])
698
        return split_pos
699
700
    def find_position(self, pos):
701
        """ Find the numerical index of a position (a string). """
702
        pos_list = self.get_positions()
703
        if not pos_list:
704
            print("There are no items to access in your list.")
705
            raise Exception("Please add an item to the process list.")
706
        else:
707
            if pos not in pos_list:
708
                raise ValueError("Incorrect plugin position.")
709
            return pos_list.index(pos)
710
711
    def inc_positions(self, start, pos_list, entry, inc):
712
        if len(entry) == 1:
713
            self.inc_numbers(start, pos_list, inc)
714
        else:
715
            idx = [
716
                i
717
                for i in range(start, len(pos_list))
718
                if pos_list[i][0] == entry[0]
719
            ]
720
            self.inc_letters(idx, pos_list, inc)
721
        return start, "".join(entry)
722
723
    def inc_numbers(self, start, pos_list, inc):
724
        for i in range(start, len(pos_list)):
725
            pos_list[i][0] = str(int(pos_list[i][0]) + inc)
726
            self.plugin_list.plugin_list[i]["pos"] = "".join(pos_list[i])
727
728
    def inc_letters(self, idx, pos_list, inc):
729
        for i in idx:
730
            pos_list[i][1] = str(chr(ord(pos_list[i][1]) + inc))
731
            self.plugin_list.plugin_list[i]["pos"] = "".join(pos_list[i])
732
733
    def split_plugin_string(self, start, stop, subelem_view=False):
734
        """Find the start and stop number for the plugin range selected.
735
736
        :param start: Plugin starting index (including a subelem value
737
          if permitted)
738
        :param stop: Plugin stopping index
739
        :param subelem_view: False if subelem value not permitted
740
        :return: range_dict containing start stop (and possible subelem)
741
        """
742
        range_dict = {}
743
        if start:
744
            if subelem_view and "." in start:
745
                start, stop, subelem = self._split_subelem(start)
746
                range_dict["subelem"] = subelem
747
            else:
748
                start, stop = self._get_start_stop(start, stop)
749
            range_dict["start"] = start
750
            range_dict["stop"] = stop
751
        return range_dict
752
753
    def _get_start_stop(self, start, stop):
754
        """Find the start and stop number for the plugin range selected """
755
        start = self.find_position(start)
756
        stop = self.find_position(stop) + 1 if stop else start + 1
757
        return start, stop
758
759
    def _split_subelem(self, start, config_disp=True):
760
        """Separate the start string containing the plugin number,
761
        parameter(subelement), dimension and command
762
763
        :param start: The plugin to start at
764
        :param config_disp: True if command and dimension arguments
765
          are not permitted
766
        :return: start plugin, range_dict containing a subelem
767
            if a parameter is specified
768
        """
769
        start, subelem, dim, command = self.separate_plugin_subelem(
770
            start, config_disp
771
        )
772
        start, stop = self._get_start_stop(start, "")
773
        return start, stop, subelem
774
775
    def _check_command_valid(self, plugin_param, config_disp):
776
        """Check the plugin_param string length
777
778
        :param plugin_param: The string containing plugin number, parameter,
779
         and command
780
        :param config_disp: bool, True if command and dimension arguments are
781
          not permitted
782
        """
783
        if config_disp:
784
            if not 1 < len(plugin_param) < 3:
785
                raise ValueError(
786
                    "Use either 'plugin_pos.param_name' or"
787
                    " 'plugin_pos.param_no'"
788
                )
789
        else:
790
            # The modify command is being used
791
            if len(plugin_param) <= 1:
792
                raise ValueError(
793
                    "Please enter the plugin parameter to modify"
794
                    ". Either 'plugin_pos.param_name' or"
795
                    " 'plugin_pos.param_no'"
796
                )
797
            if not 1 < len(plugin_param) < 5:
798
                raise ValueError(
799
                    "Enter 'plugin_pos.param_no.dimension'. "
800
                    "Following the dimension, use start/stop/step"
801
                    " eg. '1.1.dim1.start' "
802
                )
803
804
    def separate_plugin_subelem(self, plugin_param, config_disp):
805
        """Separate the plugin number,parameter (subelement) number
806
        and additional command if present.
807
808
        :param plugin_param: A string supplied by the user input which
809
         contains the plugin element to edit/display. eg "1.1.dim.command"
810
        :param config_disp: bool, True if command and dimension arguments are
811
          not permitted
812
813
        :returns plugin: The number of the plugin element
814
                 subelem: The number of the parameter
815
                 dim: The dimension
816
                 command: The supplied command, eg expand or a dimension
817
                          string
818
        """
819
        plugin_param = plugin_param.split(".")
820
        plugin = plugin_param[0]
821
        # change str plugin name to a number
822
        start = self.find_position(plugin)
823
        self._check_command_valid(plugin_param, config_disp)
824
        subelem = plugin_param[1]
825
        if len(plugin_param) > 2:
826
            dim = self.dim_str_to_int(plugin_param[2])
827
            command = str(dim)
828
            if len(plugin_param) == 4:
829
                self._check_command_str(plugin_param[3])
830
                command += "." + plugin_param[3]
831
        else:
832
            dim, command = "", ""
833
        return plugin, subelem, dim, command
834
835
    def _check_command_str(self, command_str):
836
        """Check the additional 1.1.dim.command for slice or 'expand'
837
        keywords
838
        """
839
        command_list = ["expand", "start", "stop", "step", "chunk"]
840
        if command_str not in command_list:
841
            raise ValueError(
842
                "Following the dimension, use start/stop/step eg.  "
843
                "'1.1.dim1.start' "
844
            )
845
        return command_str
846
847
    def insert(self, plugin, pos, str_pos, replace=False):
848
        plugin_dict = self.create_plugin_dict(plugin)
849
        plugin_dict["pos"] = str_pos
850
        if replace:
851
            self.plugin_list.plugin_list[pos] = plugin_dict
852
        else:
853
            self.plugin_list.plugin_list.insert(pos, plugin_dict)
854
855
    def create_plugin_dict(self, plugin):
856
        tools = plugin.get_plugin_tools()
857
        plugin_dict = {}
858
        plugin_dict["name"] = plugin.name
859
        plugin_dict["id"] = plugin.__module__
860
        plugin_dict["data"] = plugin.parameters
861
        plugin_dict["active"] = True
862
        plugin_dict["tools"] = tools
863
        plugin_dict["param"] = tools.get_param_definitions()
864
        plugin_dict["doc"] = tools.docstring_info
865
        return plugin_dict
866
867
    def get(self, pos):
868
        return self.plugin_list.plugin_list[pos]
869
870
    def _separate_dimension_and_slice(self, command_str):
871
        """Check the start stop step command
872
873
        param command_str: a string '1.1' containing the dimension
874
            and slice number seperated by a full stop
875
876
        :returns dim, slice
877
        """
878
        if isinstance(command_str, str) and "." in command_str:
879
            # If the slice value is included
880
            slice_dict = {"start": 0, "stop": 1, "step": 2, "chunk": 3}
881
            _slice = slice_dict[command_str.split(".")[1]]
882
            dim = int(command_str.split(".")[0])
883
        else:
884
            dim = int(command_str)
885
            _slice = ""
886
        return dim, _slice
887
888
    def dim_str_to_int(self, dim_str):
889
        """Check the additional 1.1.dim keyword
890
891
        :param dim: A string 'dim1' specifying the dimension
892
893
        :return: dim - An integer dimension value
894
        """
895
        number = "".join(l for l in dim_str if l.isdigit())
896
        letters = "".join(l for l in dim_str if l.isalpha())
897
898
        if letters == "dim" and number.strip():
899
            dim = int(number)
900
        else:
901
            raise ValueError(
902
                "Following the second decimal place, please "
903
                "specify a dimension 1.1.dim1 or 1.preview.dim1"
904
            )
905
        return dim
906
907
    def modify_preview(self, parameters, param_name, value, dim, _slice):
908
        """ Check the entered value is valid and edit preview"""
909
        slice_list = [0, 1, 2, 3]
910
        type_check_value = pu._dumps(value)
911
        current_preview_value = pu._dumps(parameters[param_name])
912
        pu.check_valid_dimension(dim, current_preview_value)
913
        if _slice in slice_list:
914
            # Modify this dimension and slice only
915
            if param_u._preview_dimension_singular(type_check_value):
916
                value = self._modify_preview_dimension_slice(
917
                    value, current_preview_value, dim, _slice
918
                )
919
            else:
920
                raise ValueError(
921
                    "Invalid preview dimension slice value. Please "
922
                    "enter a float, an integer or a string including "
923
                    "only start, mid and end keywords."
924
                )
925
        else:
926
            # If the entered value is a valid dimension value
927
            if param_u._preview_dimension(type_check_value):
928
                # Modify the whole dimension
929
                value = self._modify_preview_dimension(
930
                    value, current_preview_value, dim
931
                )
932
            else:
933
                raise ValueError(
934
                    "Invalid preview dimension value. Please "
935
                    "enter a float, an integer or slice notation."
936
                )
937
        return value
938
939
    def _modify_preview_dimension_slice(self, value, current_val, dim, _slice):
940
        """Modify the preview dimension slice value at the dimension (dim)
941
        provided
942
943
        :param value: The new value
944
        :param current_value: The current preview parameter value
945
        :param dim: The dimension to modify
946
        :param _slice: The slice value to modify
947
        :return: The modified value
948
        """
949
        if not current_val:
950
            current_val = self._set_empty_list(
951
                dim, self._set_empty_dimension_slice(value, _slice)
952
            )
953
        else:
954
            current_val[dim - 1] = self._modified_slice_notation(
955
                current_val[dim - 1], value, _slice
956
            )
957
        return current_val
958
959
    def _modified_slice_notation(self, old_value, value, _slice):
960
        """Change the current value at the provided slice
961
962
        :param old_value: Previous slice notation
963
        :param value: New value to set
964
        :param _slice: Slice to modify
965
        :return: Changed value (str/int)
966
        """
967
        old_value = self._set_incomplete_slices(str(old_value), _slice)
968
        if pu.is_slice_notation(old_value):
969
            start_stop_split = old_value.split(":")
970
            return self._get_modified_slice(start_stop_split, value, _slice)
971
        elif _slice == 0:
972
            # If there is no slice notation, only allow first slice to
973
            # be modified
974
            return value
975
        else:
976
            raise Exception(
977
                "There is no existing slice notation to modify."
978
            )
979
980
    def _get_modified_slice(self, start_stop_split, value, _slice):
981
        if all(v == "" for v in start_stop_split):
982
            return self._set_empty_dimension_slice(value, _slice)
983
        else:
984
            start_stop_split[_slice] = str(value)
985
            start_stop_split = ":".join(start_stop_split)
986
            return start_stop_split
987
988
    def _modify_preview_dimension(self, value, current_preview, dim):
989
        """Modify the preview list value at the dimension provided (dim)
990
991
        :param value: The new value
992
        :param current_value: The current preview parameter list value
993
        :param dim: The dimension to modify
994
        :return: The modified value
995
        """
996
        if not current_preview:
997
            return self._set_empty_list(dim, value)
998
        current_preview[dim - 1] = value
999
        # Save the altered preview value
1000
        return current_preview
1001
1002
    def _set_empty_dimension_slice(self, value, _slice):
1003
        """Set the empty dimension and insert colons to indicate
1004
        the correct slice notation.
1005
1006
        :param value: New value to set
1007
        :param _slice: start/stop/step/chunk value
1008
        :return: String for the new value
1009
        """
1010
        return _slice * ":" + str(value)
1011
1012
    def _set_incomplete_slices(self, old_value, _slice):
1013
        """Append default slice values.to the current string value, in order
1014
         to allow later slice values to be set
1015
1016
        :param old_value: Current string value with slice notation
1017
        :param _slice: slice notation index to be edited
1018
        :return: String with default slice notation entries set
1019
        """
1020
        while old_value.count(":") < _slice:
1021
            old_value += ":"
1022
        return old_value
1023
1024
    def _set_empty_list(self, dim, value):
1025
        """If the dimension is 1 then allow the whole empty list
1026
        to be set.
1027
1028
        :param dim: Dimension to be altered
1029
        :param value: New value
1030
        :return: List containing new value
1031
        """
1032
        if dim == 1:
1033
            return [value]
1034
        else:
1035
            raise ValueError("You have not set earlier dimensions")
1036
1037
    def level(self, level):
1038
        """ Set the visibility level of parameters """
1039
        if level:
1040
            self.disp_level = level
1041
            print(f"Level set to '{level}'")
1042
        else:
1043
            print(f"Level is set at '{self.disp_level}'")
1044
1045
    def remove(self, pos):
1046
        if pos >= self.size:
1047
            raise Exception(
1048
                "Cannot remove plugin %s as it does not exist."
1049
                % self.plugin_list.plugin_list[pos]["name"]
1050
            )
1051
        pos_str = self.plugin_list.plugin_list[pos]["pos"]
1052
        self.plugin_list.plugin_list.pop(pos)
1053
        pos_list = self.get_split_positions()
1054
        self.inc_positions(pos, pos_list, pos_str, -1)
1055
1056
    @property
1057
    def size(self):
1058
        return len(self.plugin_list.plugin_list)
1059