#!/usr/bin/env python3 r""" This module provides validation functions like valid_value(), valid_integer(), etc. """ import datetime import os import func_args as fa import gen_cmd as gc import gen_print as gp exit_on_error = False def set_exit_on_error(value): r""" Set the exit_on_error value to either True or False. If exit_on_error is set, validation functions like valid_value() will exit the program on error instead of returning False. Description of argument(s): value Value to set global exit_on_error to. """ global exit_on_error exit_on_error = value def get_var_name(var_name): r""" If var_name is not None, simply return its value. Otherwise, get the variable name of the first argument used to call the validation function (e.g. valid, valid_integer, etc.) and return it. This function is designed solely for use by other functions in this file. Example: A programmer codes this: valid_value(last_name) Which results in the following call stack: valid_value(last_name) -> get_var_name(var_name) In this example, this function will return "last_name". Example: err_msg = valid_value(last_name, var_name="some_other_name") Which results in the following call stack: valid_value(var_value, var_name="some_other_name") -> get_var_name(var_name) In this example, this function will return "some_other_name". Description of argument(s): var_name The name of the variable. """ return var_name or gp.get_arg_name(0, 1, stack_frame_ix=3) def process_error_message(error_message): r""" Process the error_message in the manner described below. This function is designed solely for use by other functions in this file. NOTE: A blank error_message means that there is no error. For the following explanations, assume the caller of this function is a function with the following definition: valid_value(var_value, valid_values=[], invalid_values=[], var_name=None): If the user of valid_value() is assigning the valid_value() return value to a variable, process_error_message() will simply return the error_message. This mode of usage is illustrated by the following example: error_message = valid_value(var1) This mode is useful for callers who wish to validate a variable and then decide for themselves what to do with the error_message (e.g. raise(error_message), BuiltIn().fail(error_message), etc.). If the user of valid_value() is NOT assigning the valid_value() return value to a variable, process_error_message() will behave as follows. First, if error_message is non-blank, it will be printed to stderr via a call to gp.print_error_report(error_message). If exit_on_error is set: - If the error_message is blank, simply return. - If the error_message is non-blank, exit the program with a return code of 1. If exit_on_error is NOT set: - If the error_message is blank, return True. - If the error_message is non-blank, return False. Description of argument(s): error_message An error message. """ # Determine whether the caller's caller is assigning the result to a variable. l_value = gp.get_arg_name(None, -1, stack_frame_ix=3) if l_value: return error_message if error_message == "": if exit_on_error: return return True gp.print_error_report(error_message, stack_frame_ix=4) if exit_on_error: exit(1) return False # Note to programmers: All of the validation functions in this module should follow the same basic template: # def valid_value(var_value, var1, var2, varn, var_name=None): # # error_message = "" # if not valid: # var_name = get_var_name(var_name) # error_message += "The following variable is invalid because...:\n" # error_message += gp.sprint_varx(var_name, var_value, gp.blank()) # # return process_error_message(error_message) # The docstring header and footer will be added to each validation function's existing docstring. docstring_header = r""" Determine whether var_value is valid, construct an error_message and call process_error_message(error_message). See the process_error_message() function defined in this module for a description of how error messages are processed. """ additional_args_docstring_footer = r""" var_name The name of the variable whose value is passed in var_value. For the general case, this argument is unnecessary as this function can figure out the var_name. This is provided for Robot callers in which case, this function lacks the ability to determine the variable name. """ def valid_type(var_value, required_type, var_name=None): r""" The variable value is valid if it is of the required type. Examples: valid_type(var1, int) valid_type(var1, (list, dict)) Description of argument(s): var_value The value being validated. required_type A type or a tuple of types (e.g. str, int, etc.). """ error_message = "" if type(required_type) is tuple: if type(var_value) in required_type: return process_error_message(error_message) else: if type(var_value) is required_type: return process_error_message(error_message) # If we get to this point, the validation has failed. var_name = get_var_name(var_name) error_message += "Invalid variable type:\n" error_message += gp.sprint_varx( var_name, var_value, gp.blank() | gp.show_type() ) error_message += "\n" error_message += gp.sprint_var(required_type) return process_error_message(error_message) def valid_value(var_value, valid_values=[], invalid_values=[], var_name=None): r""" The variable value is valid if it is either contained in the valid_values list or if it is NOT contained in the invalid_values list. If the caller specifies nothing for either of these 2 arguments, invalid_values will be initialized to ['', None]. This is a good way to fail on variables which contain blank values. It is illegal to specify both valid_values and invalid values. Example: var1 = '' valid_value(var1) This code would fail because var1 is blank and the default value for invalid_values is ['', None]. Example: var1 = 'yes' valid_value(var1, valid_values=['yes', 'true']) This code would pass. Description of argument(s): var_value The value being validated. valid_values A list of valid values. The variable value must be equal to one of these values to be considered valid. invalid_values A list of invalid values. If the variable value is equal to any of these, it is considered invalid. """ error_message = "" # Validate this function's arguments. len_valid_values = len(valid_values) len_invalid_values = len(invalid_values) if len_valid_values > 0 and len_invalid_values > 0: error_message += "Programmer error - You must provide either an" error_message += " invalid_values list or a valid_values" error_message += " list but NOT both:\n" error_message += gp.sprint_var(invalid_values) error_message += gp.sprint_var(valid_values) return process_error_message(error_message) error_message = valid_type(valid_values, list, var_name="valid_values") if error_message: return process_error_message(error_message) error_message = valid_type(invalid_values, list, var_name="invalid_values") if error_message: return process_error_message(error_message) if len_valid_values > 0: # Processing the valid_values list. if var_value in valid_values: return process_error_message(error_message) var_name = get_var_name(var_name) error_message += "Invalid variable value:\n" error_message += gp.sprint_varx( var_name, var_value, gp.blank() | gp.verbose() | gp.show_type() ) error_message += "\n" error_message += "It must be one of the following values:\n" error_message += "\n" error_message += gp.sprint_var( valid_values, gp.blank() | gp.show_type() ) return process_error_message(error_message) if len_invalid_values == 0: # Assign default value. invalid_values = ["", None] # Assertion: We have an invalid_values list. Processing it now. if var_value not in invalid_values: return process_error_message(error_message) var_name = get_var_name(var_name) error_message += "Invalid variable value:\n" error_message += gp.sprint_varx( var_name, var_value, gp.blank() | gp.verbose() | gp.show_type() ) error_message += "\n" error_message += "It must NOT be any of the following values:\n" error_message += "\n" error_message += gp.sprint_var(invalid_values, gp.blank() | gp.show_type()) return process_error_message(error_message) def valid_range(var_value, lower=None, upper=None, var_name=None): r""" The variable value is valid if it is within the specified range. This function can be used with any type of operands where they can have a greater than/less than relationship to each other (e.g. int, float, str). Description of argument(s): var_value The value being validated. lower The lower end of the range. If not None, the var_value must be greater than or equal to lower. upper The upper end of the range. If not None, the var_value must be less than or equal to upper. """ error_message = "" if lower is None and upper is None: return process_error_message(error_message) if lower is None and var_value <= upper: return process_error_message(error_message) if upper is None and var_value >= lower: return process_error_message(error_message) if lower is not None and upper is not None: if lower > upper: var_name = get_var_name(var_name) error_message += "Programmer error - the lower value is greater" error_message += " than the upper value:\n" error_message += gp.sprint_vars(lower, upper, fmt=gp.show_type()) return process_error_message(error_message) if lower <= var_value <= upper: return process_error_message(error_message) var_name = get_var_name(var_name) error_message += "The following variable is not within the expected" error_message += " range:\n" error_message += gp.sprint_varx(var_name, var_value, gp.show_type()) error_message += "\n" error_message += "range:\n" error_message += gp.sprint_vars(lower, upper, fmt=gp.show_type(), indent=2) return process_error_message(error_message) def valid_integer(var_value, lower=None, upper=None, var_name=None): r""" The variable value is valid if it is an integer or can be interpreted as an integer (e.g. 7, "7", etc.). This function also calls valid_range to make sure the integer value is within the specified range (if any). Description of argument(s): var_value The value being validated. lower The lower end of the range. If not None, the var_value must be greater than or equal to lower. upper The upper end of the range. If not None, the var_value must be less than or equal to upper. """ error_message = "" var_name = get_var_name(var_name) try: var_value = int(str(var_value), 0) except ValueError: error_message += "Invalid integer value:\n" error_message += gp.sprint_varx( var_name, var_value, gp.blank() | gp.show_type() ) return process_error_message(error_message) # Check the range (if any). if lower: lower = int(str(lower), 0) if upper: upper = int(str(upper), 0) error_message = valid_range(var_value, lower, upper, var_name=var_name) return process_error_message(error_message) def valid_float(var_value, lower=None, upper=None, var_name=None): r""" The variable value is valid if it is a floating point value or can be interpreted as a floating point value (e.g. 7.5, "7.5", etc.). This function also calls valid_range to make sure the float value is within the specified range (if any). Description of argument(s): var_value The value being validated. lower The lower end of the range. If not None, the var_value must be greater than or equal to lower. upper The upper end of the range. If not None, the var_value must be less than or equal to upper. """ error_message = "" var_name = get_var_name(var_name) try: var_value = float(str(var_value)) except ValueError: error_message += "Invalid float value:\n" error_message += gp.sprint_varx( var_name, var_value, gp.blank() | gp.show_type() ) return process_error_message(error_message) # Check the range (if any). if lower: lower = float(str(lower)) if upper: upper = float(str(upper)) error_message = valid_range(var_value, lower, upper, var_name=var_name) return process_error_message(error_message) def valid_date_time(var_value, var_name=None): r""" The variable value is valid if it can be interpreted as a date/time (e.g. "14:49:49.981", "tomorrow", etc.) by the linux date command. Description of argument(s): var_value The value being validated. """ error_message = "" rc, out_buf = gc.shell_cmd( "date -d '" + str(var_value) + "'", quiet=1, show_err=0, ignore_err=1 ) if rc: var_name = get_var_name(var_name) error_message += "Invalid date/time value:\n" error_message += gp.sprint_varx( var_name, var_value, gp.blank() | gp.show_type() ) return process_error_message(error_message) return process_error_message(error_message) def valid_dir_path(var_value, var_name=None): r""" The variable value is valid if it contains the path of an existing directory. Description of argument(s): var_value The value being validated. """ error_message = "" if not os.path.isdir(str(var_value)): var_name = get_var_name(var_name) error_message += "The following directory does not exist:\n" error_message += gp.sprint_varx(var_name, var_value, gp.blank()) return process_error_message(error_message) def valid_file_path(var_value, var_name=None): r""" The variable value is valid if it contains the path of an existing file. Description of argument(s): var_value The value being validated. """ error_message = "" if not os.path.isfile(str(var_value)): var_name = get_var_name(var_name) error_message += "The following file does not exist:\n" error_message += gp.sprint_varx(var_name, var_value, gp.blank()) return process_error_message(error_message) def valid_path(var_value, var_name=None): r""" The variable value is valid if it contains the path of an existing file or directory. Description of argument(s): var_value The value being validated. """ error_message = "" if not (os.path.isfile(str(var_value)) or os.path.isdir(str(var_value))): var_name = get_var_name(var_name) error_message += "Invalid path (file or directory does not exist):\n" error_message += gp.sprint_varx(var_name, var_value, gp.blank()) return process_error_message(error_message) def valid_list( var_value, valid_values=[], invalid_values=[], required_values=[], fail_on_empty=False, var_name=None, ): r""" The variable value is valid if it is a list where each entry can be found in the valid_values list or if none of its values can be found in the invalid_values list or if all of the values in the required_values list can be found in var_value. The caller may only specify one of these 3 arguments: valid_values, invalid_values, required_values. Description of argument(s): var_value The value being validated. valid_values A list of valid values. Each element in the var_value list must be equal to one of these values to be considered valid. invalid_values A list of invalid values. If any element in var_value is equal to any of the values in this argument, var_value is considered invalid. required_values Every value in required_values must be found in var_value. Otherwise, var_value is considered invalid. fail_on_empty Indicates that an empty list for the variable value should be considered an error. """ error_message = "" # Validate this function's arguments. if not ( bool(len(valid_values)) ^ bool(len(invalid_values)) ^ bool(len(required_values)) ): error_message += "Programmer error - You must provide only one of the" error_message += " following: valid_values, invalid_values," error_message += " required_values.\n" error_message += gp.sprint_var(invalid_values, gp.show_type()) error_message += gp.sprint_var(valid_values, gp.show_type()) error_message += gp.sprint_var(required_values, gp.show_type()) return process_error_message(error_message) if type(var_value) is not list: var_name = get_var_name(var_name) error_message = valid_type(var_value, list, var_name=var_name) if error_message: return process_error_message(error_message) if fail_on_empty and len(var_value) == 0: var_name = get_var_name(var_name) error_message += "Invalid empty list:\n" error_message += gp.sprint_varx(var_name, var_value, gp.show_type()) return process_error_message(error_message) if len(required_values): found_error = 0 display_required_values = list(required_values) for ix in range(0, len(required_values)): if required_values[ix] not in var_value: found_error = 1 display_required_values[ix] = ( str(display_required_values[ix]) + "*" ) if found_error: var_name = get_var_name(var_name) error_message += "The following list is invalid:\n" error_message += gp.sprint_varx( var_name, var_value, gp.blank() | gp.show_type() ) error_message += "\n" error_message += "Because some of the values in the " error_message += "required_values list are not present (see" error_message += ' entries marked with "*"):\n' error_message += "\n" error_message += gp.sprint_varx( "required_values", display_required_values, gp.blank() | gp.show_type(), ) error_message += "\n" return process_error_message(error_message) if len(invalid_values): found_error = 0 display_var_value = list(var_value) for ix in range(0, len(var_value)): if var_value[ix] in invalid_values: found_error = 1 display_var_value[ix] = str(var_value[ix]) + "*" if found_error: var_name = get_var_name(var_name) error_message += "The following list is invalid (see entries" error_message += ' marked with "*"):\n' error_message += gp.sprint_varx( var_name, display_var_value, gp.blank() | gp.show_type() ) error_message += "\n" error_message += gp.sprint_var(invalid_values, gp.show_type()) return process_error_message(error_message) found_error = 0 display_var_value = list(var_value) for ix in range(0, len(var_value)): if var_value[ix] not in valid_values: found_error = 1 display_var_value[ix] = str(var_value[ix]) + "*" if found_error: var_name = get_var_name(var_name) error_message += "The following list is invalid (see entries marked" error_message += ' with "*"):\n' error_message += gp.sprint_varx( var_name, display_var_value, gp.blank() | gp.show_type() ) error_message += "\n" error_message += gp.sprint_var(valid_values, gp.show_type()) return process_error_message(error_message) return process_error_message(error_message) def valid_dict( var_value, required_keys=[], valid_values={}, invalid_values={}, var_name=None, ): r""" The dictionary variable value is valid if it contains all required keys and each entry passes the valid_value() call. Examples: person_record = {'last_name': 'Jones', 'first_name': 'John'} valid_values = {'last_name': ['Doe', 'Jones', 'Johnson'], 'first_name': ['John', 'Mary']} invalid_values = {'last_name': ['Manson', 'Hitler', 'Presley'], 'first_name': ['Mickey', 'Goofy']} valid_dict(person_record, valid_values=valid_values) valid_dict(person_record, invalid_values=invalid_values) Description of argument(s): var_value The value being validated. required_keys A list of keys which must be found in the dictionary for it to be considered valid. valid_values A dictionary whose entries correspond to the entries in var_value. Each value in valid_values is itself a valid_values list for the corresponding value in var_value. For any var_value[key] to be considered valid, its value must be found in valid_values[key]. invalid_values A dictionary whose entries correspond to the entries in var_value. Each value in invalid_values is itself an invalid_values list for the corresponding value in var_value. For any var_value[key] to be considered valid, its value must NOT be found in invalid_values[key]. """ error_message = "" missing_keys = list(set(required_keys) - set(var_value.keys())) if len(missing_keys) > 0: var_name = get_var_name(var_name) error_message += "The following dictionary is invalid because it is" error_message += " missing required keys:\n" error_message += gp.sprint_varx( var_name, var_value, gp.blank() | gp.show_type() ) error_message += "\n" error_message += gp.sprint_var(missing_keys, gp.show_type()) return process_error_message(error_message) var_name = get_var_name(var_name) if len(valid_values): keys = valid_values.keys() error_message = valid_dict( var_value, required_keys=keys, var_name=var_name ) if error_message: return process_error_message(error_message) for key, value in valid_values.items(): key_name = " [" + key + "]" sub_error_message = valid_value( var_value[key], valid_values=value, var_name=key_name ) if sub_error_message: error_message += ( "The following dictionary is invalid because one of its" " entries is invalid:\n" ) error_message += gp.sprint_varx( var_name, var_value, gp.blank() | gp.show_type() ) error_message += "\n" error_message += sub_error_message return process_error_message(error_message) for key, value in invalid_values.items(): if key not in var_value: continue key_name = " [" + key + "]" sub_error_message = valid_value( var_value[key], invalid_values=value, var_name=key_name ) if sub_error_message: error_message += ( "The following dictionary is invalid because one of its" " entries is invalid:\n" ) error_message += gp.sprint_varx( var_name, var_value, gp.blank() | gp.show_type() ) error_message += "\n" error_message += sub_error_message return process_error_message(error_message) return process_error_message(error_message) def valid_program(var_value, var_name=None): r""" The variable value is valid if it contains the name of a program which can be located using the "which" command. Description of argument(s): var_value The value being validated. """ error_message = "" rc, out_buf = gc.shell_cmd( "which " + var_value, quiet=1, show_err=0, ignore_err=1 ) if rc: var_name = get_var_name(var_name) error_message += "The following required program could not be found" error_message += " using the $PATH environment variable:\n" error_message += gp.sprint_varx(var_name, var_value, gp.blank()) PATH = os.environ.get("PATH", "").split(":") error_message += "\n" error_message += gp.sprint_var(PATH) return process_error_message(error_message) def valid_length(var_value, min_length=None, max_length=None, var_name=None): r""" The variable value is valid if it is an object (e.g. list, dictionary) whose length is within the specified range. Description of argument(s): var_value The value being validated. min_length The minimum length of the object. If not None, the length of var_value must be greater than or equal to min_length. max_length The maximum length of the object. If not None, the length of var_value must be less than or equal to min_length. """ error_message = "" length = len(var_value) error_message = valid_range(length, min_length, max_length) if error_message: var_name = get_var_name(var_name) error_message = "The length of the following object is not within the" error_message += " expected range:\n" error_message += gp.sprint_vars(min_length, max_length) error_message += gp.sprint_var(length) error_message += gp.sprint_varx(var_name, var_value, gp.blank()) error_message += "\n" return process_error_message(error_message) return process_error_message(error_message) # Modify selected function docstrings by adding headers/footers. func_names = [ "valid_type", "valid_value", "valid_range", "valid_integer", "valid_dir_path", "valid_file_path", "valid_path", "valid_list", "valid_dict", "valid_program", "valid_length", "valid_float", "valid_date_time", ] raw_doc_strings = {} for func_name in func_names: cmd_buf = "raw_doc_strings['" + func_name + "'] = " + func_name cmd_buf += ".__doc__" exec(cmd_buf) cmd_buf = func_name + ".__doc__ = docstring_header + " + func_name cmd_buf += '.__doc__.rstrip(" \\n") + additional_args_docstring_footer' exec(cmd_buf)