| | """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 |
| |
|