1#!/usr/bin/env python3
2
3r"""
4This module provides validation functions like valid_value(), valid_integer(), etc.
5"""
6
7import datetime
8import os
9
10import func_args as fa
11import gen_cmd as gc
12import gen_print as gp
13
14exit_on_error = False
15
16
17def set_exit_on_error(value):
18    r"""
19    Set the exit_on_error value to either True or False.
20
21    If exit_on_error is set, validation functions like valid_value() will exit the program on error instead
22    of returning False.
23
24    Description of argument(s):
25    value                           Value to set global exit_on_error to.
26    """
27
28    global exit_on_error
29    exit_on_error = value
30
31
32def get_var_name(var_name):
33    r"""
34    If var_name is not None, simply return its value.  Otherwise, get the variable name of the first argument
35    used to call the validation function (e.g. valid, valid_integer, etc.) and return it.
36
37    This function is designed solely for use by other functions in this file.
38
39    Example:
40
41    A programmer codes this:
42
43    valid_value(last_name)
44
45    Which results in the following call stack:
46
47    valid_value(last_name)
48      -> get_var_name(var_name)
49
50    In this example, this function will return "last_name".
51
52    Example:
53
54    err_msg = valid_value(last_name, var_name="some_other_name")
55
56    Which results in the following call stack:
57
58    valid_value(var_value, var_name="some_other_name")
59      -> get_var_name(var_name)
60
61    In this example, this function will return "some_other_name".
62
63    Description of argument(s):
64    var_name                        The name of the variable.
65    """
66
67    return var_name or gp.get_arg_name(0, 1, stack_frame_ix=3)
68
69
70def process_error_message(error_message):
71    r"""
72    Process the error_message in the manner described below.
73
74    This function is designed solely for use by other functions in this file.
75
76    NOTE: A blank error_message means that there is no error.
77
78    For the following explanations, assume the caller of this function is a function with the following
79    definition:
80    valid_value(var_value, valid_values=[], invalid_values=[], var_name=None):
81
82    If the user of valid_value() is assigning the valid_value() return value to a variable,
83    process_error_message() will simply return the error_message.  This mode of usage is illustrated by the
84    following example:
85
86    error_message = valid_value(var1)
87
88    This mode is useful for callers who wish to validate a variable and then decide for themselves what to do
89    with the error_message (e.g. raise(error_message), BuiltIn().fail(error_message), etc.).
90
91    If the user of valid_value() is NOT assigning the valid_value() return value to a variable,
92    process_error_message() will behave as follows.
93
94    First, if error_message is non-blank, it will be printed to stderr via a call to
95    gp.print_error_report(error_message).
96
97    If exit_on_error is set:
98    - If the error_message is blank, simply return.
99    - If the error_message is non-blank, exit the program with a return code of 1.
100
101    If exit_on_error is NOT set:
102    - If the error_message is blank, return True.
103    - If the error_message is non-blank, return False.
104
105    Description of argument(s):
106    error_message                   An error message.
107    """
108
109    # Determine whether the caller's caller is assigning the result to a variable.
110    l_value = gp.get_arg_name(None, -1, stack_frame_ix=3)
111    if l_value:
112        return error_message
113
114    if error_message == "":
115        if exit_on_error:
116            return
117        return True
118
119    gp.print_error_report(error_message, stack_frame_ix=4)
120    if exit_on_error:
121        exit(1)
122    return False
123
124
125# Note to programmers:  All of the validation functions in this module should follow the same basic template:
126# def valid_value(var_value, var1, var2, varn, var_name=None):
127#
128#     error_message = ""
129#     if not valid:
130#         var_name = get_var_name(var_name)
131#         error_message += "The following variable is invalid because...:\n"
132#         error_message += gp.sprint_varx(var_name, var_value, gp.blank())
133#
134#     return process_error_message(error_message)
135
136
137# The docstring header and footer will be added to each validation function's existing docstring.
138docstring_header = r"""
139    Determine whether var_value is valid, construct an error_message and call
140    process_error_message(error_message).
141
142    See the process_error_message() function defined in this module for a description of how error messages
143    are processed.
144    """
145
146additional_args_docstring_footer = r"""
147    var_name                        The name of the variable whose value is passed in var_value.  For the
148                                    general case, this argument is unnecessary as this function can figure
149                                    out the var_name.  This is provided for Robot callers in which case, this
150                                    function lacks the ability to determine the variable name.
151    """
152
153
154def valid_type(var_value, required_type, var_name=None):
155    r"""
156    The variable value is valid if it is of the required type.
157
158    Examples:
159
160    valid_type(var1, int)
161
162    valid_type(var1, (list, dict))
163
164    Description of argument(s):
165    var_value                       The value being validated.
166    required_type                   A type or a tuple of types (e.g. str, int, etc.).
167    """
168
169    error_message = ""
170    if type(required_type) is tuple:
171        if type(var_value) in required_type:
172            return process_error_message(error_message)
173    else:
174        if type(var_value) is required_type:
175            return process_error_message(error_message)
176
177    # If we get to this point, the validation has failed.
178    var_name = get_var_name(var_name)
179    error_message += "Invalid variable type:\n"
180    error_message += gp.sprint_varx(
181        var_name, var_value, gp.blank() | gp.show_type()
182    )
183    error_message += "\n"
184    error_message += gp.sprint_var(required_type)
185
186    return process_error_message(error_message)
187
188
189def valid_value(var_value, valid_values=[], invalid_values=[], var_name=None):
190    r"""
191    The variable value is valid if it is either contained in the valid_values list or if it is NOT contained
192    in the invalid_values list.  If the caller specifies nothing for either of these 2 arguments,
193    invalid_values will be initialized to ['', None].  This is a good way to fail on variables which contain
194    blank values.
195
196    It is illegal to specify both valid_values and invalid values.
197
198    Example:
199
200    var1 = ''
201    valid_value(var1)
202
203    This code would fail because var1 is blank and the default value for invalid_values is ['', None].
204
205    Example:
206    var1 = 'yes'
207    valid_value(var1, valid_values=['yes', 'true'])
208
209    This code would pass.
210
211    Description of argument(s):
212    var_value                       The value being validated.
213    valid_values                    A list of valid values.  The variable value must be equal to one of these
214                                    values to be considered valid.
215    invalid_values                  A list of invalid values.  If the variable value is equal to any of
216                                    these, it is considered invalid.
217    """
218
219    error_message = ""
220
221    # Validate this function's arguments.
222    len_valid_values = len(valid_values)
223    len_invalid_values = len(invalid_values)
224    if len_valid_values > 0 and len_invalid_values > 0:
225        error_message += "Programmer error - You must provide either an"
226        error_message += " invalid_values list or a valid_values"
227        error_message += " list but NOT both:\n"
228        error_message += gp.sprint_var(invalid_values)
229        error_message += gp.sprint_var(valid_values)
230        return process_error_message(error_message)
231
232    error_message = valid_type(valid_values, list, var_name="valid_values")
233    if error_message:
234        return process_error_message(error_message)
235
236    error_message = valid_type(invalid_values, list, var_name="invalid_values")
237    if error_message:
238        return process_error_message(error_message)
239
240    if len_valid_values > 0:
241        # Processing the valid_values list.
242        if var_value in valid_values:
243            return process_error_message(error_message)
244        var_name = get_var_name(var_name)
245        error_message += "Invalid variable value:\n"
246        error_message += gp.sprint_varx(
247            var_name, var_value, gp.blank() | gp.verbose() | gp.show_type()
248        )
249        error_message += "\n"
250        error_message += "It must be one of the following values:\n"
251        error_message += "\n"
252        error_message += gp.sprint_var(
253            valid_values, gp.blank() | gp.show_type()
254        )
255        return process_error_message(error_message)
256
257    if len_invalid_values == 0:
258        # Assign default value.
259        invalid_values = ["", None]
260
261    # Assertion: We have an invalid_values list.  Processing it now.
262    if var_value not in invalid_values:
263        return process_error_message(error_message)
264
265    var_name = get_var_name(var_name)
266    error_message += "Invalid variable value:\n"
267    error_message += gp.sprint_varx(
268        var_name, var_value, gp.blank() | gp.verbose() | gp.show_type()
269    )
270    error_message += "\n"
271    error_message += "It must NOT be any of the following values:\n"
272    error_message += "\n"
273    error_message += gp.sprint_var(invalid_values, gp.blank() | gp.show_type())
274    return process_error_message(error_message)
275
276
277def valid_range(var_value, lower=None, upper=None, var_name=None):
278    r"""
279    The variable value is valid if it is within the specified range.
280
281    This function can be used with any type of operands where they can have a greater than/less than
282    relationship to each other (e.g. int, float, str).
283
284    Description of argument(s):
285    var_value                       The value being validated.
286    lower                           The lower end of the range.  If not None, the var_value must be greater
287                                    than or equal to lower.
288    upper                           The upper end of the range.  If not None, the var_value must be less than
289                                    or equal to upper.
290    """
291
292    error_message = ""
293    if lower is None and upper is None:
294        return process_error_message(error_message)
295    if lower is None and var_value <= upper:
296        return process_error_message(error_message)
297    if upper is None and var_value >= lower:
298        return process_error_message(error_message)
299    if lower is not None and upper is not None:
300        if lower > upper:
301            var_name = get_var_name(var_name)
302            error_message += "Programmer error - the lower value is greater"
303            error_message += " than the upper value:\n"
304            error_message += gp.sprint_vars(lower, upper, fmt=gp.show_type())
305            return process_error_message(error_message)
306        if lower <= var_value <= upper:
307            return process_error_message(error_message)
308
309    var_name = get_var_name(var_name)
310    error_message += "The following variable is not within the expected"
311    error_message += " range:\n"
312    error_message += gp.sprint_varx(var_name, var_value, gp.show_type())
313    error_message += "\n"
314    error_message += "range:\n"
315    error_message += gp.sprint_vars(lower, upper, fmt=gp.show_type(), indent=2)
316    return process_error_message(error_message)
317
318
319def valid_integer(var_value, lower=None, upper=None, var_name=None):
320    r"""
321    The variable value is valid if it is an integer or can be interpreted as an integer (e.g. 7, "7", etc.).
322
323    This function also calls valid_range to make sure the integer value is within the specified range (if
324    any).
325
326    Description of argument(s):
327    var_value                       The value being validated.
328    lower                           The lower end of the range.  If not None, the var_value must be greater
329                                    than or equal to lower.
330    upper                           The upper end of the range.  If not None, the var_value must be less than
331                                    or equal to upper.
332    """
333
334    error_message = ""
335    var_name = get_var_name(var_name)
336    try:
337        var_value = int(str(var_value), 0)
338    except ValueError:
339        error_message += "Invalid integer value:\n"
340        error_message += gp.sprint_varx(
341            var_name, var_value, gp.blank() | gp.show_type()
342        )
343        return process_error_message(error_message)
344
345    # Check the range (if any).
346    if lower:
347        lower = int(str(lower), 0)
348    if upper:
349        upper = int(str(upper), 0)
350    error_message = valid_range(var_value, lower, upper, var_name=var_name)
351
352    return process_error_message(error_message)
353
354
355def valid_float(var_value, lower=None, upper=None, var_name=None):
356    r"""
357    The variable value is valid if it is a floating point value or can be interpreted as a floating point
358    value (e.g. 7.5, "7.5", etc.).
359
360    This function also calls valid_range to make sure the float value is within the specified range (if any).
361
362    Description of argument(s):
363    var_value                       The value being validated.
364    lower                           The lower end of the range.  If not None, the var_value must be greater
365                                    than or equal to lower.
366    upper                           The upper end of the range.  If not None, the var_value must be less than
367                                    or equal to upper.
368    """
369
370    error_message = ""
371    var_name = get_var_name(var_name)
372    try:
373        var_value = float(str(var_value))
374    except ValueError:
375        error_message += "Invalid float value:\n"
376        error_message += gp.sprint_varx(
377            var_name, var_value, gp.blank() | gp.show_type()
378        )
379        return process_error_message(error_message)
380
381    # Check the range (if any).
382    if lower:
383        lower = float(str(lower))
384    if upper:
385        upper = float(str(upper))
386    error_message = valid_range(var_value, lower, upper, var_name=var_name)
387
388    return process_error_message(error_message)
389
390
391def valid_date_time(var_value, var_name=None):
392    r"""
393    The variable value is valid if it can be interpreted as a date/time (e.g. "14:49:49.981", "tomorrow",
394    etc.) by the linux date command.
395
396    Description of argument(s):
397    var_value                       The value being validated.
398    """
399
400    error_message = ""
401    rc, out_buf = gc.shell_cmd(
402        "date -d '" + str(var_value) + "'", quiet=1, show_err=0, ignore_err=1
403    )
404    if rc:
405        var_name = get_var_name(var_name)
406        error_message += "Invalid date/time value:\n"
407        error_message += gp.sprint_varx(
408            var_name, var_value, gp.blank() | gp.show_type()
409        )
410        return process_error_message(error_message)
411
412    return process_error_message(error_message)
413
414
415def valid_dir_path(var_value, var_name=None):
416    r"""
417    The variable value is valid if it contains the path of an existing directory.
418
419    Description of argument(s):
420    var_value                       The value being validated.
421    """
422
423    error_message = ""
424    if not os.path.isdir(str(var_value)):
425        var_name = get_var_name(var_name)
426        error_message += "The following directory does not exist:\n"
427        error_message += gp.sprint_varx(var_name, var_value, gp.blank())
428
429    return process_error_message(error_message)
430
431
432def valid_file_path(var_value, var_name=None):
433    r"""
434    The variable value is valid if it contains the path of an existing file.
435
436    Description of argument(s):
437    var_value                       The value being validated.
438    """
439
440    error_message = ""
441    if not os.path.isfile(str(var_value)):
442        var_name = get_var_name(var_name)
443        error_message += "The following file does not exist:\n"
444        error_message += gp.sprint_varx(var_name, var_value, gp.blank())
445
446    return process_error_message(error_message)
447
448
449def valid_path(var_value, var_name=None):
450    r"""
451    The variable value is valid if it contains the path of an existing file or directory.
452
453    Description of argument(s):
454    var_value                       The value being validated.
455    """
456
457    error_message = ""
458    if not (os.path.isfile(str(var_value)) or os.path.isdir(str(var_value))):
459        var_name = get_var_name(var_name)
460        error_message += "Invalid path (file or directory does not exist):\n"
461        error_message += gp.sprint_varx(var_name, var_value, gp.blank())
462
463    return process_error_message(error_message)
464
465
466def valid_list(
467    var_value,
468    valid_values=[],
469    invalid_values=[],
470    required_values=[],
471    fail_on_empty=False,
472    var_name=None,
473):
474    r"""
475    The variable value is valid if it is a list where each entry can be found in the valid_values list or if
476    none of its values can be found in the invalid_values list or if all of the values in the required_values
477    list can be found in var_value.
478
479    The caller may only specify one of these 3 arguments: valid_values, invalid_values, required_values.
480
481    Description of argument(s):
482    var_value                       The value being validated.
483    valid_values                    A list of valid values.  Each element in the var_value list must be equal
484                                    to one of these values to be considered valid.
485    invalid_values                  A list of invalid values.  If any element in var_value is equal to any of
486                                    the values in this argument, var_value is considered invalid.
487    required_values                 Every value in required_values must be found in var_value.  Otherwise,
488                                    var_value is considered invalid.
489    fail_on_empty                   Indicates that an empty list for the variable value should be considered
490                                    an error.
491    """
492
493    error_message = ""
494
495    # Validate this function's arguments.
496    if not (
497        bool(len(valid_values))
498        ^ bool(len(invalid_values))
499        ^ bool(len(required_values))
500    ):
501        error_message += "Programmer error - You must provide only one of the"
502        error_message += " following: valid_values, invalid_values,"
503        error_message += " required_values.\n"
504        error_message += gp.sprint_var(invalid_values, gp.show_type())
505        error_message += gp.sprint_var(valid_values, gp.show_type())
506        error_message += gp.sprint_var(required_values, gp.show_type())
507        return process_error_message(error_message)
508
509    if type(var_value) is not list:
510        var_name = get_var_name(var_name)
511        error_message = valid_type(var_value, list, var_name=var_name)
512        if error_message:
513            return process_error_message(error_message)
514
515    if fail_on_empty and len(var_value) == 0:
516        var_name = get_var_name(var_name)
517        error_message += "Invalid empty list:\n"
518        error_message += gp.sprint_varx(var_name, var_value, gp.show_type())
519        return process_error_message(error_message)
520
521    if len(required_values):
522        found_error = 0
523        display_required_values = list(required_values)
524        for ix in range(0, len(required_values)):
525            if required_values[ix] not in var_value:
526                found_error = 1
527                display_required_values[ix] = (
528                    str(display_required_values[ix]) + "*"
529                )
530        if found_error:
531            var_name = get_var_name(var_name)
532            error_message += "The following list is invalid:\n"
533            error_message += gp.sprint_varx(
534                var_name, var_value, gp.blank() | gp.show_type()
535            )
536            error_message += "\n"
537            error_message += "Because some of the values in the "
538            error_message += "required_values list are not present (see"
539            error_message += ' entries marked with "*"):\n'
540            error_message += "\n"
541            error_message += gp.sprint_varx(
542                "required_values",
543                display_required_values,
544                gp.blank() | gp.show_type(),
545            )
546            error_message += "\n"
547
548        return process_error_message(error_message)
549
550    if len(invalid_values):
551        found_error = 0
552        display_var_value = list(var_value)
553        for ix in range(0, len(var_value)):
554            if var_value[ix] in invalid_values:
555                found_error = 1
556                display_var_value[ix] = str(var_value[ix]) + "*"
557
558        if found_error:
559            var_name = get_var_name(var_name)
560            error_message += "The following list is invalid (see entries"
561            error_message += ' marked with "*"):\n'
562            error_message += gp.sprint_varx(
563                var_name, display_var_value, gp.blank() | gp.show_type()
564            )
565            error_message += "\n"
566            error_message += gp.sprint_var(invalid_values, gp.show_type())
567        return process_error_message(error_message)
568
569    found_error = 0
570    display_var_value = list(var_value)
571    for ix in range(0, len(var_value)):
572        if var_value[ix] not in valid_values:
573            found_error = 1
574            display_var_value[ix] = str(var_value[ix]) + "*"
575
576    if found_error:
577        var_name = get_var_name(var_name)
578        error_message += "The following list is invalid (see entries marked"
579        error_message += ' with "*"):\n'
580        error_message += gp.sprint_varx(
581            var_name, display_var_value, gp.blank() | gp.show_type()
582        )
583        error_message += "\n"
584        error_message += gp.sprint_var(valid_values, gp.show_type())
585        return process_error_message(error_message)
586
587    return process_error_message(error_message)
588
589
590def valid_dict(
591    var_value,
592    required_keys=[],
593    valid_values={},
594    invalid_values={},
595    var_name=None,
596):
597    r"""
598    The dictionary variable value is valid if it contains all required keys and each entry passes the
599    valid_value() call.
600
601    Examples:
602    person_record = {'last_name': 'Jones', 'first_name': 'John'}
603    valid_values = {'last_name': ['Doe', 'Jones', 'Johnson'], 'first_name': ['John', 'Mary']}
604    invalid_values = {'last_name': ['Manson', 'Hitler', 'Presley'], 'first_name': ['Mickey', 'Goofy']}
605
606    valid_dict(person_record, valid_values=valid_values)
607    valid_dict(person_record, invalid_values=invalid_values)
608
609    Description of argument(s):
610    var_value                       The value being validated.
611    required_keys                   A list of keys which must be found in the dictionary for it to be
612                                    considered valid.
613    valid_values                    A dictionary whose entries correspond to the entries in var_value.  Each
614                                    value in valid_values is itself a valid_values list for the corresponding
615                                    value in var_value.  For any var_value[key] to be considered valid, its
616                                    value must be found in valid_values[key].
617
618    invalid_values                  A dictionary whose entries correspond to the entries in var_value.  Each
619                                    value in invalid_values is itself an invalid_values list for the
620                                    corresponding value in var_value.  For any var_value[key] to be
621                                    considered valid, its value must NOT be found in invalid_values[key].
622    """
623
624    error_message = ""
625    missing_keys = list(set(required_keys) - set(var_value.keys()))
626    if len(missing_keys) > 0:
627        var_name = get_var_name(var_name)
628        error_message += "The following dictionary is invalid because it is"
629        error_message += " missing required keys:\n"
630        error_message += gp.sprint_varx(
631            var_name, var_value, gp.blank() | gp.show_type()
632        )
633        error_message += "\n"
634        error_message += gp.sprint_var(missing_keys, gp.show_type())
635        return process_error_message(error_message)
636
637    var_name = get_var_name(var_name)
638    if len(valid_values):
639        keys = valid_values.keys()
640        error_message = valid_dict(
641            var_value, required_keys=keys, var_name=var_name
642        )
643        if error_message:
644            return process_error_message(error_message)
645    for key, value in valid_values.items():
646        key_name = "  [" + key + "]"
647        sub_error_message = valid_value(
648            var_value[key], valid_values=value, var_name=key_name
649        )
650        if sub_error_message:
651            error_message += (
652                "The following dictionary is invalid because one of its"
653                " entries is invalid:\n"
654            )
655            error_message += gp.sprint_varx(
656                var_name, var_value, gp.blank() | gp.show_type()
657            )
658            error_message += "\n"
659            error_message += sub_error_message
660            return process_error_message(error_message)
661
662    for key, value in invalid_values.items():
663        if key not in var_value:
664            continue
665        key_name = "  [" + key + "]"
666        sub_error_message = valid_value(
667            var_value[key], invalid_values=value, var_name=key_name
668        )
669        if sub_error_message:
670            error_message += (
671                "The following dictionary is invalid because one of its"
672                " entries is invalid:\n"
673            )
674            error_message += gp.sprint_varx(
675                var_name, var_value, gp.blank() | gp.show_type()
676            )
677            error_message += "\n"
678            error_message += sub_error_message
679            return process_error_message(error_message)
680
681    return process_error_message(error_message)
682
683
684def valid_program(var_value, var_name=None):
685    r"""
686    The variable value is valid if it contains the name of a program which can be located using the "which"
687    command.
688
689    Description of argument(s):
690    var_value                       The value being validated.
691    """
692
693    error_message = ""
694    rc, out_buf = gc.shell_cmd(
695        "which " + var_value, quiet=1, show_err=0, ignore_err=1
696    )
697    if rc:
698        var_name = get_var_name(var_name)
699        error_message += "The following required program could not be found"
700        error_message += " using the $PATH environment variable:\n"
701        error_message += gp.sprint_varx(var_name, var_value, gp.blank())
702        PATH = os.environ.get("PATH", "").split(":")
703        error_message += "\n"
704        error_message += gp.sprint_var(PATH)
705    return process_error_message(error_message)
706
707
708def valid_length(var_value, min_length=None, max_length=None, var_name=None):
709    r"""
710    The variable value is valid if it is an object (e.g. list, dictionary) whose length is within the
711    specified range.
712
713    Description of argument(s):
714    var_value                       The value being validated.
715    min_length                      The minimum length of the object.  If not None, the length of var_value
716                                    must be greater than or equal to min_length.
717    max_length                      The maximum length of the object.  If not None, the length of var_value
718                                    must be less than or equal to min_length.
719    """
720
721    error_message = ""
722    length = len(var_value)
723    error_message = valid_range(length, min_length, max_length)
724    if error_message:
725        var_name = get_var_name(var_name)
726        error_message = "The length of the following object is not within the"
727        error_message += " expected range:\n"
728        error_message += gp.sprint_vars(min_length, max_length)
729        error_message += gp.sprint_var(length)
730        error_message += gp.sprint_varx(var_name, var_value, gp.blank())
731        error_message += "\n"
732        return process_error_message(error_message)
733
734    return process_error_message(error_message)
735
736
737# Modify selected function docstrings by adding headers/footers.
738
739func_names = [
740    "valid_type",
741    "valid_value",
742    "valid_range",
743    "valid_integer",
744    "valid_dir_path",
745    "valid_file_path",
746    "valid_path",
747    "valid_list",
748    "valid_dict",
749    "valid_program",
750    "valid_length",
751    "valid_float",
752    "valid_date_time",
753]
754
755raw_doc_strings = {}
756
757for func_name in func_names:
758    cmd_buf = "raw_doc_strings['" + func_name + "'] = " + func_name
759    cmd_buf += ".__doc__"
760    exec(cmd_buf)
761    cmd_buf = func_name + ".__doc__ = docstring_header + " + func_name
762    cmd_buf += '.__doc__.rstrip(" \\n") + additional_args_docstring_footer'
763    exec(cmd_buf)
764