Source code for solidipes.validators.validator

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 of {obj.path}: {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