1#!/usr/bin/env python
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
11
12exit_on_error = False
13
14
15def set_exit_on_error(value):
16    r"""
17    Set the exit_on_error value to either True or False.
18
19    If exit_on_error is set, validation functions like valid_value() will exit the program on error instead
20    of returning False.
21
22    Description of argument(s):
23    value                           Value to set global exit_on_error to.
24    """
25
26    global exit_on_error
27    exit_on_error = value
28
29
30def get_var_name(var_name):
31    r"""
32    If var_name is not None, simply return its value.  Otherwise, get the variable name of the first argument
33    used to call the validation function (e.g. valid, valid_integer, etc.) and return it.
34
35    This function is designed solely for use by other functions in this file.
36
37    Example:
38
39    A programmer codes this:
40
41    valid_value(last_name)
42
43    Which results in the following call stack:
44
45    valid_value(last_name)
46      -> get_var_name(var_name)
47
48    In this example, this function will return "last_name".
49
50    Example:
51
52    err_msg = valid_value(last_name, var_name="some_other_name")
53
54    Which results in the following call stack:
55
56    valid_value(var_value, var_name="some_other_name")
57      -> get_var_name(var_name)
58
59    In this example, this function will return "some_other_name".
60
61    Description of argument(s):
62    var_name                        The name of the variable.
63    """
64
65    return var_name or gp.get_arg_name(0, 1, stack_frame_ix=3)
66
67
68def process_error_message(error_message):
69    r"""
70    Process the error_message in the manner described below.
71
72    This function is designed solely for use by other functions in this file.
73
74    NOTE: A blank error_message means that there is no error.
75
76    For the following explanations, assume the caller of this function is a function with the following
77    definition:
78    valid_value(var_value, valid_values=[], invalid_values=[], var_name=None):
79
80    If the user of valid_value() is assigning the valid_value() return value to a variable,
81    process_error_message() will simply return the error_message.  This mode of usage is illustrated by the
82    following example:
83
84    error_message = valid_value(var1)
85
86    This mode is useful for callers who wish to validate a variable and then decide for themselves what to do
87    with the error_message (e.g. raise(error_message), BuiltIn().fail(error_message), etc.).
88
89    If the user of valid_value() is NOT assigning the valid_value() return value to a variable,
90    process_error_message() will behave as follows.
91
92    First, if error_message is non-blank, it will be printed to stderr via a call to
93    gp.print_error_report(error_message).
94
95    If exit_on_error is set:
96    - If the error_message is blank, simply return.
97    - If the error_message is non-blank, exit the program with a return code of 1.
98
99    If exit_on_error is NOT set:
100    - If the error_message is blank, return True.
101    - If the error_message is non-blank, return False.
102
103    Description of argument(s):
104    error_message                   An error message.
105    """
106
107    # Determine whether the caller's caller is assigning the result to a variable.
108    l_value = gp.get_arg_name(None, -1, stack_frame_ix=3)
109    if l_value:
110        return error_message
111
112    if error_message == "":
113        if exit_on_error:
114            return
115        return True
116
117    gp.print_error_report(error_message, stack_frame_ix=4)
118    if exit_on_error:
119        exit(1)
120    return False
121
122
123# Note to programmers:  All of the validation functions in this module should follow the same basic template:
124# def valid_value(var_value, var1, var2, varn, var_name=None):
125#
126#     error_message = ""
127#     if not valid:
128#         var_name = get_var_name(var_name)
129#         error_message += "The following variable is invalid because...:\n"
130#         error_message += gp.sprint_varx(var_name, var_value, gp.blank())
131#
132#     return process_error_message(error_message)
133
134
135# The docstring header and footer will be added to each validation function's existing docstring.
136docstring_header = \
137    r"""
138    Determine whether var_value is valid, construct an error_message and call
139    process_error_message(error_message).
140
141    See the process_error_message() function defined in this module for a description of how error messages
142    are processed.
143    """
144
145additional_args_docstring_footer = \
146    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(var_name, var_value,
181                                    gp.blank() | gp.show_type())
182    error_message += "\n"
183    error_message += gp.sprint_var(required_type)
184
185    return process_error_message(error_message)
186
187
188def valid_value(var_value, valid_values=[], invalid_values=[], var_name=None):
189
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    if len_valid_values > 0:
233        # Processing the valid_values list.
234        if var_value in valid_values:
235            return process_error_message(error_message)
236        var_name = get_var_name(var_name)
237        error_message += "Invalid variable value:\n"
238        error_message += gp.sprint_varx(var_name, var_value,
239                                        gp.blank() | gp.verbose()
240                                        | gp.show_type())
241        error_message += "\n"
242        error_message += "It must be one of the following values:\n"
243        error_message += "\n"
244        error_message += gp.sprint_var(valid_values,
245                                       gp.blank() | gp.show_type())
246        return process_error_message(error_message)
247
248    if len_invalid_values == 0:
249        # Assign default value.
250        invalid_values = ["", None]
251
252    # Assertion: We have an invalid_values list.  Processing it now.
253    if var_value not in invalid_values:
254        return process_error_message(error_message)
255
256    var_name = get_var_name(var_name)
257    error_message += "Invalid variable value:\n"
258    error_message += gp.sprint_varx(var_name, var_value,
259                                    gp.blank() | gp.verbose()
260                                    | gp.show_type())
261    error_message += "\n"
262    error_message += "It must NOT be one of the following values:\n"
263    error_message += "\n"
264    error_message += gp.sprint_var(invalid_values,
265                                   gp.blank() | gp.show_type())
266    return process_error_message(error_message)
267
268
269def valid_range(var_value, lower=None, upper=None, var_name=None):
270    r"""
271    The variable value is valid if it is within the specified range.
272
273    This function can be used with any type of operands where they can have a greater than/less than
274    relationship to each other (e.g. int, float, str).
275
276    Description of argument(s):
277    var_value                       The value being validated.
278    lower                           The lower end of the range.  If not None, the var_value must be greater
279                                    than or equal to lower.
280    upper                           The upper end of the range.  If not None, the var_value must be less than
281                                    or equal to upper.
282    """
283
284    error_message = ""
285    if lower is None and upper is None:
286        return process_error_message(error_message)
287    if lower is None and var_value <= upper:
288        return process_error_message(error_message)
289    if upper is None and var_value >= lower:
290        return process_error_message(error_message)
291    if lower and upper:
292        if lower > upper:
293            var_name = get_var_name(var_name)
294            error_message += "Programmer error - the lower value is greater"
295            error_message += " than the upper value:\n"
296            error_message += gp.sprint_vars(lower, upper, fmt=gp.show_type())
297            return process_error_message(error_message)
298        if lower <= var_value <= upper:
299            return process_error_message(error_message)
300
301    var_name = get_var_name(var_name)
302    error_message += "The following variable is not within the expected"
303    error_message += " range:\n"
304    error_message += gp.sprint_varx(var_name, var_value, gp.show_type())
305    error_message += "\n"
306    error_message += "range:\n"
307    error_message += gp.sprint_vars(lower, upper, fmt=gp.show_type(), indent=2)
308    return process_error_message(error_message)
309
310
311def valid_integer(var_value, lower=None, upper=None, var_name=None):
312    r"""
313    The variable value is valid if it is an integer or can be interpreted as an integer (e.g. 7, "7", etc.).
314
315    This function also calls valid_range to make sure the integer value is within the specified range (if
316    any).
317
318    Description of argument(s):
319    var_value                       The value being validated.
320    lower                           The lower end of the range.  If not None, the var_value must be greater
321                                    than or equal to lower.
322    upper                           The upper end of the range.  If not None, the var_value must be less than
323                                    or equal to upper.
324    """
325
326    error_message = ""
327    var_name = get_var_name(var_name)
328    try:
329        var_value = int(str(var_value), 0)
330    except ValueError:
331        error_message += "Invalid integer value:\n"
332        error_message += gp.sprint_varx(var_name, var_value,
333                                        gp.blank() | gp.show_type())
334        return process_error_message(error_message)
335
336    # Check the range (if any).
337    if lower:
338        lower = int(str(lower), 0)
339    if upper:
340        upper = int(str(upper), 0)
341    error_message = valid_range(var_value, lower, upper, var_name=var_name)
342
343    return process_error_message(error_message)
344
345
346def valid_dir_path(var_value, var_name=None):
347    r"""
348    The variable value is valid if it contains the path of an existing directory.
349
350    Description of argument(s):
351    var_value                       The value being validated.
352    """
353
354    error_message = ""
355    if not os.path.isdir(str(var_value)):
356        var_name = get_var_name(var_name)
357        error_message += "The following directory does not exist:\n"
358        error_message += gp.sprint_varx(var_name, var_value, gp.blank())
359
360    return process_error_message(error_message)
361
362
363def valid_file_path(var_value, var_name=None):
364    r"""
365    The variable value is valid if it contains the path of an existing file.
366
367    Description of argument(s):
368    var_value                       The value being validated.
369    """
370
371    error_message = ""
372    if not os.path.isfile(str(var_value)):
373        var_name = get_var_name(var_name)
374        error_message += "The following file does not exist:\n"
375        error_message += gp.sprint_varx(var_name, var_value, gp.blank())
376
377    return process_error_message(error_message)
378
379
380def valid_path(var_value, var_name=None):
381    r"""
382    The variable value is valid if it contains the path of an existing file or directory.
383
384    Description of argument(s):
385    var_value                       The value being validated.
386    """
387
388    error_message = ""
389    if not (os.path.isfile(str(var_value)) or os.path.isdir(str(var_value))):
390        var_name = get_var_name(var_name)
391        error_message += "Invalid path (file or directory does not exist):\n"
392        error_message += gp.sprint_varx(var_name, var_value, gp.blank())
393
394    return process_error_message(error_message)
395
396
397def valid_list(var_value, valid_values=[], invalid_values=[],
398               required_values=[], fail_on_empty=False, var_name=None):
399    r"""
400    The variable value is valid if it is a list where each entry can be found in the valid_values list or if
401    none of its values can be found in the invalid_values list or if all of the values in the required_values
402    list can be found in var_value.
403
404    The caller may only specify one of these 3 arguments: valid_values, invalid_values, required_values.
405
406    Description of argument(s):
407    var_value                       The value being validated.
408    valid_values                    A list of valid values.  Each element in the var_value list must be equal
409                                    to one of these values to be considered valid.
410    invalid_values                  A list of invalid values.  If any element in var_value is equal to any of
411                                    the values in this argument, var_value is considered invalid.
412    required_values                 Every value in required_values must be found in var_value.  Otherwise,
413                                    var_value is considered invalid.
414    fail_on_empty                   Indicates that an empty list for the variable value should be considered
415                                    an error.
416    """
417
418    error_message = ""
419
420    # Validate this function's arguments.
421    if not (bool(len(valid_values)) ^ bool(len(invalid_values)) ^ bool(len(required_values))):
422        error_message += "Programmer error - You must provide only one of the"
423        error_message += " following: valid_values, invalid_values,"
424        error_message += " required_values.\n"
425        error_message += gp.sprint_var(invalid_values, gp.show_type())
426        error_message += gp.sprint_var(valid_values, gp.show_type())
427        error_message += gp.sprint_var(required_values, gp.show_type())
428        return process_error_message(error_message)
429
430    if type(var_value) is not list:
431        var_name = get_var_name(var_name)
432        error_message = valid_type(var_value, list, var_name=var_name)
433        if error_message:
434            return process_error_message(error_message)
435
436    if fail_on_empty and len(var_value) == 0:
437        var_name = get_var_name(var_name)
438        error_message += "Invalid empty list:\n"
439        error_message += gp.sprint_varx(var_name, var_value, gp.show_type())
440        return process_error_message(error_message)
441
442    if len(required_values):
443        found_error = 0
444        display_required_values = list(required_values)
445        for ix in range(0, len(required_values)):
446            if required_values[ix] not in var_value:
447                found_error = 1
448                display_required_values[ix] = \
449                    str(display_required_values[ix]) + "*"
450        if found_error:
451            var_name = get_var_name(var_name)
452            error_message += "The following list is invalid:\n"
453            error_message += gp.sprint_varx(var_name, var_value,
454                                            gp.blank() | gp.show_type())
455            error_message += "\n"
456            error_message += "Because some of the values in the "
457            error_message += "required_values list are not present (see"
458            error_message += " entries marked with \"*\"):\n"
459            error_message += "\n"
460            error_message += gp.sprint_varx('required_values',
461                                            display_required_values,
462                                            gp.blank() | gp.show_type())
463            error_message += "\n"
464
465        return process_error_message(error_message)
466
467    if len(invalid_values):
468        found_error = 0
469        display_var_value = list(var_value)
470        for ix in range(0, len(var_value)):
471            if var_value[ix] in invalid_values:
472                found_error = 1
473                display_var_value[ix] = str(var_value[ix]) + "*"
474
475        if found_error:
476            var_name = get_var_name(var_name)
477            error_message += "The following list is invalid (see entries"
478            error_message += " marked with \"*\"):\n"
479            error_message += gp.sprint_varx(var_name, display_var_value,
480                                            gp.blank() | gp.show_type())
481            error_message += "\n"
482            error_message += gp.sprint_var(invalid_values, gp.show_type())
483        return process_error_message(error_message)
484
485    found_error = 0
486    display_var_value = list(var_value)
487    for ix in range(0, len(var_value)):
488        if var_value[ix] not in valid_values:
489            found_error = 1
490            display_var_value[ix] = str(var_value[ix]) + "*"
491
492    if found_error:
493        var_name = get_var_name(var_name)
494        error_message += "The following list is invalid (see entries marked"
495        error_message += " with \"*\"):\n"
496        error_message += gp.sprint_varx(var_name, display_var_value,
497                                        gp.blank() | gp.show_type())
498        error_message += "\n"
499        error_message += gp.sprint_var(valid_values, gp.show_type())
500        return process_error_message(error_message)
501
502    return process_error_message(error_message)
503
504
505def valid_dict(var_value, required_keys=[], var_name=None):
506    r"""
507    The variable value is valid if it is a dictionary containing all of the required keys.
508
509    Description of argument(s):
510    var_value                       The value being validated.
511    required_keys                   A list of keys which must be found in the dictionary for it to be
512                                    considered valid.
513    """
514
515    error_message = ""
516    missing_keys = list(set(required_keys) - set(var_value.keys()))
517    if len(missing_keys) > 0:
518        var_name = get_var_name(var_name)
519        error_message += "The following dictionary is invalid because it is"
520        error_message += " missing required keys:\n"
521        error_message += gp.sprint_varx(var_name, var_value,
522                                        gp.blank() | gp.show_type())
523        error_message += "\n"
524        error_message += gp.sprint_var(missing_keys, gp.show_type())
525    return process_error_message(error_message)
526
527
528def valid_program(var_value, var_name=None):
529    r"""
530    The variable value is valid if it contains the name of a program which can be located using the "which"
531    command.
532
533    Description of argument(s):
534    var_value                       The value being validated.
535    """
536
537    error_message = ""
538    rc, out_buf = gc.shell_cmd("which " + var_value, quiet=1, show_err=0,
539                               ignore_err=1)
540    if rc:
541        var_name = get_var_name(var_name)
542        error_message += "The following required program could not be found"
543        error_message += " using the $PATH environment variable:\n"
544        error_message += gp.sprint_varx(var_name, var_value, gp.blank())
545        PATH = os.environ.get("PATH", "").split(":")
546        error_message += "\n"
547        error_message += gp.sprint_var(PATH)
548    return process_error_message(error_message)
549
550
551def valid_length(var_value, min_length=None, max_length=None, var_name=None):
552    r"""
553    The variable value is valid if it is an object (e.g. list, dictionary) whose length is within the
554    specified range.
555
556    Description of argument(s):
557    var_value                       The value being validated.
558    min_length                      The minimum length of the object.  If not None, the length of var_value
559                                    must be greater than or equal to min_length.
560    max_length                      The maximum length of the object.  If not None, the length of var_value
561                                    must be less than or equal to min_length.
562    """
563
564    error_message = ""
565    length = len(var_value)
566    error_message = valid_range(length, min_length, max_length)
567    if error_message:
568        var_name = get_var_name(var_name)
569        error_message = "The length of the following object is not within the"
570        error_message += " expected range:\n"
571        error_message += gp.sprint_vars(min_length, max_length)
572        error_message += gp.sprint_var(length)
573        error_message += gp.sprint_varx(var_name, var_value, gp.blank())
574        error_message += "\n"
575        return process_error_message(error_message)
576
577    return process_error_message(error_message)
578
579
580# Modify selected function docstrings by adding headers/footers.
581
582func_names = [
583    "valid_type", "valid_value", "valid_range", "valid_integer",
584    "valid_dir_path", "valid_file_path", "valid_path", "valid_list",
585    "valid_dict", "valid_program", "valid_length"
586]
587
588raw_doc_strings = {}
589
590for func_name in func_names:
591    cmd_buf = "raw_doc_strings['" + func_name + "'] = " + func_name
592    cmd_buf += ".__doc__"
593    exec(cmd_buf)
594    cmd_buf = func_name + ".__doc__ = docstring_header + " + func_name
595    cmd_buf += ".__doc__.rstrip(\" \\n\") + additional_args_docstring_footer"
596    exec(cmd_buf)
597