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