Test Failed
Push — master ( 6e1f01...b5951e )
by
unknown
01:27 queued 19s
created

scripts.config_generator.parameter_utils._dict()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 3
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:: 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
    if isinstance(value, list):
143
        if value:
144
            return all(func(item) for item in value)
145
    return False
146
147
148
def _preview_dimension(value):
149
    """ Check the full preview parameter value """
150
    if _str(value):
151
        slice_str = [":"*n for n in range(1,5)]
152
        if value in slice_str:
153
            # If : notation is used, accept this
154
            valid = True
155
        elif ":" in value:
156
            valid = _split_notation_is_valid(value)
157
        else:
158
            valid = _preview_dimension_singular(value)
159
    else:
160
        valid = _float(value)
161
    return valid
162
163
164
def _split_notation_is_valid(value):
165
    """Check if the start step stock chunk entries are valid
166
167
    :param value: The value to check
168
    :return: parameter_valid True if the split notation is valid
169
    """
170
    if value.count(":") < 4:
171
        # Only allow 4 colons, start stop step block
172
        start_stop_split = value.split(":")
173
        try:
174
            type_list = [pu._dumps(v) for v in start_stop_split if v]
175
            return _typelist(_preview_dimension_singular,
176
                             type_list)
177
        except Exception as e:
178
            print(f"There was an error with your slice notation, '{value}'")
179
    return False
180
181
182
def _preview_dimension_singular(value):
183
    """ Check the singular value within the preview dimension"""
184
    valid = False
185
    if _str(value):
186
        string_valid = re.fullmatch("(mid|end|[^a-zA-z])+", value)
187
        # Check that the string does not contain any letters [^a-zA-Z]
188
        # If it does contain letters, mid and end are the only keywords allowed
189
        if string_valid:
190
            try:
191
                # Attempt to evaluate the provided equation
192
                temp_value = _preview_eval(value)
193
                valid = _float(temp_value)
194
            except Exception:
195
                print("There was an error with your dimension value input.")
196
        else:
197
            print('If you are trying to use an expression, '
198
                  'please only use mid and end command words.')
199
    else:
200
        valid = _float(value)
201
    return valid
202
203
204
def _preview_eval(value):
205
    """ Evaluate with mid and end"""
206
    mid = 0
207
    end = 0
208
    return eval(value,{"__builtins__":None},{"mid":mid,"end":end})
209
210
211
#Replace this with if list combination contains filepath and h5path e.g. list[filepath, h5path, int] then perform this check
212
def _check_h5path(filepath, h5path):
213
    """ Check if the internal path is valid"""
214
    with h5py.File(filepath, "r") as hf:
215
        try:
216
            # Hdf5 dataset object
217
            h5path = hf.get(h5path)
218
            if h5path is None:
219
                print("There is no data stored at that internal path.")
220
            else:
221
                # Internal path is valid, check data is present
222
                int_data = np.array(h5path)
223
                if int_data.size >= 1:
224
                    return True, ""
225
        except AttributeError:
226
            print("Attribute error.")
227
        except:
228
            print(
229
                Fore.BLUE + "Please choose another interior path."
230
                + Fore.RESET
231
            )
232
            print("Example interior paths: ")
233
            for group in hf:
234
                for subgroup in hf[group]:
235
                    subgroup_str = "/" + group + "/" + subgroup
236
                    print(u"\t" + subgroup_str)
237
            raise
238
    return False, "Invalid path %s for file %s" % (h5path, filepath)
239
240
241
def _list(value):
242
    """ A non-empty list """
243
    if isinstance(value, list):
244
        return True
245
    return False
246
247
248
def _dict(value):
249
    """ A dictionary """
250
    return isinstance(value, dict)
251
252
253
def _None(value):
254
    """ None """
255
    return value == None  or value == "None"
256
257
258
def _dict_combination(param_name, value, param_def):
259
    dtype = copy.copy(param_def['dtype'])
260
261
    param_def['dtype'] = 'dict'
262
    # check this is a dictionary
263
    pvalid, error_str = _check_type(param_name, value, param_def)
264
    if not pvalid:
265
        return pvalid, error_str
266
    param_def['dtype'] = dtype
267
268
    return _check_dict_combination(param_name, value, param_def)
269
270
271
def _check_dict_combination(param_name, value, param_def):
272
    dtype = copy.copy(param_def['dtype'])
273
    dtype = dtype[len('dict'):]
274
    dtype = _find_options(dtype, 'dict', '{', '}', ':')
275
276
    #special case of empty dict
277
    if not value:
278
        if dtype[0] != "":
279
            error = "The empty dict is not a valid option for %s" % param_name
280
            return False, error
281
        else:
282
            return True, ""
283
284
    # check there are only two options - for key and for value:
285
    if len(dtype) != 2:
286
        return False, "Incorrect number of dtypes supplied for dictionary"
287
    return _check_dict_entry_dtype(param_name, value, param_def, dtype)
288
289
290
def _check_dict_entry_dtype(param_name, value, param_def, dtype):
291
    """ Check that the dict keys and values are of the correct dtype """
292
    # check the keys
293
    n_vals = len(value.keys())
294
295
    multi_vals = zip(list([dtype[0]] * n_vals), list(value.keys()))
296
    pvalid, error_str = _is_valid_multi(param_name, param_def, multi_vals)
297
298
    if not pvalid:
299
        # If the keys are not the correct type, break and return False
300
        return pvalid, error_str
301
302
    # check the values:
303
    multi_vals = zip(list([dtype[1]] * n_vals), list(value.values()))
304
    return _is_valid_multi(param_name, param_def, multi_vals)
305
306
307
def _options_list(param_name, value, param_def):
308
    """
309
    There are multiple options of dtype defined in a list.
310
    E.g. dtype: [string, int] # dtype can be a string or an integer
311
    """
312
    dtype = _find_options(param_def['dtype'])
313
    for atype in dtype:
314
        param_def['dtype'] = atype
315
        pvalid, error_str = is_valid(param_name, value, param_def)
316
        if pvalid:
317
            return pvalid, error_str
318
    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 313 is not entered. Are you sure this can never be the case?
Loading history...
319
320
321
def _list_combination(param_name, value, param_def):
322
    """
323
    e.g.
324
    (1) list
325
    (1) list[btype] => any length
326
    (2) list[btype, btype]  => fixed length (and btype can be same or different)
327
        - list[int], list[string, string], list[list[string, float], int]
328
    (3) list[filepath, h5path, int]
329
    (4) list[[option1, option2]] = list[option1 AND/OR option2]
330
    """
331
    dtype = copy.copy(param_def['dtype'])
332
333
    # is it a list?
334
    param_def['dtype'] = 'list'
335
    pvalid, error_str = _check_type(param_name, value, param_def)
336
    if not pvalid:
337
        return pvalid, error_str
338
    param_def['dtype'] = dtype
339
340
    return _check_list_combination(param_name, value, param_def)
341
342
343
def _check_list_combination(param_name, value, param_def):
344
    dtype = copy.copy(param_def['dtype'])
345
    # remove outer list from dtype and find separate list entries
346
    dtype = _find_options(dtype[len('list'):])
347
348
    #special case of empty list
349
    if not value:
350
        if dtype[0] != "":
351
            error = "The empty list is not a valid option for %s" % param_name
352
            return False, error
353
        else:
354
            return True, ""
355
356
    # list can have any length if btype_list has length 1
357
    if len(dtype) == 1:
358
        dtype = dtype*len(value)
359
360
    if len(dtype) != len(value):
361
        return False, f"Incorrect number of list entries for {value}. " \
362
            f"The required format is {dtype}"
363
364
    return _is_valid_multi(param_name, param_def, zip(dtype, value))
365
366
367
def _matched_brackets(string, dtype, bstart, bend):
368
    start_brackets = [m.start() for m in re.finditer(r'\%s' % bstart, string)]
369
    end_brackets = [m.start() for m in re.finditer(r'\%s' % bend, string)]
370
    matched = []
371
    # Match start and end brackets
372
    while(end_brackets):
373
        try:
374
            end = end_brackets.pop(0)
375
            idx = start_brackets.index([s for s in start_brackets if s < end][-1])
376
            start = start_brackets.pop(idx)
377
            extra = len(dtype) if string[start-4:start] == dtype else 0
378
        except IndexError as ie:
379
            raise IndexError(f"Incorrect number of brackets in {string}")
380
        matched.append((start - extra, end))
381
382
    return matched
383
384
385
def _remove_nested_brackets(matched):
386
    if len(matched) > 1:
387
        for outer in matched[::-1]:
388
            for i in range(len(matched[:-1]))[::-1]:
389
                # Remove if is this bracket inside the outer bracket
390
                if matched[i][0] > outer[0] and matched[i][1] < outer[1]:
391
                    matched.pop(i)
392
    return matched
393
394
395
def _find_options(string, dtype='list', bstart="[", bend="]", split=","):
396
    string = string[1:-1]
397
    matched = _matched_brackets(string, dtype, bstart, bend)
398
    # find and remove nested brackets
399
    matched = _remove_nested_brackets(matched)
400
    replace_strings = {}
401
    # replace statements with place holders containing no commas
402
    shift = 0
403
    for i in range(len(matched)):
404
        replace = string[matched[i][0]-shift:matched[i][1]-shift+1]
405
        replacement = '$' + str(i)
406
        replace_strings[replacement] = replace
407
        string = string.replace(replace, replacement)
408
        shift = matched[i][1] - matched[i][0] - 1
409
410
    options = string.split(split)
411
    # substitute original statements back in
412
    for i in range(len(options)):
413
        if options[i] in replace_strings.keys():
414
            options[i] = replace_strings[options[i]]
415
416
    return options
417
418
419
def _convert_to_list(value):
420
    return value if isinstance(value, list) else [value]
421
422
423
def _is_valid_multi(param_name, param_def, multi_vals):
424
    dtype = copy.copy(param_def['dtype'])
425
    for atype, val in multi_vals:
426
        param_def['dtype'] = atype
427
        _check_val = pu._dumps(val)
428
        pvalid, error_str = is_valid(param_name, _check_val, param_def)
429
        if not pvalid:
430
            error_str = "The value %s should be of type %s" % (val, atype)
431
            return pvalid, error_str
432
    param_def['dtype'] = dtype
433
    return True, ""
434
435
436
def is_valid(param_name, value, param_def, check=False):
437
    """Check if the parameter value is a valid data type for the parameter
438
439
    :param param_name: The name of the parameter
440
    :param value: The new value of the parameter
441
    :param param_def: Parameter definition dictionary, containing e.g.,
442
        description, dtype, default
443
    :return: boolean True if the value is a valid parameter value
444
    """
445
    original_dtype = copy.copy(param_def['dtype'])
446
    # remove all whitespaces from dtype
447
    param_def['dtype'] = param_def['dtype'].replace(" ", "")
448
449
    # If a default value is used, this is a valid option
450
    # Don't perform this check when checking the default value itself
451
    if not check:
452
        if _check_default(value, param_def['default']):
453
            return True, ""
454
455
    dtype = param_def["dtype"]
456
457
    # If this is parameter tuning, check each individually
458
    if is_multi_param(param_name, value):
459
        return _check_multi_param(param_name, value, param_def)
460
461
    if not dtype.split('list[')[0]:
462
        pvalid, error_str = _list_combination(param_name, value, param_def)
463
    elif not dtype.split('dict{')[0]:
464
        pvalid, error_str = _dict_combination(param_name, value, param_def)
465
    elif not dtype.split('[')[0] and not dtype.split(']')[-1]:
466
        pvalid, error_str = _options_list(param_name, value, param_def)
467
    else:
468
        pvalid, error_str =_check_type(param_name, value, param_def)
469
470
    # set dtype back to the original
471
    param_def['dtype'] = original_dtype
472
    return pvalid, error_str
473
474
475
def _check_type(param_name, value, param_def):
476
    """Check if the provided value matches the required date type
477
478
    :param param_name: The parameter name
479
    :param value: The new value
480
    :param param_def: Parameter definition dictionary
481
    :return: pvalid, True if the value type matches the required dtype
482
              type_error_str, Error message
483
    """
484
    dtype = param_def['dtype']
485
    # If this is parameter tuning, check each individually
486
    try:
487
        pvalid = globals()["_" + dtype](value)
488
    except KeyError:
489
        return False, "Unknown dtype '%s'" % dtype
490
491
    pvalid, opt_err = _check_options(param_def, value, pvalid)
492
    if not pvalid:
493
        return pvalid, opt_err if opt_err \
494
            else _error_message(param_name, dtype)
495
496
    return True, ""
497
498
499
def _check_multi_param(param_name, value, param_def):
500
    """ Check each multi parameter value individually
501
502
    :param param_name: The parameter name
503
    :param value: The multi parameter value to check
504
    :param param_def: The dictionary of parameter definitions
505
    :return: pvalid True if the value type matches the required dtype
506
             type_error_str, Error message
507
    """
508
    val_list, error_str = pu.convert_multi_params(param_name, value)
509
    # incorrect parameter tuning syntax
510
    if error_str:
511
        return False, error_str
512
    for val in val_list:
513
        pvalid, error_str = is_valid(param_name, val, param_def)
514
        if not pvalid:
515
            break
516
    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 512 is not entered. Are you sure this can never be the case?
Loading history...
517
518
519
def is_multi_param(param_name, value):
520
    """Return True if the value is made up of multiple parameters"""
521
    return (
522
        _str(value) and (";" in value) and param_name != "preview"
523
    )
524
525
526
def _check_default(value, default_value):
527
    """Return true if the new value is a match for the default
528
    parameter value
529
    """
530
    default_present = False
531
    if str(default_value) == str(value):
532
        default_present = True
533
    return default_present
534
535
536
def _check_options(param_def, value, pvalid):
537
    """Check if the input value matches one of the valid parameter options"""
538
    option_error_str = ""
539
    options = param_def.get("options") or {}
540
    if len(options) >= 1:
541
        if value in options or str(value) in options:
542
            pvalid = True
543
        else:
544
            pvalid = False
545
            option_error_str = (
546
                "That does not match one of the required options."
547
            )
548
            option_error_str += Fore.CYAN + "\nThe options are:\n"
549
            option_error_str += "\n".join(str(o) for o in options) + Fore.RESET
550
    return pvalid, option_error_str
551
552
553
def _error_message(param_name, dtype):
554
    """Create an error message"""
555
    if isinstance(dtype, list):
556
        type_options = "' or '".join(
557
            [str(type_error_dict[t] if t in type_error_dict else t)
558
                for t in dtype]
559
        )
560
        error_str = f"The parameter '{param_name}' does not match" \
561
                    f" the options: '{type_options}'."
562
    else:
563
        error_str = f"The parameter '{param_name}' does not match " \
564
                    f"the type: '{type_error_dict[dtype]}'."
565
    return error_str
566
567
568
def _gui_error_message(param_name, dtype):
569
    """Create an error string for the GUI
570
    Remove the paramter name, as the GUI message will be displayed below
571
    each parameter input box
572
    """
573
    if isinstance(dtype, list):
574
        type_options = "' or '".join([str(t) for t in dtype])
575
        error_str = f"Type must match '{type_options}'."
576
    else:
577
        error_str = f"Type must match '{type_error_dict[dtype]}'."
578
    return error_str
579
580
581
type_error_dict = {
582
    "preview": "preview slices",
583
    "yamlfilepath": "yaml filepath",
584
    "filepath": "filepath",
585
    "h5path" : "hdf5 path",
586
    "filename": "file name",
587
    "dir": "directory",
588
    "nptype": "numpy data type",
589
    "int": "integer",
590
    "bool": "true/false",
591
    "str": "string",
592
    "float": "float/integer",
593
    "list": "list",
594
    "dict": "dict",
595
    "None": "None"
596
}
597
598
599
def is_valid_dtype(dtype):
600
    """
601
    Checks if the dtype is defined correctly
602
    """
603
    if not dtype.split('list[')[0]:
604
        pvalid, error_str = _is_valid_list_combination_type(dtype)
605
    elif not dtype.split('dict{')[0]:
606
        pvalid, error_str = _is_valid_dict_combination_type(dtype)
607
    elif not dtype.split('[')[0] and not dtype.split(']')[-1]:
608
        pvalid, error_str = _is_valid_options_list_type(dtype)
609
    else:
610
        if '_' + dtype in globals().keys():
611
            return True, ""
612
        else:
613
            return "False", "The basic dtype %s does not exist" % dtype
614
    return pvalid, error_str
615
616
617
def _is_valid_list_combination_type(dtype):
618
    if not dtype:
619
        return True, "" # the empty list
620
    if not dtype[-1] == ']':
621
        return False, "List combination is missing a closing bracket."
622
    return is_valid_dtype(dtype[len('list['):-1])
623
624
625
def _is_valid_dict_combination_type(dtype):
626
    if not dtype[-1] == '}':
627
        return False, "Dict combination is missing a closing bracket"
628
    dtype = dtype[len('dict{'):-1]
629
    dtype = _find_options(dtype, 'dict', '{', '}', ':')
630
    for atype in dtype:
631
        pvalid, error_str = is_valid_dtype(atype)
632
        if not pvalid:
633
            break
634
    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 630 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 630 is not entered. Are you sure this can never be the case?
Loading history...
635
636
637
def _is_valid_options_list_type(dtype):
638
    if not dtype[-1] == ']':
639
        return False, "Options list is missing a closing bracket."
640
    dtype = _find_options(dtype)
641
    for atype in dtype:
642
        pvalid, error_str = is_valid_dtype(atype)
643
        if not pvalid:
644
            break
645
    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 641 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 641 is not entered. Are you sure this can never be the case?
Loading history...
646