Test Failed
Pull Request — master (#848)
by
unknown
03:43 queued 12s
created

Content._check_command_valid()   A

Complexity

Conditions 5

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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