def _validate_expected_list(list_expected): """Validate an expected List and raises exceptions. The syntax of the expected list is validated. Examples that will raise errors: Variable length should be at beginning or end. [1, 2, "...", 4, 5] [1, 2, "(3)", 4, 5] Additional arguments are not accepted [1, 2, 3, 4, "5..."] [1, 2, 3, 4, "5(5)"] params ------ list_expected: Expected List to validate against raises ------ ValueError: ValueError will be raised if an rule violation is found """ validator = 0 for exp in list_expected: if validator == 1 and not ("(" in str(exp) or "..." in str(exp)): raise WxSyntaxError( "Optional dimensions in the expected " "shape should only stand at the end/beginning.") if validator == 2: raise WxSyntaxError('After "..." should not be another dimension.') if "..." in str(exp): if "..." != exp: raise WxSyntaxError( f'"..." should not have additional properties:' f" {exp} was found.") validator = 2 elif "(" in str(exp): val = re.search(r"\((.*)\)", exp) if (val is None or len(val.group(1)) + 2 != len(exp) or not _is_range_format_valid(val.group(1))): raise WxSyntaxError( f'Invalid optional dimension format. Correct format is "(_)", but ' f" {exp} was found.") validator = 1 elif not _is_range_format_valid(str(exp)): raise WxSyntaxError( f"{exp} is an invalid range format." f"Consult the documentation for a list of all valid options")
def _compare_tag_version(instance_tag: str, tagname: str): """Compare ASDF tag-strings with flexible version syntax. Parameters ---------- instance_tag: the full ASDF tag to validate tagname: tag string with custom version syntax to validate against Returns ------- bool """ if instance_tag is None: return True if instance_tag.startswith("tag:yaml.org"): # test for python builtins return instance_tag == tagname instance_tag_version = [ int(v) for v in instance_tag.rpartition("-")[-1].split(".") ] tag_parts = tagname.rpartition("-") tag_uri = tag_parts[0] tag_version = [v for v in tag_parts[-1].split(".")] if tag_version == ["*"]: version_compatible = True elif all([vstr.isdigit() for vstr in tag_version]): vnum = [int(vstr) for vstr in tag_version] version_compatible = all( [v[0] == v[1] for v in zip(vnum, instance_tag_version)]) else: raise WxSyntaxError(f"Unknown wx_tag syntax {tagname}") if (not instance_tag.startswith(tag_uri)) or (not version_compatible): return False return True
def _custom_shape_validator(dict_test: Dict[str, Any], dict_expected: Dict[str, Any]): """Validate dimensions which are stored in two dictionaries dict_test and dict_expected. Syntax for the dict_expected: ----------------------------- Items with arrays with each value having the following Syntax: 1) 3 : an integer indicates a fix dimension for the same item in dict_test 2) "~", ":" or None : this string indicates a single dimension of arbitrary length. 3) "..." : this string indicates an arbitrary number of dimensions of arbitrary length. Can be optional. 4) "2~4" : this string indicates a single dimension of length 2, 3 or 4. This has to be ascending or you can give an unlimited interval limit like this "2~" which would indicate a single dimension of length greater then 1. 5) "n" : this indicates a single dimension fixed to a letter. Any letter or combination of letters should work The letter will be compared to the same letter in all the arrays in the dict_expected. 6) (x) : parenthesis indicates that the dict_test does not need to have this dimension. This can NOT be combined with 3) or the None from 2). Parameters ---------- dict_test: dictionary to test against dict_expected: dictionary with the expected values raises ------ ValueError: when dict_expected does violate against the Syntax rules returns ------- False when any dimension mismatch occurs dict_values Dictionary - keys: variable names in the validation schemes. values: values of the validation schemes. """ dict_values = {} # catch single shape definitions if isinstance(dict_expected, list): # get the shape of current shape = _get_instance_shape(dict_test) if not shape: raise ValidationError( f"Could not determine shape in instance {dict_test}.") list_test, list_expected = _prepare_list(shape, dict_expected) _validate_expected_list(list_expected) _dict_values = _compare_lists(list_test, list_expected) if _dict_values is False: raise ValidationError( f"Shape {list_test[::-1]} does not match requirement " f"{list_expected[::-1]}") return _dict_values elif isinstance(dict_expected, dict): for key, item in dict_expected.items(): # allow optional syntax _optional = False if key.startswith("(") and key.endswith(")"): key = key[1:-1] _optional = True if len(key) == 0: raise WxSyntaxError("wx_shape entry undefined") # test shapes if key in dict_test: # go one level deeper in the dictionary _dict_values = _custom_shape_validator(dict_test[key], item) elif _optional: _dict_values = {} else: raise ValidationError( f"Could not access key '{key}' in instance {dict_test}.") for key in _dict_values: if key not in dict_values: dict_values[key] = _dict_values[key] elif dict_values[key] != _dict_values[key]: return False else: raise WxSyntaxError( f"Found an incorrect object: {type(dict_expected)}. " "Should be a dict or list.") return dict_values
def _compare(_int, exp_string): """Compare helper of two strings for _custom_shape_validator. An integer and an expected string are compared so that the string either contains a ":" and thus describes an interval or a string consisting of numbers. So if our integer is within the interval or equal to the described number, True is returned. The interval can be open, in that there is no number left or right of the ":" symbol. Examples: --------- _int = 5 exp_string = "5" -> True _int = 5 exp_string = ":" -> True Open interval: _int = 5 exp_string = "3:" -> True _int = 5 exp_string = "4" -> False _int = 5 exp_string = "6:8" -> False Parameters ---------- _int: Integer exp_string: String with the expected dimension Returns ------- bool True or False """ if _int < 0: raise WxSyntaxError("Negative dimension found") if ":" in exp_string: ranges = exp_string.split(":") if ranges[0] == "": ranges[0] = 0 elif ranges[0].isnumeric(): ranges[0] = int(ranges[0]) else: raise WxSyntaxError(f"Non numeric character in range {exp_string}") if ranges[1] == "": ranges[1] = _int elif ranges[1].isnumeric(): ranges[1] = int(ranges[1]) else: raise WxSyntaxError(f"Non numeric character in range {exp_string}") if ranges[0] > ranges[1]: raise WxSyntaxError( f"The range should not be descending in {exp_string}") return int(ranges[0]) <= _int <= int(ranges[1]) else: return _int == int(exp_string)