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