radubulimac's picture
fix import issue
2d876d1
"""Contains the interface class :class:`.BaseComplexPrompt` for more complex prompts and the mocked document class :class:`.FakeDocument`."""
import shutil
from dataclasses import dataclass
from typing import Any, Callable, List, Optional, Tuple, Union
from prompt_toolkit.application import Application
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.filters.base import Condition, FilterOrBool
from prompt_toolkit.key_binding.key_bindings import KeyHandlerCallable
from prompt_toolkit.keys import Keys
from InquirerPy.base.simple import BaseSimplePrompt
from InquirerPy.enum import INQUIRERPY_KEYBOARD_INTERRUPT
from InquirerPy.utils import (
InquirerPySessionResult,
InquirerPyStyle,
InquirerPyValidate,
)
@dataclass
class FakeDocument:
"""A fake `prompt_toolkit` document class.
Work around to allow non-buffer type :class:`~prompt_toolkit.layout.UIControl` to use
:class:`~prompt_toolkit.validation.Validator`.
Args:
text: Content to be validated.
cursor_position: Fake cursor position.
"""
text: str
cursor_position: int = 0
class BaseComplexPrompt(BaseSimplePrompt):
"""A base class to create a more complex prompt that will involve :class:`~prompt_toolkit.application.Application`.
Note:
This class does not create :class:`~prompt_toolkit.layout.Layout` nor :class:`~prompt_toolkit.application.Application`,
it only contains the necessary attributes and helper functions to be consumed.
Note:
Use :class:`~InquirerPy.base.BaseListPrompt` to create a complex list prompt which involves multiple choices. It has
more methods and helper function implemented.
See Also:
:class:`~InquirerPy.base.BaseListPrompt`
:class:`~InquirerPy.prompts.fuzzy.FuzzyPrompt`
"""
def __init__(
self,
message: Union[str, Callable[[InquirerPySessionResult], str]],
style: Optional[InquirerPyStyle] = None,
border: bool = False,
vi_mode: bool = False,
qmark: str = "?",
amark: str = "?",
instruction: str = "",
long_instruction: str = "",
transformer: Optional[Callable[[Any], Any]] = None,
filter: Optional[Callable[[Any], Any]] = None,
validate: Optional[InquirerPyValidate] = None,
invalid_message: str = "Invalid input",
wrap_lines: bool = True,
raise_keyboard_interrupt: bool = True,
mandatory: bool = True,
mandatory_message: str = "Mandatory prompt",
session_result: Optional[InquirerPySessionResult] = None,
) -> None:
super().__init__(
message=message,
style=style,
vi_mode=vi_mode,
qmark=qmark,
amark=amark,
instruction=instruction,
transformer=transformer,
filter=filter,
invalid_message=invalid_message,
validate=validate,
wrap_lines=wrap_lines,
raise_keyboard_interrupt=raise_keyboard_interrupt,
mandatory=mandatory,
mandatory_message=mandatory_message,
session_result=session_result,
)
self._invalid_message = invalid_message
self._rendered = False
self._invalid = False
self._loading = False
self._application: Application
self._long_instruction = long_instruction
self._border = border
self._height_offset = 2 # prev prompt result + current prompt question
if self._border:
self._height_offset += 2
if self._long_instruction:
self._height_offset += 1
self._validation_window_bottom_offset = 0 if not self._long_instruction else 1
if self._wrap_lines:
self._validation_window_bottom_offset += (
self.extra_long_instruction_line_count
)
self._is_vim_edit = Condition(lambda: self._editing_mode == EditingMode.VI)
self._is_invalid = Condition(lambda: self._invalid)
self._is_displaying_long_instruction = Condition(
lambda: self._long_instruction != ""
)
def _redraw(self) -> None:
"""Redraw the application UI."""
self._application.invalidate()
def register_kb(
self, *keys: Union[Keys, str], filter: FilterOrBool = True
) -> Callable[[KeyHandlerCallable], KeyHandlerCallable]:
"""Decorate keybinding registration function.
Ensure that the `invalid` state is cleared on next keybinding entered.
"""
kb_dec = super().register_kb(*keys, filter=filter)
def decorator(func: KeyHandlerCallable) -> KeyHandlerCallable:
@kb_dec
def executable(event):
if self._invalid:
self._invalid = False
func(event)
return executable
return decorator
def _exception_handler(self, _, context) -> None:
"""Set exception handler for the event loop.
Skip the question and raise exception.
Args:
loop: Current event loop.
context: Exception context.
"""
self._status["answered"] = True
self._status["result"] = INQUIRERPY_KEYBOARD_INTERRUPT
self._status["skipped"] = True
self._application.exit(exception=context["exception"])
def _after_render(self, app: Optional[Application]) -> None:
"""Run after the :class:`~prompt_toolkit.application.Application` is rendered/updated.
Since this function is fired up on each render, adding a check on `self._rendered` to
process logics that should only run once.
Set event loop exception handler here, since its guaranteed that the event loop is running
in `_after_render`.
"""
if not self._rendered:
self._rendered = True
self._keybinding_factory()
self._on_rendered(app)
def _set_error(self, message: str) -> None:
"""Set error message and set invalid state.
Args:
message: Error message to display.
"""
self._invalid_message = message
self._invalid = True
def _get_error_message(self) -> List[Tuple[str, str]]:
"""Obtain the error message dynamically.
Returns:
FormattedText in list of tuple format.
"""
return [
(
"class:validation-toolbar",
self._invalid_message,
)
]
def _on_rendered(self, _: Optional[Application]) -> None:
"""Run once after the UI is rendered. Acts like `ComponentDidMount`."""
pass
def _get_prompt_message(self) -> List[Tuple[str, str]]:
"""Get the prompt message to display.
Returns:
Formatted text in list of tuple format.
"""
pre_answer = (
"class:instruction",
" %s " % self.instruction if self.instruction else " ",
)
post_answer = ("class:answer", " %s" % self.status["result"])
return super()._get_prompt_message(pre_answer, post_answer)
def _run(self) -> Any:
"""Run the application."""
return self.application.run()
async def _run_async(self) -> None:
"""Run the application asynchronously."""
return await self.application.run_async()
@property
def application(self) -> Application:
"""Get the application.
:class:`.BaseComplexPrompt` requires :attr:`.BaseComplexPrompt._application` to be defined since this class
doesn't implement :class:`~prompt_toolkit.layout.Layout` and :class:`~prompt_toolkit.application.Application`.
Raises:
NotImplementedError: When `self._application` is not defined.
"""
if not self._application:
raise NotImplementedError
return self._application
@application.setter
def application(self, value: Application) -> None:
self._application = value
@property
def height_offset(self) -> int:
"""int: Height offset to apply."""
if not self._wrap_lines:
return self._height_offset
return self.extra_line_count + self._height_offset
@property
def total_message_length(self) -> int:
"""int: Total length of the message."""
total_message_length = 0
if self._qmark:
total_message_length += len(self._qmark)
total_message_length += 1 # Extra space if qmark is present
total_message_length += len(str(self._message))
total_message_length += 1 # Extra space between message and instruction
total_message_length += len(str(self._instruction))
if self._instruction:
total_message_length += 1 # Extra space behind the instruction
return total_message_length
@property
def extra_message_line_count(self) -> int:
"""int: Get the extra lines created caused by line wrapping.
Minus 1 on the totoal message length as we only want the extra line.
24 // 24 will equal to 1 however we only want the value to be 1 when we have 25 char
which will create an extra line.
"""
term_width, _ = shutil.get_terminal_size()
return (self.total_message_length - 1) // term_width
@property
def extra_long_instruction_line_count(self) -> int:
"""int: Get the extra lines created caused by line wrapping.
See Also:
:attr:`.BaseComplexPrompt.extra_message_line_count`
"""
if self._long_instruction:
term_width, _ = shutil.get_terminal_size()
return (len(self._long_instruction) - 1) // term_width
else:
return 0
@property
def extra_line_count(self) -> int:
"""Get the extra lines created caused by line wrapping.
Used mainly to calculate how much additional offset should be applied when getting
the height.
Returns:
Total extra lines created due to line wrapping.
"""
result = 0
# message wrap
result += self.extra_message_line_count
# long instruction wrap
result += self.extra_long_instruction_line_count
return result