Shofel2_T124_python/venv/lib/python3.10/site-packages/questionary/prompts/common.py

561 lines
18 KiB
Python
Raw Normal View History

2024-05-25 16:45:07 +00:00
import inspect
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
from prompt_toolkit import PromptSession
from prompt_toolkit.filters import Always
from prompt_toolkit.filters import Condition
from prompt_toolkit.filters import IsDone
from prompt_toolkit.layout import ConditionalContainer
from prompt_toolkit.layout import FormattedTextControl
from prompt_toolkit.layout import HSplit
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout import Window
from prompt_toolkit.styles import Style
from prompt_toolkit.validation import ValidationError
from prompt_toolkit.validation import Validator
from questionary.constants import DEFAULT_SELECTED_POINTER
from questionary.constants import DEFAULT_STYLE
from questionary.constants import INDICATOR_SELECTED
from questionary.constants import INDICATOR_UNSELECTED
from questionary.constants import INVALID_INPUT
# This is a cut-down version of `prompt_toolkit.formatted_text.AnyFormattedText`
# which does not exist in v2 of prompt_toolkit
FormattedText = Union[
str,
List[Tuple[str, str]],
List[Tuple[str, str, Callable[[Any], None]]],
None,
]
class Choice:
"""One choice in a :meth:`select`, :meth:`rawselect` or :meth:`checkbox`.
Args:
title: Text shown in the selection list.
value: Value returned, when the choice is selected. If this argument
is `None` or unset, then the value of `title` is used.
disabled: If set, the choice can not be selected by the user. The
provided text is used to explain, why the selection is
disabled.
checked: Preselect this choice when displaying the options.
shortcut_key: Key shortcut used to select this item.
"""
title: FormattedText
"""Display string for the choice"""
value: Optional[Any]
"""Value of the choice"""
disabled: Optional[str]
"""Whether the choice can be selected"""
checked: Optional[bool]
"""Whether the choice is initially selected"""
shortcut_key: Optional[str]
"""A shortcut key for the choice"""
def __init__(
self,
title: FormattedText,
value: Optional[Any] = None,
disabled: Optional[str] = None,
checked: Optional[bool] = False,
shortcut_key: Optional[Union[str, bool]] = True,
) -> None:
self.disabled = disabled
self.title = title
self.checked = checked if checked is not None else False
if value is not None:
self.value = value
elif isinstance(title, list):
self.value = "".join([token[1] for token in title])
else:
self.value = title
if shortcut_key is not None:
if isinstance(shortcut_key, bool):
self.auto_shortcut = shortcut_key
self.shortcut_key = None
else:
self.shortcut_key = str(shortcut_key)
self.auto_shortcut = False
else:
self.shortcut_key = None
self.auto_shortcut = True
@staticmethod
def build(c: Union[str, "Choice", Dict[str, Any]]) -> "Choice":
"""Create a choice object from different representations.
Args:
c: Either a :obj:`str`, :class:`Choice` or :obj:`dict` with
``name``, ``value``, ``disabled``, ``checked`` and
``key`` properties.
Returns:
An instance of the :class:`Choice` object.
"""
if isinstance(c, Choice):
return c
elif isinstance(c, str):
return Choice(c, c)
else:
return Choice(
c.get("name"),
c.get("value"),
c.get("disabled", None),
c.get("checked"),
c.get("key"),
)
def get_shortcut_title(self):
if self.shortcut_key is None:
return "-) "
else:
return "{}) ".format(self.shortcut_key)
class Separator(Choice):
"""Used to space/separate choices group."""
default_separator: str = "-" * 15
"""The default separator used if none is specified"""
line: str
"""The string being used as a separator"""
def __init__(self, line: Optional[str] = None) -> None:
"""Create a separator in a list.
Args:
line: Text to be displayed in the list, by default uses ``---``.
"""
self.line = line or self.default_separator
super().__init__(self.line, None, "-")
class InquirerControl(FormattedTextControl):
SHORTCUT_KEYS = [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
]
choices: List[Choice]
default: Optional[Union[str, Choice, Dict[str, Any]]]
selected_options: List[Any]
use_indicator: bool
use_shortcuts: bool
use_arrow_keys: bool
pointer: Optional[str]
pointed_at: int
is_answered: bool
def __init__(
self,
choices: Sequence[Union[str, Choice, Dict[str, Any]]],
default: Optional[Union[str, Choice, Dict[str, Any]]] = None,
pointer: Optional[str] = DEFAULT_SELECTED_POINTER,
use_indicator: bool = True,
use_shortcuts: bool = False,
show_selected: bool = False,
use_arrow_keys: bool = True,
initial_choice: Optional[Union[str, Choice, Dict[str, Any]]] = None,
**kwargs: Any,
):
self.use_indicator = use_indicator
self.use_shortcuts = use_shortcuts
self.show_selected = show_selected
self.use_arrow_keys = use_arrow_keys
self.default = default
self.pointer = pointer
if isinstance(default, Choice):
default = default.value
choices_values = [
choice.value for choice in choices if isinstance(choice, Choice)
]
if (
default is not None
and default not in choices
and default not in choices_values
):
raise ValueError(
f"Invalid `default` value passed. The value (`{default}`) "
f"does not exist in the set of choices. Please make sure the "
f"default value is one of the available choices."
)
if initial_choice is None:
pointed_at = None
elif initial_choice in choices:
pointed_at = choices.index(initial_choice)
elif initial_choice in choices_values:
for k, choice in enumerate(choices):
if isinstance(choice, Choice):
if choice.value == initial_choice:
pointed_at = k
break
else:
raise ValueError(
f"Invalid `initial_choice` value passed. The value "
f"(`{initial_choice}`) does not exist in "
f"the set of choices. Please make sure the initial value is "
f"one of the available choices."
)
self.is_answered = False
self.choices = []
self.submission_attempted = False
self.error_message = None
self.selected_options = []
self._init_choices(choices, pointed_at)
self._assign_shortcut_keys()
super().__init__(self._get_choice_tokens, **kwargs)
if not self.is_selection_valid():
raise ValueError(
f"Invalid 'initial_choice' value ('{initial_choice}'). "
f"It must be a selectable value."
)
def _is_selected(self, choice: Choice):
if isinstance(self.default, Choice):
compare_default = self.default == choice
else:
compare_default = self.default == choice.value
return choice.checked or compare_default and self.default is not None
def _assign_shortcut_keys(self):
available_shortcuts = self.SHORTCUT_KEYS[:]
# first, make sure we do not double assign a shortcut
for c in self.choices:
if c.shortcut_key is not None:
if c.shortcut_key in available_shortcuts:
available_shortcuts.remove(c.shortcut_key)
else:
raise ValueError(
"Invalid shortcut '{}'"
"for choice '{}'. Shortcuts "
"should be single characters or numbers. "
"Make sure that all your shortcuts are "
"unique.".format(c.shortcut_key, c.title)
)
shortcut_idx = 0
for c in self.choices:
if c.auto_shortcut and not c.disabled:
c.shortcut_key = available_shortcuts[shortcut_idx]
shortcut_idx += 1
if shortcut_idx == len(available_shortcuts):
break # fail gracefully if we run out of shortcuts
def _init_choices(
self,
choices: Sequence[Union[str, Choice, Dict[str, Any]]],
pointed_at: Optional[int],
):
# helper to convert from question format to internal format
self.choices = []
if pointed_at is not None:
self.pointed_at = pointed_at
for i, c in enumerate(choices):
choice = Choice.build(c)
if self._is_selected(choice):
self.selected_options.append(choice.value)
if pointed_at is None and not choice.disabled:
# find the first (available) choice
self.pointed_at = pointed_at = i
self.choices.append(choice)
@property
def choice_count(self) -> int:
return len(self.choices)
def _get_choice_tokens(self):
tokens = []
def append(index: int, choice: Choice):
# use value to check if option has been selected
selected = choice.value in self.selected_options
if index == self.pointed_at:
if self.pointer is not None:
tokens.append(("class:pointer", " {} ".format(self.pointer)))
else:
tokens.append(("class:text", " " * 3))
tokens.append(("[SetCursorPosition]", ""))
else:
pointer_length = len(self.pointer) if self.pointer is not None else 1
tokens.append(("class:text", " " * (2 + pointer_length)))
if isinstance(choice, Separator):
tokens.append(("class:separator", "{}".format(choice.title)))
elif choice.disabled: # disabled
if isinstance(choice.title, list):
tokens.append(
("class:selected" if selected else "class:disabled", "- ")
)
tokens.extend(choice.title)
else:
tokens.append(
(
"class:selected" if selected else "class:disabled",
"- {}".format(choice.title),
)
)
tokens.append(
(
"class:selected" if selected else "class:disabled",
"{}".format(
""
if isinstance(choice.disabled, bool)
else " ({})".format(choice.disabled)
),
)
)
else:
shortcut = choice.get_shortcut_title() if self.use_shortcuts else ""
if selected:
if self.use_indicator:
indicator = INDICATOR_SELECTED + " "
else:
indicator = ""
tokens.append(("class:selected", "{}".format(indicator)))
else:
if self.use_indicator:
indicator = INDICATOR_UNSELECTED + " "
else:
indicator = ""
tokens.append(("class:text", "{}".format(indicator)))
if isinstance(choice.title, list):
tokens.extend(choice.title)
elif selected:
tokens.append(
("class:selected", "{}{}".format(shortcut, choice.title))
)
elif index == self.pointed_at:
tokens.append(
("class:highlighted", "{}{}".format(shortcut, choice.title))
)
else:
tokens.append(("class:text", "{}{}".format(shortcut, choice.title)))
tokens.append(("", "\n"))
# prepare the select choices
for i, c in enumerate(self.choices):
append(i, c)
if self.show_selected:
current = self.get_pointed_at()
answer = current.get_shortcut_title() if self.use_shortcuts else ""
answer += (
current.title if isinstance(current.title, str) else current.title[0][1]
)
tokens.append(("class:text", " Answer: {}".format(answer)))
else:
tokens.pop() # Remove last newline.
return tokens
def is_selection_a_separator(self) -> bool:
selected = self.choices[self.pointed_at]
return isinstance(selected, Separator)
def is_selection_disabled(self) -> Optional[str]:
return self.choices[self.pointed_at].disabled
def is_selection_valid(self) -> bool:
return not self.is_selection_disabled() and not self.is_selection_a_separator()
def select_previous(self) -> None:
self.pointed_at = (self.pointed_at - 1) % self.choice_count
def select_next(self) -> None:
self.pointed_at = (self.pointed_at + 1) % self.choice_count
def get_pointed_at(self) -> Choice:
return self.choices[self.pointed_at]
def get_selected_values(self) -> List[Choice]:
# get values not labels
return [
c
for c in self.choices
if (not isinstance(c, Separator) and c.value in self.selected_options)
]
def build_validator(validate: Any) -> Optional[Validator]:
if validate:
if inspect.isclass(validate) and issubclass(validate, Validator):
return validate()
elif isinstance(validate, Validator):
return validate
elif callable(validate):
class _InputValidator(Validator):
def validate(self, document):
verdict = validate(document.text)
if verdict is not True:
if verdict is False:
verdict = INVALID_INPUT
raise ValidationError(
message=verdict, cursor_position=len(document.text)
)
return _InputValidator()
return None
def _fix_unecessary_blank_lines(ps: PromptSession) -> None:
"""This is a fix for additional empty lines added by prompt toolkit.
This assumes the layout of the default session doesn't change, if it
does, this needs an update."""
default_container = ps.layout.container
default_buffer_window = (
default_container.get_children()[0].content.get_children()[1].content # type: ignore[attr-defined]
)
assert isinstance(default_buffer_window, Window)
# this forces the main window to stay as small as possible, avoiding
# empty lines in selections
default_buffer_window.dont_extend_height = Always()
default_buffer_window.always_hide_cursor = Always()
def create_inquirer_layout(
ic: InquirerControl,
get_prompt_tokens: Callable[[], List[Tuple[str, str]]],
**kwargs: Any,
) -> Layout:
"""Create a layout combining question and inquirer selection."""
ps: PromptSession = PromptSession(
get_prompt_tokens, reserve_space_for_menu=0, **kwargs
)
_fix_unecessary_blank_lines(ps)
validation_prompt: PromptSession = PromptSession(
bottom_toolbar=lambda: ic.error_message, **kwargs
)
return Layout(
HSplit(
[
ps.layout.container,
ConditionalContainer(Window(ic), filter=~IsDone()),
ConditionalContainer(
validation_prompt.layout.container,
filter=Condition(lambda: ic.error_message is not None),
),
]
)
)
def print_formatted_text(text: str, style: Optional[str] = None, **kwargs: Any) -> None:
"""Print formatted text.
Sometimes you want to spice up your printed messages a bit,
:meth:`questionary.print` is a helper to do just that.
Example:
>>> import questionary
>>> questionary.print("Hello World 🦄", style="bold italic fg:darkred")
Hello World 🦄
.. image:: ../images/print.gif
Args:
text: Text to be printed.
style: Style used for printing. The style argument uses the
prompt :ref:`toolkit style strings <prompt_toolkit:styling>`.
"""
from prompt_toolkit import print_formatted_text as pt_print
from prompt_toolkit.formatted_text import FormattedText as FText
if style is not None:
text_style = Style([("text", style)])
else:
text_style = DEFAULT_STYLE
pt_print(FText([("class:text", text)]), style=text_style, **kwargs)