From 6b532e8a70fdd10d6da9e6499d34c9f277b31d88 Mon Sep 17 00:00:00 2001 From: PatchOfScotland Date: Fri, 31 Mar 2023 12:51:12 +0200 Subject: [PATCH] added more hinting to pattern validation messages --- core/base_pattern.py | 6 +- core/correctness/validation.py | 161 ++++++++++++++++++++++----------- patterns/file_event_pattern.py | 56 ++++++++++-- 3 files changed, 163 insertions(+), 60 deletions(-) diff --git a/core/base_pattern.py b/core/base_pattern.py index cbd32da..fbb6073 100644 --- a/core/base_pattern.py +++ b/core/base_pattern.py @@ -59,7 +59,11 @@ class BasePattern: """Validation check for 'name' variable from main constructor. Is automatically called during initialisation. This does not need to be overridden by child classes.""" - valid_string(name, VALID_PATTERN_NAME_CHARS) + valid_string( + name, + VALID_PATTERN_NAME_CHARS, + hint="BasePattern._is_valid_name.name" + ) def _is_valid_recipe(self, recipe:Any)->None: """Validation check for 'recipe' variable from main constructor. Must diff --git a/core/correctness/validation.py b/core/correctness/validation.py index 348c080..0f161ee 100644 --- a/core/correctness/validation.py +++ b/core/correctness/validation.py @@ -48,11 +48,18 @@ def check_type(variable:Any, expected_type:Type, alt_types:List[Type]=[], msg = f"Expected type(s) are '{type_list}', got {type(variable)}" raise TypeError(msg) -def check_callable(call:Any)->None: +def check_callable(call:Any, hint:str="")->None: """Checks if a given variable is a callable function. Raises TypeError if not.""" if not callable(call): - raise TypeError(f"Given object '{call}' is not a callable function") + if hint: + raise TypeError( + f"Given object '{call}' by '{hint}' is not a callable function" + ) + else: + raise TypeError( + f"Given object '{call}' is not a callable function" + ) def check_implementation(child_func, parent_class): """Checks if the given function has been overridden from the one inherited @@ -84,68 +91,82 @@ def check_script(script:Any): for line in script: check_type(line, str) -def valid_string(variable:str, valid_chars:str, min_length:int=1)->None: +def valid_string(variable:str, valid_chars:str, min_length:int=1, hint:str="" + )->None: """Checks that all characters in a given string are present in a provided list of characters. Will raise an ValueError if unexpected character is encountered.""" - check_type(variable, str) - check_type(valid_chars, str) + check_type(variable, str, hint=hint) + check_type(valid_chars, str, hint=hint) # Check string is long enough if len(variable) < min_length: - raise ValueError ( - f"String '{variable}' is too short. Minimum length is {min_length}" - ) + if hint: + msg = f"String '{variable}' for '{hint}' is too short. Minimum " \ + f"length is {min_length}" + else: + msg = f"String '{variable}' is too short. Minimum length is " \ + f"{min_length}" + raise ValueError (msg) # Check each char is acceptable for char in variable: if char not in valid_chars: - raise ValueError( - "Invalid character '%s'. Only valid characters are: " - "%s" % (char, valid_chars) - ) + if hint : + msg = f"Invalid character '{char}' in '{hint}'. Only valid " \ + f"characters are: {valid_chars}" + else: + msg = f"Invalid character '{char}'. Only valid characters " \ + f"are: {valid_chars}" + raise ValueError(msg) + def valid_dict(variable:Dict[Any, Any], key_type:Type, value_type:Type, required_keys:List[Any]=[], optional_keys:List[Any]=[], - strict:bool=True, min_length:int=1)->None: + strict:bool=True, min_length:int=1, hint:str="")->None: """Checks that a given dictionary is valid. Key and Value types are enforced, as are required and optional keys. Will raise ValueError, TypeError or KeyError depending on the problem encountered.""" # Validate inputs - check_type(variable, Dict) - check_type(key_type, Type, alt_types=[_SpecialForm]) - check_type(value_type, Type, alt_types=[_SpecialForm]) - check_type(required_keys, list) - check_type(optional_keys, list) - check_type(strict, bool) + check_type(variable, Dict, hint=hint) + check_type(key_type, Type, alt_types=[_SpecialForm], hint=hint) + check_type(value_type, Type, alt_types=[_SpecialForm], hint=hint) + check_type(required_keys, list, hint=hint) + check_type(optional_keys, list, hint=hint) + check_type(strict, bool, hint=hint) + + if hint: + hint = f"in '{hint}' " # Check dict meets minimum length if len(variable) < min_length: - raise ValueError(f"Dictionary '{variable}' is below minimum length of " - f"{min_length}") + raise ValueError( + f"Dictionary '{variable}' {hint}is below minimum length of " + f"{min_length}" + ) # Check key and value types for k, v in variable.items(): if key_type != Any and not isinstance(k, key_type): - raise TypeError(f"Key {k} had unexpected type '{type(k)}' " + raise TypeError(f"Key {k} {hint}had unexpected type '{type(k)}' " f"rather than expected '{key_type}' in dict '{variable}'") if value_type != Any and not isinstance(v, value_type): - raise TypeError(f"Value {v} had unexpected type '{type(v)}' " + raise TypeError(f"Value {v} {hint}had unexpected type '{type(v)}' " f"rather than expected '{value_type}' in dict '{variable}'") # Check all required keys present for rk in required_keys: if rk not in variable.keys(): raise KeyError(f"Missing required key '{rk}' from dict " - f"'{variable}'") + f"'{variable}' {hint}.") # If strict checking, enforce that only required and optional keys are # present if strict: for k in variable.keys(): if k not in required_keys and k not in optional_keys: - raise ValueError(f"Unexpected key '{k}' should not be present " - f"in dict '{variable}'") + raise ValueError(f"Unexpected key '{k}' {hint}should not be " + f"present in dict '{variable}'") def valid_list(variable:List[Any], entry_type:Type, alt_types:List[Type]=[], min_length:int=1, hint:str="")->None: @@ -155,8 +176,13 @@ def valid_list(variable:List[Any], entry_type:Type, # Check length meets minimum if len(variable) < min_length: - raise ValueError(f"List '{variable}' is too short. Should be at least " - f"of length {min_length}") + if hint: + msg = f"List '{variable}' is too short in {hint}. Should be at " \ + f"least of length {min_length}" + else: + msg = f"List '{variable}' is too short. Should be at least " \ + f"of length {min_length}" + raise ValueError(msg) # Check type of each value for n, entry in enumerate(variable): @@ -167,59 +193,92 @@ def valid_list(variable:List[Any], entry_type:Type, check_type(entry, entry_type, alt_types=alt_types) def valid_path(variable:str, allow_base:bool=False, extension:str="", - min_length:int=1): + min_length:int=1, hint:str=""): """Check that a given string expresses a valid path.""" - valid_string(variable, VALID_PATH_CHARS, min_length=min_length) + valid_string(variable, VALID_PATH_CHARS, min_length=min_length, hint=hint) # Check we aren't given a root path if not allow_base and variable.startswith(sep): - raise ValueError(f"Cannot accept path '{variable}'. Must be relative.") + if hint: + msg = f"Cannot accept path '{variable}' in '{hint}'. Must be " \ + "relative." + else: + msg = f"Cannot accept path '{variable}'. Must be relative." + raise ValueError(msg) # Check path contains a valid extension if extension and not variable.endswith(extension): - raise ValueError(f"Path '{variable}' does not have required " - f"extension '{extension}'.") + if hint: + msg = f"Path '{variable}' in '{hint}' does not have required " \ + f"extension '{extension}'." + else: + msg = f"Path '{variable}' does not have required extension " \ + f"'{extension}'." + raise ValueError(msg) def valid_existing_file_path(variable:str, allow_base:bool=False, - extension:str=""): + extension:str="", hint:str=""): """Check the given string is a path to an existing file.""" # Check that the string is a path - valid_path(variable, allow_base=allow_base, extension=extension) + valid_path(variable, allow_base=allow_base, extension=extension, hint=hint) # Check the path exists if not exists(variable): - raise FileNotFoundError( - f"Requested file path '{variable}' does not exist.") + if hint: + msg = f"Requested file path '{variable}' in '{hint}' does not " \ + "exist." + else: + msg = f"Requested file path '{variable}' does not exist." + raise FileNotFoundError(msg) # Check it is a file if not isfile(variable): - raise ValueError( - f"Requested file '{variable}' is not a file.") + if hint: + msg = f"Requested file '{variable}' in '{hint}' is not a file." + else: + msg = f"Requested file '{variable}' is not a file." + raise ValueError(msg) -def valid_dir_path(variable:str, must_exist:bool=False, allow_base:bool=False - )->None: +def valid_dir_path(variable:str, must_exist:bool=False, allow_base:bool=False, + hint:str="")->None: """Check the given string is a valid directory path, either to an existing one or a location that could contain one.""" # Check that the string is a path - valid_path(variable, allow_base=allow_base, extension="") + valid_path(variable, allow_base=allow_base, extension="", hint=hint) # Check the path exists does_exist = exists(variable) if must_exist and not does_exist: - raise FileNotFoundError( - f"Requested dir path '{variable}' does not exist.") + if hint: + msg = f"Requested dir path '{variable}' in '{hint}' does not " \ + "exist." + else: + msg = f"Requested dir path '{variable}' does not exist." + raise FileNotFoundError(msg) # Check it is a directory if does_exist and not isdir(variable): - raise ValueError( - f"Requested dir '{variable}' is not a directory.") + if hint: + msg = f"Requested dir '{variable}' in '{hint}' is not a directory." + else: + msg = f"Requested dir '{variable}' is not a directory." + raise ValueError() -def valid_non_existing_path(variable:str, allow_base:bool=False): +def valid_non_existing_path(variable:str, allow_base:bool=False, hint:str="" + )->None: """Check the given string is a path to something that does not exist.""" # Check that the string is a path - valid_path(variable, allow_base=allow_base, extension="") + valid_path(variable, allow_base=allow_base, extension="", hint=hint) # Check the path does not exist if exists(variable): - raise ValueError(f"Requested path '{variable}' already exists.") + if hint: + msg = f"Requested path '{variable}' in '{hint}' already exists." + else: + msg = f"Requested path '{variable}' already exists." + raise ValueError(msg) # Check that any intermediate directories exist if dirname(variable) and not exists(dirname(variable)): - raise ValueError( - f"Route to requested path '{variable}' does not exist.") + if hint: + msg = f"Route to requested path '{variable}' in '{hint}' does " \ + "not exist." + else: + msg = f"Route to requested path '{variable}' does not exist." + raise ValueError(msg) # TODO add validation for requirement functions \ No newline at end of file diff --git a/patterns/file_event_pattern.py b/patterns/file_event_pattern.py index df95371..93bb587 100644 --- a/patterns/file_event_pattern.py +++ b/patterns/file_event_pattern.py @@ -65,7 +65,12 @@ class FileEventPattern(BasePattern): def _is_valid_triggering_path(self, triggering_path:str)->None: """Validation check for 'triggering_path' variable from main constructor.""" - valid_string(triggering_path, VALID_PATH_CHARS+'*', min_length=1) + valid_string( + triggering_path, + VALID_PATH_CHARS+'*', + min_length=1, + hint="FileEventPattern._is_valid_triggering_path.triggering_path" + ) if len(triggering_path) < 1: raise ValueError ( f"triggiering path '{triggering_path}' is too short. " @@ -75,30 +80,65 @@ class FileEventPattern(BasePattern): def _is_valid_triggering_file(self, triggering_file:str)->None: """Validation check for 'triggering_file' variable from main constructor.""" - valid_string(triggering_file, VALID_VARIABLE_NAME_CHARS) + valid_string( + triggering_file, + VALID_VARIABLE_NAME_CHARS, + hint="FileEventPattern._is_valid_triggering_file.triggering_file" + ) def _is_valid_recipe(self, recipe:str)->None: """Validation check for 'recipe' variable from main constructor. Called within parent BasePattern constructor.""" - valid_string(recipe, VALID_RECIPE_NAME_CHARS) + valid_string( + recipe, + VALID_RECIPE_NAME_CHARS, + hint="FileEventPattern._is_valid_recipe.recipe" + ) def _is_valid_parameters(self, parameters:Dict[str,Any])->None: """Validation check for 'parameters' variable from main constructor. Called within parent BasePattern constructor.""" - valid_dict(parameters, str, Any, strict=False, min_length=0) + valid_dict( + parameters, + str, + Any, + strict=False, + min_length=0, + hint="FileEventPattern._is_valid_parameters.parameters" + ) for k in parameters.keys(): - valid_string(k, VALID_VARIABLE_NAME_CHARS) + valid_string( + k, + VALID_VARIABLE_NAME_CHARS, + hint=f"FileEventPattern._is_valid_parameters.parameters[{k}]" + ) def _is_valid_output(self, outputs:Dict[str,str])->None: """Validation check for 'output' variable from main constructor. Called within parent BasePattern constructor.""" - valid_dict(outputs, str, str, strict=False, min_length=0) + valid_dict( + outputs, + str, + str, + strict=False, + min_length=0, + hint="FileEventPattern._is_valid_outputs.outputs" + ) for k in outputs.keys(): - valid_string(k, VALID_VARIABLE_NAME_CHARS) + valid_string( + k, + VALID_VARIABLE_NAME_CHARS, + hint=f"FileEventPattern._is_valid_outputs.outputs[{k}]" + ) def _is_valid_event_mask(self, event_mask)->None: """Validation check for 'event_mask' variable from main constructor.""" - valid_list(event_mask, str, min_length=1) + valid_list( + event_mask, + str, + min_length=1, + hint="FileEventPattern._is_valid_event_mask.event_mask" + ) for mask in event_mask: if mask not in FILE_EVENTS: raise ValueError(f"Invalid event mask '{mask}'. Valid are: "