Initial commit: Tamigo CLI with Gitea Actions and global installation support
This commit is contained in:
670
venv/lib/python3.12/site-packages/questionary/prompts/common.py
Normal file
670
venv/lib/python3.12/site-packages/questionary/prompts/common.py
Normal file
@@ -0,0 +1,670 @@
|
||||
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.keys import Keys
|
||||
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.layout.controls import BufferControl
|
||||
from prompt_toolkit.layout.dimension import LayoutDimension
|
||||
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.
|
||||
|
||||
description: Optional description of the item that can be displayed.
|
||||
"""
|
||||
|
||||
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[Union[str, bool]]
|
||||
|
||||
description: Optional[str]
|
||||
"""Choice description"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: FormattedText,
|
||||
value: Optional[Any] = None,
|
||||
disabled: Optional[str] = None,
|
||||
checked: Optional[bool] = False,
|
||||
shortcut_key: Optional[Union[str, bool]] = True,
|
||||
description: Optional[str] = None,
|
||||
) -> None:
|
||||
self.disabled = disabled
|
||||
self.title = title
|
||||
self.shortcut_key = shortcut_key
|
||||
# self.auto_shortcut is set by the self.shortcut_key setter
|
||||
self.checked = checked if checked is not None else False
|
||||
self.description = description
|
||||
|
||||
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
|
||||
|
||||
@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"),
|
||||
c.get("description", None),
|
||||
)
|
||||
|
||||
@property
|
||||
def shortcut_key(self) -> Optional[Union[str, bool]]:
|
||||
"""A shortcut key for the choice"""
|
||||
return self.__shortcut_key
|
||||
|
||||
@shortcut_key.setter
|
||||
def shortcut_key(self, key: Optional[Union[str, bool]]):
|
||||
if key is not None:
|
||||
if isinstance(key, bool):
|
||||
self.__auto_shortcut = key
|
||||
self.__shortcut_key = None
|
||||
else:
|
||||
self.__shortcut_key = str(key)
|
||||
self.__auto_shortcut = False
|
||||
else:
|
||||
self.__shortcut_key = None
|
||||
self.__auto_shortcut = True
|
||||
|
||||
@shortcut_key.deleter
|
||||
def shortcut_key(self):
|
||||
self.__shortcut_key = None
|
||||
self.__auto_shortcut = True
|
||||
|
||||
def get_shortcut_title(self):
|
||||
if self.shortcut_key is None:
|
||||
return "-) "
|
||||
else:
|
||||
return "{}) ".format(self.shortcut_key)
|
||||
|
||||
@property
|
||||
def auto_shortcut(self) -> bool:
|
||||
"""Whether to assign a shortcut key to the choice
|
||||
|
||||
Keys are assigned starting with numbers and proceeding
|
||||
through the ASCII alphabet.
|
||||
"""
|
||||
return self.__auto_shortcut
|
||||
|
||||
@auto_shortcut.setter
|
||||
def auto_shortcut(self, should_assign: bool):
|
||||
self.__auto_shortcut = should_assign
|
||||
if self.__auto_shortcut:
|
||||
self.__shortcut_key = None
|
||||
|
||||
@auto_shortcut.deleter
|
||||
def auto_shortcut(self):
|
||||
self.__auto_shortcut = False
|
||||
|
||||
|
||||
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]
|
||||
search_filter: Union[str, None] = None
|
||||
use_indicator: bool
|
||||
use_shortcuts: bool
|
||||
use_arrow_keys: bool
|
||||
pointer: Optional[str]
|
||||
pointed_at: int
|
||||
is_answered: bool
|
||||
show_description: 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,
|
||||
show_description: bool = True,
|
||||
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.show_description = show_description
|
||||
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.found_in_search = False
|
||||
|
||||
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 filtered_choices(self):
|
||||
if not self.search_filter:
|
||||
return self.choices
|
||||
filtered = [
|
||||
c for c in self.choices if self.search_filter.lower() in c.title.lower()
|
||||
]
|
||||
self.found_in_search = len(filtered) > 0
|
||||
return filtered if self.found_in_search else self.choices
|
||||
|
||||
@property
|
||||
def choice_count(self) -> int:
|
||||
return len(self.filtered_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.filtered_choices):
|
||||
append(i, c)
|
||||
|
||||
current = self.get_pointed_at()
|
||||
|
||||
if self.show_selected:
|
||||
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)))
|
||||
|
||||
show_description = self.show_description and current.description is not None
|
||||
if show_description:
|
||||
tokens.append(
|
||||
("class:text", " Description: {}".format(current.description))
|
||||
)
|
||||
|
||||
if not (self.show_selected or show_description):
|
||||
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.filtered_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 add_search_character(self, char: Keys) -> None:
|
||||
"""Adds a character to the search filter"""
|
||||
if char == Keys.Backspace:
|
||||
self.remove_search_character()
|
||||
else:
|
||||
if self.search_filter is None:
|
||||
self.search_filter = str(char)
|
||||
else:
|
||||
self.search_filter += str(char)
|
||||
|
||||
# Make sure that the selection is in the bounds of the filtered list
|
||||
self.pointed_at = 0
|
||||
|
||||
def remove_search_character(self) -> None:
|
||||
if self.search_filter and len(self.search_filter) > 1:
|
||||
self.search_filter = self.search_filter[:-1]
|
||||
else:
|
||||
self.search_filter = None
|
||||
|
||||
def get_search_string_tokens(self):
|
||||
if self.search_filter is None:
|
||||
return None
|
||||
|
||||
return [
|
||||
("", "\n"),
|
||||
("class:question-mark", "/ "),
|
||||
(
|
||||
"class:search_success" if self.found_in_search else "class:search_none",
|
||||
self.search_filter,
|
||||
),
|
||||
("class:question-mark", "..."),
|
||||
]
|
||||
|
||||
|
||||
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_buffer_window: Window = next(
|
||||
win
|
||||
for win in ps.layout.find_all_windows()
|
||||
if isinstance(win.content, BufferControl)
|
||||
and win.content.buffer.name == "DEFAULT_BUFFER"
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
@Condition
|
||||
def has_search_string():
|
||||
return ic.get_search_string_tokens() is not None
|
||||
|
||||
validation_prompt: PromptSession = PromptSession(
|
||||
bottom_toolbar=lambda: ic.error_message, **kwargs
|
||||
)
|
||||
|
||||
return Layout(
|
||||
HSplit(
|
||||
[
|
||||
ps.layout.container,
|
||||
ConditionalContainer(Window(ic), filter=~IsDone()),
|
||||
ConditionalContainer(
|
||||
Window(
|
||||
height=LayoutDimension.exact(2),
|
||||
content=FormattedTextControl(ic.get_search_string_tokens),
|
||||
),
|
||||
filter=has_search_string & ~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)
|
||||
Reference in New Issue
Block a user