validation.py 5.7 KB

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