validation_element.py 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. from typing import Any, Awaitable, Callable, Dict, Optional, Union
  2. from typing_extensions import Self
  3. from ... import background_tasks, helpers
  4. from .value_element import ValueElement
  5. ValidationFunction = Callable[[Any], Union[Optional[str], Awaitable[Optional[str]]]]
  6. ValidationDict = Dict[str, Callable[[Any], bool]]
  7. class ValidationElement(ValueElement):
  8. def __init__(self, validation: Optional[Union[ValidationFunction, ValidationDict]], **kwargs: Any) -> None:
  9. self._validation = validation
  10. self._auto_validation = True
  11. self._error: Optional[str] = None
  12. super().__init__(**kwargs)
  13. self._props['error'] = None if validation is None else False # NOTE: reserve bottom space for error message
  14. @property
  15. def validation(self) -> Optional[Union[ValidationFunction, ValidationDict]]:
  16. """The validation function or dictionary of validation functions."""
  17. return self._validation
  18. @validation.setter
  19. def validation(self, validation: Optional[Union[ValidationFunction, ValidationDict]]) -> None:
  20. """Sets the validation function or dictionary of validation functions.
  21. :param validation: validation function or dictionary of validation functions (``None`` to disable validation)
  22. """
  23. self._validation = validation
  24. self.validate(return_result=False)
  25. @property
  26. def error(self) -> Optional[str]:
  27. """The latest error message from the validation functions."""
  28. return self._error
  29. @error.setter
  30. def error(self, error: Optional[str]) -> None:
  31. """Sets the error message.
  32. :param error: The optional error message
  33. """
  34. new_error_prop = None if self.validation is None else (error is not None)
  35. if self._error == error and self._props['error'] == new_error_prop:
  36. return
  37. self._error = error
  38. self._props['error'] = new_error_prop
  39. self._props['error-message'] = error
  40. self.update()
  41. def validate(self, *, return_result: bool = True) -> bool:
  42. """Validate the current value and set the error message if necessary.
  43. For async validation functions, ``return_result`` must be set to ``False`` and the return value will be ``True``,
  44. independently of the validation result which is evaluated in the background.
  45. :param return_result: whether to return the result of the validation (default: ``True``)
  46. :return: whether the validation was successful (always ``True`` for async validation functions)
  47. """
  48. if helpers.is_coroutine_function(self._validation):
  49. async def await_error():
  50. assert callable(self._validation)
  51. result = self._validation(self.value)
  52. assert isinstance(result, Awaitable)
  53. self.error = await result
  54. if return_result:
  55. raise NotImplementedError('The validate method cannot return results for async validation functions.')
  56. background_tasks.create(await_error())
  57. return True
  58. if callable(self._validation):
  59. result = self._validation(self.value)
  60. assert not isinstance(result, Awaitable)
  61. self.error = result
  62. return self.error is None
  63. if isinstance(self._validation, dict):
  64. for message, check in self._validation.items():
  65. if not check(self.value):
  66. self.error = message
  67. return False
  68. self.error = None
  69. return True
  70. def without_auto_validation(self) -> Self:
  71. """Disable automatic validation on value change."""
  72. self._auto_validation = False
  73. return self
  74. def _handle_value_change(self, value: Any) -> None:
  75. super()._handle_value_change(value)
  76. if self._auto_validation:
  77. self.validate(return_result=False)