Test Failed
Pull Request — master (#820)
by
unknown
04:20
created

Content.get_split_positions()   A

Complexity

Conditions 3

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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