Spaces:
Sleeping
Sleeping
File size: 8,334 Bytes
2d876d1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
"""Contains the content control class :class:`.InquirerPyUIListControl`."""
from abc import abstractmethod
from dataclasses import asdict, dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple, cast
from prompt_toolkit.layout.controls import FormattedTextControl
from InquirerPy.exceptions import InvalidArgument, RequiredKeyNotFound
from InquirerPy.separator import Separator
from InquirerPy.utils import InquirerPyListChoices, InquirerPySessionResult
__all__ = ["Choice", "InquirerPyUIListControl"]
@dataclass
class Choice:
"""Class to create choices for list type prompts.
A simple dataclass that can be used as an alternate to using :class:`dict`
when working with choices.
Args:
value: The value of the choice when user selects this choice.
name: The value that should be presented to the user prior/after selection of the choice.
This value is optional, if not provided, it will fallback to the string representation of `value`.
enabled: Indicates if the choice should be pre-selected.
This only has effects when the prompt has `multiselect` enabled.
"""
value: Any
name: Optional[str] = None
enabled: bool = False
def __post_init__(self):
"""Assign strinify value to name if not present."""
if self.name is None:
self.name = str(self.value)
class InquirerPyUIListControl(FormattedTextControl):
"""A base class to create :class:`~prompt_toolkit.layout.UIControl` to display list type contents.
Args:
choices(InquirerPyListChoices): List of choices to display as the content.
Can also be a callable or async callable that returns a list of choices.
default: Default value, this will affect the cursor position.
multiselect: Indicate if the current prompt has `multiselect` enabled.
session_result: Current session result.
"""
def __init__(
self,
choices: InquirerPyListChoices,
default: Any = None,
multiselect: bool = False,
session_result: Optional[InquirerPySessionResult] = None,
) -> None:
self._session_result = session_result or {}
self._selected_choice_index: int = 0
self._choice_func = None
self._multiselect = multiselect
self._default = (
default
if not isinstance(default, Callable)
else cast(Callable, default)(self._session_result)
)
self._raw_choices = (
choices
if not isinstance(choices, Callable)
else cast(Callable, choices)(self._session_result)
)
self._choices = self._get_choices(self._raw_choices, self._default)
self._safety_check()
self._format_choices()
super().__init__(self._get_formatted_choices)
def _get_choices(self, choices: List[Any], default: Any) -> List[Dict[str, Any]]:
"""Process the raw user input choices and format it into dictionary.
Args:
choices: List of chices to display.
default: Default value, this will affect the :attr:`.InquirerPyUIListControl.selected_choice_index`
Returns:
List of choices.
Raises:
RequiredKeyNotFound: When the provided choice is missing the `name` or `value` key.
"""
processed_choices: List[Dict[str, Any]] = []
try:
for index, choice in enumerate(choices, start=0):
if isinstance(choice, dict):
if choice["value"] == default:
self.selected_choice_index = index
processed_choices.append(
{
"name": str(choice["name"]),
"value": choice["value"],
"enabled": choice.get("enabled", False)
if self._multiselect
else False,
}
)
elif isinstance(choice, Separator):
if self.selected_choice_index == index:
self.selected_choice_index = (
self.selected_choice_index + 1
) % len(choices)
processed_choices.append(
{"name": str(choice), "value": choice, "enabled": False}
)
elif isinstance(choice, Choice):
dict_choice = asdict(choice)
if dict_choice["value"] == default:
self.selected_choice_index = index
if not self._multiselect:
dict_choice["enabled"] = False
processed_choices.append(dict_choice)
else:
if choice == default:
self.selected_choice_index = index
processed_choices.append(
{"name": str(choice), "value": choice, "enabled": False}
)
except KeyError:
raise RequiredKeyNotFound(
"dictionary type of choice require a 'name' key and a 'value' key"
)
return processed_choices
@property
def selected_choice_index(self) -> int:
"""int: Current highlighted index."""
return self._selected_choice_index
@selected_choice_index.setter
def selected_choice_index(self, value: int) -> None:
self._selected_choice_index = value
@property
def choices(self) -> List[Dict[str, Any]]:
"""List[Dict[str, Any]]: Get all processed choices."""
return self._choices
@choices.setter
def choices(self, value: List[Dict[str, Any]]) -> None:
self._choices = value
def _safety_check(self) -> None:
"""Validate processed choices.
Check if the choices are empty or if it only contains :class:`~InquirerPy.separator.Separator`.
"""
if not self.choices:
raise InvalidArgument("argument choices cannot be empty")
should_proceed: bool = False
for choice in self.choices:
if not isinstance(choice["value"], Separator):
should_proceed = True
break
if not should_proceed:
raise InvalidArgument(
"argument choices should contain choices other than separator"
)
def _get_formatted_choices(self) -> List[Tuple[str, str]]:
"""Get all choices in formatted text format.
Returns:
List of choices in formatted text form.
"""
display_choices = []
for index, choice in enumerate(self.choices):
if index == self.selected_choice_index:
display_choices += self._get_hover_text(choice)
else:
display_choices += self._get_normal_text(choice)
display_choices.append(("", "\n"))
if display_choices:
display_choices.pop()
return display_choices
def _format_choices(self) -> None:
"""Perform post processing on the choices.
Additional customisation to the choices after :meth:`.InquirerPyUIListControl._get_choices` call.
"""
pass
@abstractmethod
def _get_hover_text(self, choice) -> List[Tuple[str, str]]:
"""Generate the formatted text for hovered choice.
Returns:
Formatted text in list of tuple format.
"""
pass
@abstractmethod
def _get_normal_text(self, choice) -> List[Tuple[str, str]]:
"""Generate the formatted text for non-hovered choices.
Returns:
Formatted text in list of tuple format.
"""
pass
@property
def choice_count(self) -> int:
"""int: Total count of choices."""
return len(self.choices)
@property
def selection(self) -> Dict[str, Any]:
"""Dict[str, Any]: Current selected choice."""
return self.choices[self.selected_choice_index]
@property
def loading(self) -> bool:
"""bool: Indicate if the content control is loading."""
return self._loading
@loading.setter
def loading(self, value: bool) -> None:
self._loading = value
|