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