validation.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. """
  2. Input validation for a `Buffer`.
  3. (Validators will be called before accepting input.)
  4. """
  5. from __future__ import annotations
  6. from abc import ABCMeta, abstractmethod
  7. from typing import Callable
  8. from prompt_toolkit.eventloop import run_in_executor_with_context
  9. from .document import Document
  10. from .filters import FilterOrBool, to_filter
  11. __all__ = [
  12. "ConditionalValidator",
  13. "ValidationError",
  14. "Validator",
  15. "ThreadedValidator",
  16. "DummyValidator",
  17. "DynamicValidator",
  18. ]
  19. class ValidationError(Exception):
  20. """
  21. Error raised by :meth:`.Validator.validate`.
  22. :param cursor_position: The cursor position where the error occurred.
  23. :param message: Text.
  24. """
  25. def __init__(self, cursor_position: int = 0, message: str = "") -> None:
  26. super().__init__(message)
  27. self.cursor_position = cursor_position
  28. self.message = message
  29. def __repr__(self) -> str:
  30. return f"{self.__class__.__name__}(cursor_position={self.cursor_position!r}, message={self.message!r})"
  31. class Validator(metaclass=ABCMeta):
  32. """
  33. Abstract base class for an input validator.
  34. A validator is typically created in one of the following two ways:
  35. - Either by overriding this class and implementing the `validate` method.
  36. - Or by passing a callable to `Validator.from_callable`.
  37. If the validation takes some time and needs to happen in a background
  38. thread, this can be wrapped in a :class:`.ThreadedValidator`.
  39. """
  40. @abstractmethod
  41. def validate(self, document: Document) -> None:
  42. """
  43. Validate the input.
  44. If invalid, this should raise a :class:`.ValidationError`.
  45. :param document: :class:`~prompt_toolkit.document.Document` instance.
  46. """
  47. pass
  48. async def validate_async(self, document: Document) -> None:
  49. """
  50. Return a `Future` which is set when the validation is ready.
  51. This function can be overloaded in order to provide an asynchronous
  52. implementation.
  53. """
  54. try:
  55. self.validate(document)
  56. except ValidationError:
  57. raise
  58. @classmethod
  59. def from_callable(
  60. cls,
  61. validate_func: Callable[[str], bool],
  62. error_message: str = "Invalid input",
  63. move_cursor_to_end: bool = False,
  64. ) -> Validator:
  65. """
  66. Create a validator from a simple validate callable. E.g.:
  67. .. code:: python
  68. def is_valid(text):
  69. return text in ['hello', 'world']
  70. Validator.from_callable(is_valid, error_message='Invalid input')
  71. :param validate_func: Callable that takes the input string, and returns
  72. `True` if the input is valid input.
  73. :param error_message: Message to be displayed if the input is invalid.
  74. :param move_cursor_to_end: Move the cursor to the end of the input, if
  75. the input is invalid.
  76. """
  77. return _ValidatorFromCallable(validate_func, error_message, move_cursor_to_end)
  78. class _ValidatorFromCallable(Validator):
  79. """
  80. Validate input from a simple callable.
  81. """
  82. def __init__(
  83. self, func: Callable[[str], bool], error_message: str, move_cursor_to_end: bool
  84. ) -> None:
  85. self.func = func
  86. self.error_message = error_message
  87. self.move_cursor_to_end = move_cursor_to_end
  88. def __repr__(self) -> str:
  89. return f"Validator.from_callable({self.func!r})"
  90. def validate(self, document: Document) -> None:
  91. if not self.func(document.text):
  92. if self.move_cursor_to_end:
  93. index = len(document.text)
  94. else:
  95. index = 0
  96. raise ValidationError(cursor_position=index, message=self.error_message)
  97. class ThreadedValidator(Validator):
  98. """
  99. Wrapper that runs input validation in a thread.
  100. (Use this to prevent the user interface from becoming unresponsive if the
  101. input validation takes too much time.)
  102. """
  103. def __init__(self, validator: Validator) -> None:
  104. self.validator = validator
  105. def validate(self, document: Document) -> None:
  106. self.validator.validate(document)
  107. async def validate_async(self, document: Document) -> None:
  108. """
  109. Run the `validate` function in a thread.
  110. """
  111. def run_validation_thread() -> None:
  112. return self.validate(document)
  113. await run_in_executor_with_context(run_validation_thread)
  114. class DummyValidator(Validator):
  115. """
  116. Validator class that accepts any input.
  117. """
  118. def validate(self, document: Document) -> None:
  119. pass # Don't raise any exception.
  120. class ConditionalValidator(Validator):
  121. """
  122. Validator that can be switched on/off according to
  123. a filter. (This wraps around another validator.)
  124. """
  125. def __init__(self, validator: Validator, filter: FilterOrBool) -> None:
  126. self.validator = validator
  127. self.filter = to_filter(filter)
  128. def validate(self, document: Document) -> None:
  129. # Call the validator only if the filter is active.
  130. if self.filter():
  131. self.validator.validate(document)
  132. class DynamicValidator(Validator):
  133. """
  134. Validator class that can dynamically returns any Validator.
  135. :param get_validator: Callable that returns a :class:`.Validator` instance.
  136. """
  137. def __init__(self, get_validator: Callable[[], Validator | None]) -> None:
  138. self.get_validator = get_validator
  139. def validate(self, document: Document) -> None:
  140. validator = self.get_validator() or DummyValidator()
  141. validator.validate(document)
  142. async def validate_async(self, document: Document) -> None:
  143. validator = self.get_validator() or DummyValidator()
  144. await validator.validate_async(document)