scripts.config_generator.parameter_utils   F
last analyzed

Complexity

Total Complexity 144

Size/Duplication

Total Lines 658
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 390
dl 0
loc 658
rs 2
c 0
b 0
f 0
wmc 144

44 Functions

Rating   Name   Duplication   Size   Complexity  
A _int() 0 5 2
A _preview() 0 6 4
A _float() 0 5 2
A _bool() 0 5 3
A _str() 0 4 2
A _savupath() 0 8 2
A _nptype() 0 5 3
A _filepath() 0 8 3
A _yamlfilepath() 0 11 4
A _h5path() 0 3 1
A _yaml_is_valid() 0 13 5
A _dir() 0 8 3
B is_valid_dtype() 0 16 6
A _dict() 0 3 1
A _typelist() 0 10 3
A _convert_to_list() 0 2 2
B is_valid() 0 37 8
B _check_h5path() 0 27 8
A _gui_error_message() 0 11 2
A _check_dict_entry_dtype() 0 15 2
A _options_list() 0 12 3
A _check_type() 0 22 4
A _None() 0 3 1
A _matched_brackets() 0 16 4
A _list() 0 5 2
B _remove_nested_brackets() 0 8 6
A _preview_eval() 0 8 1
A _is_valid_options_list_type() 0 9 4
A _error_message() 0 13 3
A _is_valid_multi() 0 11 3
A _split_notation_is_valid() 0 16 3
A _check_list_combination() 0 22 5
A _is_valid_list_combination_type() 0 6 3
A _preview_dimension_singular() 0 20 4
A _list_combination() 0 20 2
A is_multi_param() 0 4 1
A _check_dict_combination() 0 17 4
A _is_valid_dict_combination_type() 0 10 4
A _preview_dimension() 0 18 5
A _dict_combination() 0 11 2
A _check_multi_param() 0 18 4
A _find_options() 0 22 4
A _check_options() 0 15 4
A _check_default() 0 8 2

How to fix   Complexity   

Complexity

Complex classes like scripts.config_generator.parameter_utils often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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:: parameter_utils
17
   :platform: Unix
18
   :synopsis: Parameter utilities
19
20
.. moduleauthor:: Jessica Verschoyle,Nicola Wadeson <[email protected]>
21
22
"""
23
24
import os
25
import re
26
import copy
27
import h5py
28
import posixpath
29
import numpy as np
30
import configparser
31
32
from colorama import Fore
33
34
import savu.plugins.loaders.utils.yaml_utils as yu
35
import savu.plugins.utils as pu
36
37
38
def _int(value):
39
    # do not use isinstance as this also picks up boolean values
40
    if type(value) in (int, np.int_):
41
        return True
42
    return False
43
44
45
def _str(value):
46
    if isinstance(value, str):
47
        return True
48
    return False
49
50
51
def _float(value):
52
    valid = isinstance(value, (float, np.float))
53
    if not valid:
54
        valid = _int(value)
55
    return valid
56
57
58
def _bool(value): # should eventually be a drop-down list
59
    valid = isinstance(value, bool)
60
    if not valid and isinstance(value, str):
61
        return value.lower() == "true" or value.lower() == "false"
62
    return valid
63
64
65
def _dir(value):
66
    """ A directory """
67
    valid = False
68
    if _str(value):
69
        valid = os.path.isdir(value)
70
        if not valid:
71
            valid = os.path.isdir(_savupath(value))
72
    return valid
73
74
75
def _filepath(value):
76
    """ file path """
77
    valid = False
78
    if _str(value):
79
        valid = os.path.isfile(value)
80
        if not valid:
81
            valid = os.path.isfile(_savupath(value))
82
    return valid
83
84
85
def _h5path(value): # Extend this later as we need to know which file to apply the check to
86
    """ internal path to a hdf5 dataset """
87
    return _str(value)
88
89
90
def _savupath(value):
91
    """ A path inside the Savu directory"""
92
    savu_base_path = os.path.join(
93
        os.path.dirname(os.path.realpath(__file__)), '../../')
94
    split_path = value.split("Savu/")
95
    if len(split_path) > 1:
96
        value = os.path.join(savu_base_path, split_path[-1][:])
97
    return value
98
99
100
def _yamlfilepath(value):
101
    """ yaml_file """
102
    # does the filepath exist
103
    if _str(value):
104
        if not os.path.isfile(value):
105
            # is it a file path in Savu folder
106
            value = _savupath(value)
107
            if not os.path.isfile(value):
108
                return False
109
        return _yaml_is_valid(value)
110
    return False
111
112
def _yaml_is_valid(filepath):
113
    """Read the yaml file at the provided file path """
114
    with open(filepath, 'r') as f:
115
        errors = yu.check_yaml_errors(f)
116
        try:
117
            yu.read_yaml(filepath)
118
            return True
119
        except:
120
            if errors:
121
                print("There were some errors with your yaml file structure.")
122
                for e in errors:
123
                    print(e)
124
    return False
125
126
def _nptype(value):
127
    """Check if the value is a numpy data type. Return true if it is."""
128
    if _int(value) or _str(value):
129
        return (value in np.typecodes) or (value in np.sctypeDict.keys())
130
    return False
131
132
133
def _preview(value):
134
    """ preview value """
135
    valid = _typelist(_preview_dimension, value)
136
    if not valid and _list(value) and not value:
137
        return True # empty list is allowed
138
    return valid
139
140
141
def _typelist(func, value):
142
    """ Apply the function to each value inside the list <value>.
143
    Return true if all items in the list <value> return true
144
    :param func Name of the function to apply to each item within the list
145
    :param value The input list
146
    """
147
    if isinstance(value, list):
148
        if value:
149
            return all(func(item) for item in value)
150
    return False
151
152
153
def _preview_dimension(value):
154
    """ Check the full preview parameter value """
155
    if _str(value):
156
        slice_str = [":"*n for n in range(1,5)]
157
        if value in slice_str:
158
            # If : notation is used, accept this
159
            valid = True
160
        elif ":" in value:
161
            valid = _split_notation_is_valid(value)
162
        else:
163
            valid = _preview_dimension_singular(value)
164
    else:
165
        valid = _float(value)
166
167
        if not valid:
168
            # Allow the value if it is a list
169
            valid = _typelist(_preview_dimension_singular, value)
170
    return valid
171
172
173
def _split_notation_is_valid(value):
174
    """Check if the start step stock chunk entries are valid
175
176
    :param value: The value to check
177
    :return: parameter_valid True if the split notation is valid
178
    """
179
    if value.count(":") < 4:
180
        # Only allow 4 colons, start stop step block
181
        start_stop_split = value.split(":")
182
        try:
183
            type_list = [pu._dumps(v) for v in start_stop_split if v]
184
            return _typelist(_preview_dimension_singular,
185
                             type_list)
186
        except Exception as e:
187
            print(f"There was an error with your slice notation, '{value}'")
188
    return False
189
190
191
def _preview_dimension_singular(value):
192
    """ Check the singular value within the preview dimension"""
193
    valid = False
194
    if _str(value):
195
        string_valid = re.fullmatch("(start|mid|end|[^a-zA-z])+", value)
196
        # Check that the string does not contain any letters [^a-zA-Z]
197
        # If it does contain letters, start, mid and end are the only keywords allowed
198
        if string_valid:
199
            try:
200
                # Attempt to evaluate the provided equation
201
                temp_value = _preview_eval(value)
202
                valid = _float(temp_value)
203
            except Exception:
204
                print("There was an error with your dimension value input.")
205
        else:
206
            print('If you are trying to use an expression, '
207
                  'please only use start, mid and end command words.')
208
    else:
209
        valid = _float(value)
210
    return valid
211
212
213
def _preview_eval(value):
214
    """ Evaluate with start, mid and end"""
215
    start = 0
216
    mid = 0
217
    end = 0
218
    return eval(value,{"__builtins__":None},{"start":start,
219
                                             "mid":mid,
220
                                             "end":end})
221
222
223
#Replace this with if list combination contains filepath and h5path e.g. list[filepath, h5path, int] then perform this check
224
def _check_h5path(filepath, h5path):
225
    """ Check if the internal path is valid"""
226
    with h5py.File(filepath, "r") as hf:
227
        try:
228
            # Hdf5 dataset object
229
            h5path = hf.get(h5path)
230
            if h5path is None:
231
                print("There is no data stored at that internal path.")
232
            else:
233
                # Internal path is valid, check data is present
234
                int_data = np.array(h5path)
235
                if int_data.size >= 1:
236
                    return True, ""
237
        except AttributeError:
238
            print("Attribute error.")
239
        except:
240
            print(
241
                Fore.BLUE + "Please choose another interior path."
242
                + Fore.RESET
243
            )
244
            print("Example interior paths: ")
245
            for group in hf:
246
                for subgroup in hf[group]:
247
                    subgroup_str = "/" + group + "/" + subgroup
248
                    print(u"\t" + subgroup_str)
249
            raise
250
    return False, "Invalid path %s for file %s" % (h5path, filepath)
251
252
253
def _list(value):
254
    """ A non-empty list """
255
    if isinstance(value, list):
256
        return True
257
    return False
258
259
260
def _dict(value):
261
    """ A dictionary """
262
    return isinstance(value, dict)
263
264
265
def _None(value):
266
    """ None """
267
    return value == None  or value == "None"
268
269
270
def _dict_combination(param_name, value, param_def):
271
    dtype = copy.copy(param_def['dtype'])
272
273
    param_def['dtype'] = 'dict'
274
    # check this is a dictionary
275
    pvalid, error_str = _check_type(param_name, value, param_def)
276
    if not pvalid:
277
        return pvalid, error_str
278
    param_def['dtype'] = dtype
279
280
    return _check_dict_combination(param_name, value, param_def)
281
282
283
def _check_dict_combination(param_name, value, param_def):
284
    dtype = copy.copy(param_def['dtype'])
285
    dtype = dtype[len('dict'):]
286
    dtype = _find_options(dtype, 'dict', '{', '}', ':')
287
288
    #special case of empty dict
289
    if not value:
290
        if dtype[0] != "":
291
            error = "The empty dict is not a valid option for %s" % param_name
292
            return False, error
293
        else:
294
            return True, ""
295
296
    # check there are only two options - for key and for value:
297
    if len(dtype) != 2:
298
        return False, "Incorrect number of dtypes supplied for dictionary"
299
    return _check_dict_entry_dtype(param_name, value, param_def, dtype)
300
301
302
def _check_dict_entry_dtype(param_name, value, param_def, dtype):
303
    """ Check that the dict keys and values are of the correct dtype """
304
    # check the keys
305
    n_vals = len(value.keys())
306
307
    multi_vals = zip(list([dtype[0]] * n_vals), list(value.keys()))
308
    pvalid, error_str = _is_valid_multi(param_name, param_def, multi_vals)
309
310
    if not pvalid:
311
        # If the keys are not the correct type, break and return False
312
        return pvalid, error_str
313
314
    # check the values:
315
    multi_vals = zip(list([dtype[1]] * n_vals), list(value.values()))
316
    return _is_valid_multi(param_name, param_def, multi_vals)
317
318
319
def _options_list(param_name, value, param_def):
320
    """
321
    There are multiple options of dtype defined in a list.
322
    E.g. dtype: [string, int] # dtype can be a string or an integer
323
    """
324
    dtype = _find_options(param_def['dtype'])
325
    for atype in dtype:
326
        param_def['dtype'] = atype
327
        pvalid, error_str = is_valid(param_name, value, param_def)
328
        if pvalid:
329
            return pvalid, error_str
330
    return pvalid, _error_message(param_name, dtype)
0 ignored issues
show
introduced by
The variable pvalid does not seem to be defined in case the for loop on line 325 is not entered. Are you sure this can never be the case?
Loading history...
331
332
333
def _list_combination(param_name, value, param_def):
334
    """
335
    e.g.
336
    (1) list
337
    (1) list[btype] => any length
338
    (2) list[btype, btype]  => fixed length (and btype can be same or different)
339
        - list[int], list[string, string], list[list[string, float], int]
340
    (3) list[filepath, h5path, int]
341
    (4) list[[option1, option2]] = list[option1 AND/OR option2]
342
    """
343
    dtype = copy.copy(param_def['dtype'])
344
345
    # is it a list?
346
    param_def['dtype'] = 'list'
347
    pvalid, error_str = _check_type(param_name, value, param_def)
348
    if not pvalid:
349
        return pvalid, error_str
350
    param_def['dtype'] = dtype
351
352
    return _check_list_combination(param_name, value, param_def)
353
354
355
def _check_list_combination(param_name, value, param_def):
356
    dtype = copy.copy(param_def['dtype'])
357
    # remove outer list from dtype and find separate list entries
358
    dtype = _find_options(dtype[len('list'):])
359
360
    #special case of empty list
361
    if not value:
362
        if dtype[0] != "":
363
            error = "The empty list is not a valid option for %s" % param_name
364
            return False, error
365
        else:
366
            return True, ""
367
368
    # list can have any length if btype_list has length 1
369
    if len(dtype) == 1:
370
        dtype = dtype*len(value)
371
372
    if len(dtype) != len(value):
373
        return False, f"Incorrect number of list entries for {value}. " \
374
            f"The required format is {dtype}"
375
376
    return _is_valid_multi(param_name, param_def, zip(dtype, value))
377
378
379
def _matched_brackets(string, dtype, bstart, bend):
380
    start_brackets = [m.start() for m in re.finditer(r'\%s' % bstart, string)]
381
    end_brackets = [m.start() for m in re.finditer(r'\%s' % bend, string)]
382
    matched = []
383
    # Match start and end brackets
384
    while(end_brackets):
385
        try:
386
            end = end_brackets.pop(0)
387
            idx = start_brackets.index([s for s in start_brackets if s < end][-1])
388
            start = start_brackets.pop(idx)
389
            extra = len(dtype) if string[start-4:start] == dtype else 0
390
        except IndexError as ie:
391
            raise IndexError(f"Incorrect number of brackets in {string}")
392
        matched.append((start - extra, end))
393
394
    return matched
395
396
397
def _remove_nested_brackets(matched):
398
    if len(matched) > 1:
399
        for outer in matched[::-1]:
400
            for i in range(len(matched[:-1]))[::-1]:
401
                # Remove if is this bracket inside the outer bracket
402
                if matched[i][0] > outer[0] and matched[i][1] < outer[1]:
403
                    matched.pop(i)
404
    return matched
405
406
407
def _find_options(string, dtype='list', bstart="[", bend="]", split=","):
408
    string = string[1:-1]
409
    matched = _matched_brackets(string, dtype, bstart, bend)
410
    # find and remove nested brackets
411
    matched = _remove_nested_brackets(matched)
412
    replace_strings = {}
413
    # replace statements with place holders containing no commas
414
    shift = 0
415
    for i in range(len(matched)):
416
        replace = string[matched[i][0]-shift:matched[i][1]-shift+1]
417
        replacement = '$' + str(i)
418
        replace_strings[replacement] = replace
419
        string = string.replace(replace, replacement)
420
        shift = matched[i][1] - matched[i][0] - 1
421
422
    options = string.split(split)
423
    # substitute original statements back in
424
    for i in range(len(options)):
425
        if options[i] in replace_strings.keys():
426
            options[i] = replace_strings[options[i]]
427
428
    return options
429
430
431
def _convert_to_list(value):
432
    return value if isinstance(value, list) else [value]
433
434
435
def _is_valid_multi(param_name, param_def, multi_vals):
436
    dtype = copy.copy(param_def['dtype'])
437
    for atype, val in multi_vals:
438
        param_def['dtype'] = atype
439
        _check_val = pu._dumps(val)
440
        pvalid, error_str = is_valid(param_name, _check_val, param_def)
441
        if not pvalid:
442
            error_str = "The value %s should be of type %s" % (val, atype)
443
            return pvalid, error_str
444
    param_def['dtype'] = dtype
445
    return True, ""
446
447
448
def is_valid(param_name, value, param_def, check=False):
449
    """Check if the parameter value is a valid data type for the parameter
450
451
    :param param_name: The name of the parameter
452
    :param value: The new value of the parameter
453
    :param param_def: Parameter definition dictionary, containing e.g.,
454
        description, dtype, default
455
    :return: boolean True if the value is a valid parameter value
456
    """
457
    original_dtype = copy.copy(param_def['dtype'])
458
    # remove all whitespaces from dtype
459
    param_def['dtype'] = param_def['dtype'].replace(" ", "")
460
461
    # If a default value is used, this is a valid option
462
    # Don't perform this check when checking the default value itself
463
    if not check:
464
        if _check_default(value, param_def['default']):
465
            return True, ""
466
467
    dtype = param_def["dtype"]
468
469
    # If this is parameter tuning, check each individually
470
    if is_multi_param(param_name, value):
471
        return _check_multi_param(param_name, value, param_def)
472
473
    if not dtype.split('list[')[0]:
474
        pvalid, error_str = _list_combination(param_name, value, param_def)
475
    elif not dtype.split('dict{')[0]:
476
        pvalid, error_str = _dict_combination(param_name, value, param_def)
477
    elif not dtype.split('[')[0] and not dtype.split(']')[-1]:
478
        pvalid, error_str = _options_list(param_name, value, param_def)
479
    else:
480
        pvalid, error_str =_check_type(param_name, value, param_def)
481
482
    # set dtype back to the original
483
    param_def['dtype'] = original_dtype
484
    return pvalid, error_str
485
486
487
def _check_type(param_name, value, param_def):
488
    """Check if the provided value matches the required date type
489
490
    :param param_name: The parameter name
491
    :param value: The new value
492
    :param param_def: Parameter definition dictionary
493
    :return: pvalid, True if the value type matches the required dtype
494
              type_error_str, Error message
495
    """
496
    dtype = param_def['dtype']
497
    # If this is parameter tuning, check each individually
498
    try:
499
        pvalid = globals()["_" + dtype](value)
500
    except KeyError:
501
        return False, "Unknown dtype '%s'" % dtype
502
503
    pvalid, opt_err = _check_options(param_def, value, pvalid)
504
    if not pvalid:
505
        return pvalid, opt_err if opt_err \
506
            else _error_message(param_name, dtype)
507
508
    return True, ""
509
510
511
def _check_multi_param(param_name, value, param_def):
512
    """ Check each multi parameter value individually
513
514
    :param param_name: The parameter name
515
    :param value: The multi parameter value to check
516
    :param param_def: The dictionary of parameter definitions
517
    :return: pvalid True if the value type matches the required dtype
518
             type_error_str, Error message
519
    """
520
    val_list, error_str = pu.convert_multi_params(param_name, value)
521
    # incorrect parameter tuning syntax
522
    if error_str:
523
        return False, error_str
524
    for val in val_list:
525
        pvalid, error_str = is_valid(param_name, val, param_def)
526
        if not pvalid:
527
            break
528
    return pvalid, error_str
0 ignored issues
show
introduced by
The variable pvalid does not seem to be defined in case the for loop on line 524 is not entered. Are you sure this can never be the case?
Loading history...
529
530
531
def is_multi_param(param_name, value):
532
    """Return True if the value is made up of multiple parameters"""
533
    return (
534
        _str(value) and (";" in value) and param_name != "preview"
535
    )
536
537
538
def _check_default(value, default_value):
539
    """Return true if the new value is a match for the default
540
    parameter value
541
    """
542
    default_present = False
543
    if str(default_value) == str(value):
544
        default_present = True
545
    return default_present
546
547
548
def _check_options(param_def, value, pvalid):
549
    """Check if the input value matches one of the valid parameter options"""
550
    option_error_str = ""
551
    options = param_def.get("options") or {}
552
    if len(options) >= 1:
553
        if value in options or str(value) in options:
554
            pvalid = True
555
        else:
556
            pvalid = False
557
            option_error_str = (
558
                "It does not match one of the required options."
559
            )
560
            option_error_str += Fore.CYAN + "\nThe options are:\n"
561
            option_error_str += "\n".join(str(o) for o in options) + Fore.RESET
562
    return pvalid, option_error_str
563
564
565
def _error_message(param_name, dtype):
566
    """Create an error message"""
567
    if isinstance(dtype, list):
568
        type_options = "' or '".join(
569
            [str(type_error_dict[t] if t in type_error_dict else t)
570
                for t in dtype]
571
        )
572
        error_str = f"The parameter '{param_name}' does not match" \
573
                    f" the options: '{type_options}'."
574
    else:
575
        error_str = f"The parameter '{param_name}' does not match " \
576
                    f"the type: '{type_error_dict[dtype]}'."
577
    return error_str
578
579
580
def _gui_error_message(param_name, dtype):
581
    """Create an error string for the GUI
582
    Remove the paramter name, as the GUI message will be displayed below
583
    each parameter input box
584
    """
585
    if isinstance(dtype, list):
586
        type_options = "' or '".join([str(t) for t in dtype])
587
        error_str = f"Type must match '{type_options}'."
588
    else:
589
        error_str = f"Type must match '{type_error_dict[dtype]}'."
590
    return error_str
591
592
593
type_error_dict = {
594
    "preview": "preview slices",
595
    "yamlfilepath": "yaml filepath",
596
    "filepath": "filepath",
597
    "h5path" : "hdf5 path",
598
    "filename": "file name",
599
    "dir": "directory",
600
    "nptype": "numpy data type",
601
    "int": "integer",
602
    "bool": "true/false",
603
    "str": "string",
604
    "float": "float/integer",
605
    "list": "list",
606
    "dict": "dict",
607
    "None": "None"
608
}
609
610
611
def is_valid_dtype(dtype):
612
    """
613
    Checks if the dtype is defined correctly
614
    """
615
    if not dtype.split('list[')[0]:
616
        pvalid, error_str = _is_valid_list_combination_type(dtype)
617
    elif not dtype.split('dict{')[0]:
618
        pvalid, error_str = _is_valid_dict_combination_type(dtype)
619
    elif not dtype.split('[')[0] and not dtype.split(']')[-1]:
620
        pvalid, error_str = _is_valid_options_list_type(dtype)
621
    else:
622
        if '_' + dtype in globals().keys():
623
            return True, ""
624
        else:
625
            return "False", "The basic dtype %s does not exist" % dtype
626
    return pvalid, error_str
627
628
629
def _is_valid_list_combination_type(dtype):
630
    if not dtype:
631
        return True, "" # the empty list
632
    if not dtype[-1] == ']':
633
        return False, "List combination is missing a closing bracket."
634
    return is_valid_dtype(dtype[len('list['):-1])
635
636
637
def _is_valid_dict_combination_type(dtype):
638
    if not dtype[-1] == '}':
639
        return False, "Dict combination is missing a closing bracket"
640
    dtype = dtype[len('dict{'):-1]
641
    dtype = _find_options(dtype, 'dict', '{', '}', ':')
642
    for atype in dtype:
643
        pvalid, error_str = is_valid_dtype(atype)
644
        if not pvalid:
645
            break
646
    return pvalid, error_str
0 ignored issues
show
introduced by
The variable pvalid does not seem to be defined in case the for loop on line 642 is not entered. Are you sure this can never be the case?
Loading history...
introduced by
The variable error_str does not seem to be defined in case the for loop on line 642 is not entered. Are you sure this can never be the case?
Loading history...
647
648
649
def _is_valid_options_list_type(dtype):
650
    if not dtype[-1] == ']':
651
        return False, "Options list is missing a closing bracket."
652
    dtype = _find_options(dtype)
653
    for atype in dtype:
654
        pvalid, error_str = is_valid_dtype(atype)
655
        if not pvalid:
656
            break
657
    return pvalid, error_str
0 ignored issues
show
introduced by
The variable pvalid does not seem to be defined in case the for loop on line 653 is not entered. Are you sure this can never be the case?
Loading history...
introduced by
The variable error_str does not seem to be defined in case the for loop on line 653 is not entered. Are you sure this can never be the case?
Loading history...
658