from abc import ABC, abstractmethod
from typing import Any, Callable, Generic, Optional, TypeVar, Union
from ..utils import solidipes_logging as logging
logger = logging.getLogger()
T = TypeVar("T")
[docs]
class Validator(ABC, Generic[T]):
"""Abstract class for validators."""
def __init__(self, description: str, mandatory: bool = True, manually_settable: bool = False):
self.description = description
self.mandatory = mandatory
self.manually_settable = manually_settable
self._errors = []
self._result = None
@property
def name(self):
return self.__class__.__name__
[docs]
@abstractmethod
def _validate(self, obj: Optional[T] = None) -> Union[bool, Any]:
"""Validate an object and optionally return a boolean. Can raise exceptions."""
pass
[docs]
def validate(self, obj: Optional[T] = None) -> "ValidationResult":
"""Validate an object and return a ValidationResult, also catching exceptions."""
try:
from solidipes.loaders.file import File
if isinstance(obj, File) and not obj.is_cache_invalid() and id(self) in obj._validation_results:
v_res = obj._validation_results[id(self)]
return ValidationResult(self, v_res["_result"], v_res["_errors"])
self._errors = []
self._result = self._validate(obj)
if len(self._errors) > 0:
self._result = False
if self._result is None:
self._result = True
if isinstance(obj, File):
obj._validation_results[id(self)] = {"_result": self._result, "_errors": list(self._errors)}
return ValidationResult(self, self._result, self._errors)
except Exception as e:
logger.error(f"Exception during validation: {e}")
exc_info = e.__traceback__
while exc_info.tb_next:
exc_info = exc_info.tb_next
filename = exc_info.tb_frame.f_code.co_filename
line_number = exc_info.tb_lineno
logger.error(f"{filename}:{line_number}: {e} {e.__traceback__}")
return ValidationResult(self, False, self._errors + [str(e)])
def __call__(self, *args, **kwargs) -> "ValidationResult":
"""Call the `validate` method."""
return self.validate(*args, **kwargs)
[docs]
def add_validation_error(self, errors):
"""Add a validation error to the current validation context."""
if isinstance(errors, str):
errors = [errors]
self._errors.extend(errors)
[docs]
def has_validation_errors(self) -> bool:
"""Check if there are validation errors in the current validation context."""
return len(self._validation_errors) > 0
[docs]
class ValidationResult:
"""Result of a validation, evaluable as a boolean, and containing the list of errors and warnings."""
def __init__(self, validator: Validator, valid: bool, errors: list[str]) -> None:
self.validator = validator
self.valid = valid
#: List of errors and warnings
self.errors = errors
if len(self.errors) == 0 and not self.valid:
self.errors.append(f'"{validator.description}" is not fulfilled')
def __bool__(self) -> bool:
return self.valid
def __str__(self) -> str:
return f"{self.validator.description}: {self.valid}" + (
"\n- " + "\n- ".join(self.errors) if self.errors else ""
)
def __repr__(self) -> str:
return str(self)
[docs]
def validator(description: str, mandatory: bool = True, manually_settable=False) -> Callable:
"""Decorator to add a Validator class attribute to another class.
The decorated method should return None or a list of strings with the errors. The method can also raise exceptions.
"""
def decorator(func: Callable[[T], Union[bool, Any]]) -> Validator[T]:
class NewValidator(Validator):
def _validate(self, obj: T) -> Any:
obj._current_validator = self
self._result = func(obj)
obj._current_validator = None
return self._result
def __get__(self, obj: Optional[T], obj_class: type[T]) -> Union[Validator[T], bool, Any]:
if obj is None:
return self
return lambda: self._validate(obj)
name = "".join(word.capitalize() for word in func.__name__.split("_")) + "Validator"
NewValidator.__name__ = name
NewValidator.__qualname__ = name
validator = NewValidator(description=description, mandatory=mandatory, manually_settable=manually_settable)
validator.__doc__ = func.__doc__
return validator
return decorator