100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
from collections.abc import Awaitable
|
|
from typing import Any, Callable, Optional, Union
|
|
|
|
from typing_extensions import Self
|
|
|
|
from ... import background_tasks, helpers
|
|
from .value_element import ValueElement
|
|
|
|
ValidationFunction = Callable[[Any], Union[Optional[str], Awaitable[Optional[str]]]]
|
|
ValidationDict = dict[str, Callable[[Any], bool]]
|
|
|
|
|
|
class ValidationElement(ValueElement):
|
|
|
|
def __init__(self, validation: Optional[Union[ValidationFunction, ValidationDict]], **kwargs: Any) -> None:
|
|
self._validation = validation
|
|
self._auto_validation = True
|
|
self._error: Optional[str] = None
|
|
super().__init__(**kwargs)
|
|
self._props['error'] = None if validation is None else False # NOTE: reserve bottom space for error message
|
|
|
|
@property
|
|
def validation(self) -> Optional[Union[ValidationFunction, ValidationDict]]:
|
|
"""The validation function or dictionary of validation functions."""
|
|
return self._validation
|
|
|
|
@validation.setter
|
|
def validation(self, validation: Optional[Union[ValidationFunction, ValidationDict]]) -> None:
|
|
"""Sets the validation function or dictionary of validation functions.
|
|
|
|
:param validation: validation function or dictionary of validation functions (``None`` to disable validation)
|
|
"""
|
|
self._validation = validation
|
|
self.validate(return_result=False)
|
|
|
|
@property
|
|
def error(self) -> Optional[str]:
|
|
"""The latest error message from the validation functions."""
|
|
return self._error
|
|
|
|
@error.setter
|
|
def error(self, error: Optional[str]) -> None:
|
|
"""Sets the error message.
|
|
|
|
:param error: The optional error message
|
|
"""
|
|
new_error_prop = None if self.validation is None else (error is not None)
|
|
if self._error == error and self._props['error'] == new_error_prop:
|
|
return
|
|
self._error = error
|
|
self._props['error'] = new_error_prop
|
|
self._props['error-message'] = error
|
|
|
|
def validate(self, *, return_result: bool = True) -> bool:
|
|
"""Validate the current value and set the error message if necessary.
|
|
|
|
For async validation functions, ``return_result`` must be set to ``False`` and the return value will be ``True``,
|
|
independently of the validation result which is evaluated in the background.
|
|
|
|
*Updated in version 2.7.0: Added support for async validation functions.*
|
|
|
|
:param return_result: whether to return the result of the validation (default: ``True``)
|
|
:return: whether the validation was successful (always ``True`` for async validation functions)
|
|
"""
|
|
if helpers.is_coroutine_function(self._validation):
|
|
async def await_error():
|
|
assert callable(self._validation)
|
|
result = self._validation(self.value)
|
|
assert isinstance(result, Awaitable)
|
|
self.error = await result
|
|
if return_result:
|
|
raise NotImplementedError('The validate method cannot return results for async validation functions.')
|
|
background_tasks.create(await_error(), name=f'validate {self.id}')
|
|
return True
|
|
|
|
if callable(self._validation):
|
|
result = self._validation(self.value)
|
|
assert not isinstance(result, Awaitable)
|
|
self.error = result
|
|
return self.error is None
|
|
|
|
if isinstance(self._validation, dict):
|
|
for message, check in self._validation.items():
|
|
if not check(self.value):
|
|
self.error = message
|
|
return False
|
|
|
|
self.error = None
|
|
return True
|
|
|
|
def without_auto_validation(self) -> Self:
|
|
"""Disable automatic validation on value change."""
|
|
self._auto_validation = False
|
|
return self
|
|
|
|
def _handle_value_change(self, value: Any) -> None:
|
|
super()._handle_value_change(value)
|
|
if self._auto_validation:
|
|
self.validate(return_result=False)
|