InquirerPrompt 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inquirerprompt-0.3.0/InquirerPy/__init__.py +4 -0
- inquirerprompt-0.3.0/InquirerPy/base/__init__.py +24 -0
- inquirerprompt-0.3.0/InquirerPy/base/complex.py +295 -0
- inquirerprompt-0.3.0/InquirerPy/base/control.py +230 -0
- inquirerprompt-0.3.0/InquirerPy/base/list.py +239 -0
- inquirerprompt-0.3.0/InquirerPy/base/simple.py +379 -0
- inquirerprompt-0.3.0/InquirerPy/containers/__init__.py +3 -0
- inquirerprompt-0.3.0/InquirerPy/containers/instruction.py +38 -0
- inquirerprompt-0.3.0/InquirerPy/containers/message.py +42 -0
- inquirerprompt-0.3.0/InquirerPy/containers/spinner.py +109 -0
- inquirerprompt-0.3.0/InquirerPy/containers/validation.py +60 -0
- inquirerprompt-0.3.0/InquirerPy/enum.py +8 -0
- inquirerprompt-0.3.0/InquirerPy/exceptions.py +25 -0
- inquirerprompt-0.3.0/InquirerPy/inquirer.py +31 -0
- inquirerprompt-0.3.0/InquirerPy/prompts/__init__.py +25 -0
- inquirerprompt-0.3.0/InquirerPy/prompts/checkbox.py +249 -0
- inquirerprompt-0.3.0/InquirerPy/prompts/confirm.py +202 -0
- inquirerprompt-0.3.0/InquirerPy/prompts/expand.py +459 -0
- inquirerprompt-0.3.0/InquirerPy/prompts/filepath.py +193 -0
- inquirerprompt-0.3.0/InquirerPy/prompts/fuzzy.py +685 -0
- inquirerprompt-0.3.0/InquirerPy/prompts/input.py +256 -0
- inquirerprompt-0.3.0/InquirerPy/prompts/list.py +371 -0
- inquirerprompt-0.3.0/InquirerPy/prompts/number.py +621 -0
- inquirerprompt-0.3.0/InquirerPy/prompts/rawlist.py +286 -0
- inquirerprompt-0.3.0/InquirerPy/prompts/secret.py +137 -0
- inquirerprompt-0.3.0/InquirerPy/py.typed +1 -0
- inquirerprompt-0.3.0/InquirerPy/resolver.py +220 -0
- inquirerprompt-0.3.0/InquirerPy/separator.py +23 -0
- inquirerprompt-0.3.0/InquirerPy/utils.py +291 -0
- inquirerprompt-0.3.0/InquirerPy/validator.py +166 -0
- inquirerprompt-0.3.0/LICENSE +21 -0
- inquirerprompt-0.3.0/PKG-INFO +217 -0
- inquirerprompt-0.3.0/README.md +175 -0
- inquirerprompt-0.3.0/pyproject.toml +97 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Module contains base class for prompts.
|
|
2
|
+
|
|
3
|
+
BaseSimplePrompt ← InputPrompt ← SecretPrompt ...
|
|
4
|
+
↑
|
|
5
|
+
BaseComplexPrompt
|
|
6
|
+
↑
|
|
7
|
+
BaseListPrompt ← FuzzyPrompt
|
|
8
|
+
↑
|
|
9
|
+
ListPrompt ← ExpandPrompt ...
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"BaseComplexPrompt",
|
|
14
|
+
"FakeDocument",
|
|
15
|
+
"Choice",
|
|
16
|
+
"InquirerPyUIListControl",
|
|
17
|
+
"BaseSimplePrompt",
|
|
18
|
+
"BaseListPrompt",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
from .complex import BaseComplexPrompt, FakeDocument
|
|
22
|
+
from .control import Choice, InquirerPyUIListControl
|
|
23
|
+
from .list import BaseListPrompt
|
|
24
|
+
from .simple import BaseSimplePrompt
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Contains the interface class :class:`.BaseComplexPrompt` for more complex prompts and the mocked document class :class:`.FakeDocument`."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Callable, List, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit.application import Application
|
|
8
|
+
from prompt_toolkit.enums import EditingMode
|
|
9
|
+
from prompt_toolkit.filters.base import Condition, FilterOrBool
|
|
10
|
+
from prompt_toolkit.key_binding.key_bindings import KeyHandlerCallable
|
|
11
|
+
from prompt_toolkit.keys import Keys
|
|
12
|
+
|
|
13
|
+
from InquirerPy.base.simple import BaseSimplePrompt
|
|
14
|
+
from InquirerPy.enum import INQUIRERPY_KEYBOARD_INTERRUPT
|
|
15
|
+
from InquirerPy.utils import (
|
|
16
|
+
InquirerPySessionResult,
|
|
17
|
+
InquirerPyStyle,
|
|
18
|
+
InquirerPyValidate,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class FakeDocument:
|
|
24
|
+
"""A fake `prompt_toolkit` document class.
|
|
25
|
+
|
|
26
|
+
Work around to allow non-buffer type :class:`~prompt_toolkit.layout.UIControl` to use
|
|
27
|
+
:class:`~prompt_toolkit.validation.Validator`.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
text: Content to be validated.
|
|
31
|
+
cursor_position: Fake cursor position.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
text: str
|
|
35
|
+
cursor_position: int = 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BaseComplexPrompt(BaseSimplePrompt):
|
|
39
|
+
"""A base class to create a more complex prompt that will involve :class:`~prompt_toolkit.application.Application`.
|
|
40
|
+
|
|
41
|
+
Note:
|
|
42
|
+
This class does not create :class:`~prompt_toolkit.layout.Layout` nor :class:`~prompt_toolkit.application.Application`,
|
|
43
|
+
it only contains the necessary attributes and helper functions to be consumed.
|
|
44
|
+
|
|
45
|
+
Note:
|
|
46
|
+
Use :class:`~InquirerPy.base.BaseListPrompt` to create a complex list prompt which involves multiple choices. It has
|
|
47
|
+
more methods and helper function implemented.
|
|
48
|
+
|
|
49
|
+
See Also:
|
|
50
|
+
:class:`~InquirerPy.base.BaseListPrompt`
|
|
51
|
+
:class:`~InquirerPy.prompts.fuzzy.FuzzyPrompt`
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
message: Union[str, Callable[[InquirerPySessionResult], str]],
|
|
57
|
+
style: Optional[InquirerPyStyle] = None,
|
|
58
|
+
border: bool = False,
|
|
59
|
+
vi_mode: bool = False,
|
|
60
|
+
qmark: str = "?",
|
|
61
|
+
amark: str = "?",
|
|
62
|
+
instruction: str = "",
|
|
63
|
+
long_instruction: str = "",
|
|
64
|
+
transformer: Optional[Callable[[Any], Any]] = None,
|
|
65
|
+
filter: Optional[Callable[[Any], Any]] = None,
|
|
66
|
+
validate: Optional[InquirerPyValidate] = None,
|
|
67
|
+
invalid_message: str = "Invalid input",
|
|
68
|
+
wrap_lines: bool = True,
|
|
69
|
+
raise_keyboard_interrupt: bool = True,
|
|
70
|
+
mandatory: bool = True,
|
|
71
|
+
mandatory_message: str = "Mandatory prompt",
|
|
72
|
+
session_result: Optional[InquirerPySessionResult] = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
super().__init__(
|
|
75
|
+
message=message,
|
|
76
|
+
style=style,
|
|
77
|
+
vi_mode=vi_mode,
|
|
78
|
+
qmark=qmark,
|
|
79
|
+
amark=amark,
|
|
80
|
+
instruction=instruction,
|
|
81
|
+
transformer=transformer,
|
|
82
|
+
filter=filter,
|
|
83
|
+
invalid_message=invalid_message,
|
|
84
|
+
validate=validate,
|
|
85
|
+
wrap_lines=wrap_lines,
|
|
86
|
+
raise_keyboard_interrupt=raise_keyboard_interrupt,
|
|
87
|
+
mandatory=mandatory,
|
|
88
|
+
mandatory_message=mandatory_message,
|
|
89
|
+
session_result=session_result,
|
|
90
|
+
)
|
|
91
|
+
self._invalid_message = invalid_message
|
|
92
|
+
self._rendered = False
|
|
93
|
+
self._invalid = False
|
|
94
|
+
self._loading = False
|
|
95
|
+
self._application: Application
|
|
96
|
+
self._long_instruction = long_instruction
|
|
97
|
+
self._border = border
|
|
98
|
+
self._height_offset = 2 # prev prompt result + current prompt question
|
|
99
|
+
if self._border:
|
|
100
|
+
self._height_offset += 2
|
|
101
|
+
if self._long_instruction:
|
|
102
|
+
self._height_offset += 1
|
|
103
|
+
self._validation_window_bottom_offset = 0 if not self._long_instruction else 1
|
|
104
|
+
if self._wrap_lines:
|
|
105
|
+
self._validation_window_bottom_offset += (
|
|
106
|
+
self.extra_long_instruction_line_count
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self._is_vim_edit = Condition(lambda: self._editing_mode == EditingMode.VI)
|
|
110
|
+
self._is_invalid = Condition(lambda: self._invalid)
|
|
111
|
+
self._is_displaying_long_instruction = Condition(
|
|
112
|
+
lambda: self._long_instruction != ""
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _redraw(self) -> None:
|
|
116
|
+
"""Redraw the application UI."""
|
|
117
|
+
self._application.invalidate()
|
|
118
|
+
|
|
119
|
+
def register_kb(
|
|
120
|
+
self, *keys: Union[Keys, str], filter: FilterOrBool = True
|
|
121
|
+
) -> Callable[[KeyHandlerCallable], KeyHandlerCallable]:
|
|
122
|
+
"""Decorate keybinding registration function.
|
|
123
|
+
|
|
124
|
+
Ensure that the `invalid` state is cleared on next keybinding entered.
|
|
125
|
+
"""
|
|
126
|
+
kb_dec = super().register_kb(*keys, filter=filter)
|
|
127
|
+
|
|
128
|
+
def decorator(func: KeyHandlerCallable) -> KeyHandlerCallable:
|
|
129
|
+
@kb_dec
|
|
130
|
+
def executable(event):
|
|
131
|
+
if self._invalid:
|
|
132
|
+
self._invalid = False
|
|
133
|
+
func(event)
|
|
134
|
+
|
|
135
|
+
return executable
|
|
136
|
+
|
|
137
|
+
return decorator
|
|
138
|
+
|
|
139
|
+
def _exception_handler(self, _, context) -> None:
|
|
140
|
+
"""Set exception handler for the event loop.
|
|
141
|
+
|
|
142
|
+
Skip the question and raise exception.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
loop: Current event loop.
|
|
146
|
+
context: Exception context.
|
|
147
|
+
"""
|
|
148
|
+
self._status["answered"] = True
|
|
149
|
+
self._status["result"] = INQUIRERPY_KEYBOARD_INTERRUPT
|
|
150
|
+
self._status["skipped"] = True
|
|
151
|
+
self._application.exit(exception=context["exception"])
|
|
152
|
+
|
|
153
|
+
def _after_render(self, app: Optional[Application]) -> None:
|
|
154
|
+
"""Run after the :class:`~prompt_toolkit.application.Application` is rendered/updated.
|
|
155
|
+
|
|
156
|
+
Since this function is fired up on each render, adding a check on `self._rendered` to
|
|
157
|
+
process logics that should only run once.
|
|
158
|
+
|
|
159
|
+
Set event loop exception handler here, since its guaranteed that the event loop is running
|
|
160
|
+
in `_after_render`.
|
|
161
|
+
"""
|
|
162
|
+
if not self._rendered:
|
|
163
|
+
self._rendered = True
|
|
164
|
+
|
|
165
|
+
self._keybinding_factory()
|
|
166
|
+
self._on_rendered(app)
|
|
167
|
+
|
|
168
|
+
def _set_error(self, message: str) -> None:
|
|
169
|
+
"""Set error message and set invalid state.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
message: Error message to display.
|
|
173
|
+
"""
|
|
174
|
+
self._invalid_message = message
|
|
175
|
+
self._invalid = True
|
|
176
|
+
|
|
177
|
+
def _get_error_message(self) -> List[Tuple[str, str]]:
|
|
178
|
+
"""Obtain the error message dynamically.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
FormattedText in list of tuple format.
|
|
182
|
+
"""
|
|
183
|
+
return [
|
|
184
|
+
(
|
|
185
|
+
"class:validation-toolbar",
|
|
186
|
+
self._invalid_message,
|
|
187
|
+
)
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
def _on_rendered(self, _: Optional[Application]) -> None:
|
|
191
|
+
"""Run once after the UI is rendered. Acts like `ComponentDidMount`."""
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
def _get_prompt_message(self) -> List[Tuple[str, str]]:
|
|
195
|
+
"""Get the prompt message to display.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Formatted text in list of tuple format.
|
|
199
|
+
"""
|
|
200
|
+
pre_answer = (
|
|
201
|
+
"class:instruction",
|
|
202
|
+
" %s " % self.instruction if self.instruction else " ",
|
|
203
|
+
)
|
|
204
|
+
post_answer = ("class:answer", " %s" % self.status["result"])
|
|
205
|
+
return super()._get_prompt_message(pre_answer, post_answer)
|
|
206
|
+
|
|
207
|
+
def _run(self) -> Any:
|
|
208
|
+
"""Run the application."""
|
|
209
|
+
return self.application.run()
|
|
210
|
+
|
|
211
|
+
async def _run_async(self) -> None:
|
|
212
|
+
"""Run the application asynchronously."""
|
|
213
|
+
return await self.application.run_async()
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def application(self) -> Application:
|
|
217
|
+
"""Get the application.
|
|
218
|
+
|
|
219
|
+
:class:`.BaseComplexPrompt` requires :attr:`.BaseComplexPrompt._application` to be defined since this class
|
|
220
|
+
doesn't implement :class:`~prompt_toolkit.layout.Layout` and :class:`~prompt_toolkit.application.Application`.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
NotImplementedError: When `self._application` is not defined.
|
|
224
|
+
"""
|
|
225
|
+
if not self._application:
|
|
226
|
+
raise NotImplementedError
|
|
227
|
+
return self._application
|
|
228
|
+
|
|
229
|
+
@application.setter
|
|
230
|
+
def application(self, value: Application) -> None:
|
|
231
|
+
self._application = value
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def height_offset(self) -> int:
|
|
235
|
+
"""int: Height offset to apply."""
|
|
236
|
+
if not self._wrap_lines:
|
|
237
|
+
return self._height_offset
|
|
238
|
+
return self.extra_line_count + self._height_offset
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def total_message_length(self) -> int:
|
|
242
|
+
"""int: Total length of the message."""
|
|
243
|
+
total_message_length = 0
|
|
244
|
+
if self._qmark:
|
|
245
|
+
total_message_length += len(self._qmark)
|
|
246
|
+
total_message_length += 1 # Extra space if qmark is present
|
|
247
|
+
total_message_length += len(str(self._message))
|
|
248
|
+
total_message_length += 1 # Extra space between message and instruction
|
|
249
|
+
total_message_length += len(str(self._instruction))
|
|
250
|
+
if self._instruction:
|
|
251
|
+
total_message_length += 1 # Extra space behind the instruction
|
|
252
|
+
return total_message_length
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def extra_message_line_count(self) -> int:
|
|
256
|
+
"""int: Get the extra lines created caused by line wrapping.
|
|
257
|
+
|
|
258
|
+
Minus 1 on the totoal message length as we only want the extra line.
|
|
259
|
+
24 // 24 will equal to 1 however we only want the value to be 1 when we have 25 char
|
|
260
|
+
which will create an extra line.
|
|
261
|
+
"""
|
|
262
|
+
term_width, _ = shutil.get_terminal_size()
|
|
263
|
+
return (self.total_message_length - 1) // term_width
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def extra_long_instruction_line_count(self) -> int:
|
|
267
|
+
"""int: Get the extra lines created caused by line wrapping.
|
|
268
|
+
|
|
269
|
+
See Also:
|
|
270
|
+
:attr:`.BaseComplexPrompt.extra_message_line_count`
|
|
271
|
+
"""
|
|
272
|
+
if self._long_instruction:
|
|
273
|
+
term_width, _ = shutil.get_terminal_size()
|
|
274
|
+
return (len(self._long_instruction) - 1) // term_width
|
|
275
|
+
else:
|
|
276
|
+
return 0
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def extra_line_count(self) -> int:
|
|
280
|
+
"""Get the extra lines created caused by line wrapping.
|
|
281
|
+
|
|
282
|
+
Used mainly to calculate how much additional offset should be applied when getting
|
|
283
|
+
the height.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Total extra lines created due to line wrapping.
|
|
287
|
+
"""
|
|
288
|
+
result = 0
|
|
289
|
+
|
|
290
|
+
# message wrap
|
|
291
|
+
result += self.extra_message_line_count
|
|
292
|
+
# long instruction wrap
|
|
293
|
+
result += self.extra_long_instruction_line_count
|
|
294
|
+
|
|
295
|
+
return result
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Contains the content control class :class:`.InquirerPyUIListControl`."""
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, cast
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
8
|
+
|
|
9
|
+
from InquirerPy.exceptions import InvalidArgument, RequiredKeyNotFound
|
|
10
|
+
from InquirerPy.separator import Separator
|
|
11
|
+
from InquirerPy.utils import InquirerPyListChoices, InquirerPySessionResult
|
|
12
|
+
|
|
13
|
+
__all__ = ["Choice", "InquirerPyUIListControl"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Choice:
|
|
18
|
+
"""Class to create choices for list type prompts.
|
|
19
|
+
|
|
20
|
+
A simple dataclass that can be used as an alternate to using :class:`dict`
|
|
21
|
+
when working with choices.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
value: The value of the choice when user selects this choice.
|
|
25
|
+
name: The value that should be presented to the user prior/after selection of the choice.
|
|
26
|
+
This value is optional, if not provided, it will fallback to the string representation of `value`.
|
|
27
|
+
enabled: Indicates if the choice should be pre-selected.
|
|
28
|
+
This only has effects when the prompt has `multiselect` enabled.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
value: Any
|
|
32
|
+
name: Optional[str] = None
|
|
33
|
+
enabled: bool = False
|
|
34
|
+
|
|
35
|
+
def __post_init__(self):
|
|
36
|
+
"""Assign strinify value to name if not present."""
|
|
37
|
+
if self.name is None:
|
|
38
|
+
self.name = str(self.value)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InquirerPyUIListControl(FormattedTextControl):
|
|
42
|
+
"""A base class to create :class:`~prompt_toolkit.layout.UIControl` to display list type contents.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
choices(InquirerPyListChoices): List of choices to display as the content.
|
|
46
|
+
Can also be a callable or async callable that returns a list of choices.
|
|
47
|
+
default: Default value, this will affect the cursor position.
|
|
48
|
+
multiselect: Indicate if the current prompt has `multiselect` enabled.
|
|
49
|
+
session_result: Current session result.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
choices: InquirerPyListChoices,
|
|
55
|
+
default: Any = None,
|
|
56
|
+
multiselect: bool = False,
|
|
57
|
+
session_result: Optional[InquirerPySessionResult] = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
self._session_result = session_result or {}
|
|
60
|
+
self._selected_choice_index: int = 0
|
|
61
|
+
self._choice_func = None
|
|
62
|
+
self._multiselect = multiselect
|
|
63
|
+
self._default = (
|
|
64
|
+
default
|
|
65
|
+
if not isinstance(default, Callable)
|
|
66
|
+
else cast(Callable, default)(self._session_result)
|
|
67
|
+
)
|
|
68
|
+
self._raw_choices = (
|
|
69
|
+
choices
|
|
70
|
+
if not isinstance(choices, Callable)
|
|
71
|
+
else cast(Callable, choices)(self._session_result)
|
|
72
|
+
)
|
|
73
|
+
self._choices = self._get_choices(self._raw_choices, self._default)
|
|
74
|
+
self._safety_check()
|
|
75
|
+
self._format_choices()
|
|
76
|
+
super().__init__(self._get_formatted_choices)
|
|
77
|
+
|
|
78
|
+
def _get_choices(self, choices: List[Any], default: Any) -> List[Dict[str, Any]]:
|
|
79
|
+
"""Process the raw user input choices and format it into dictionary.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
choices: List of chices to display.
|
|
83
|
+
default: Default value, this will affect the :attr:`.InquirerPyUIListControl.selected_choice_index`
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
List of choices.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
RequiredKeyNotFound: When the provided choice is missing the `name` or `value` key.
|
|
90
|
+
"""
|
|
91
|
+
processed_choices: List[Dict[str, Any]] = []
|
|
92
|
+
try:
|
|
93
|
+
for index, choice in enumerate(choices, start=0):
|
|
94
|
+
if isinstance(choice, dict):
|
|
95
|
+
if choice["value"] == default:
|
|
96
|
+
self.selected_choice_index = index
|
|
97
|
+
processed_choices.append(
|
|
98
|
+
{
|
|
99
|
+
"name": str(choice["name"]),
|
|
100
|
+
"value": choice["value"],
|
|
101
|
+
"enabled": (
|
|
102
|
+
choice.get("enabled", False)
|
|
103
|
+
if self._multiselect
|
|
104
|
+
else False
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
elif isinstance(choice, Separator):
|
|
109
|
+
if self.selected_choice_index == index:
|
|
110
|
+
self.selected_choice_index = (
|
|
111
|
+
self.selected_choice_index + 1
|
|
112
|
+
) % len(choices)
|
|
113
|
+
processed_choices.append(
|
|
114
|
+
{"name": str(choice), "value": choice, "enabled": False}
|
|
115
|
+
)
|
|
116
|
+
elif isinstance(choice, Choice):
|
|
117
|
+
dict_choice = asdict(choice)
|
|
118
|
+
if dict_choice["value"] == default:
|
|
119
|
+
self.selected_choice_index = index
|
|
120
|
+
if not self._multiselect:
|
|
121
|
+
dict_choice["enabled"] = False
|
|
122
|
+
processed_choices.append(dict_choice)
|
|
123
|
+
else:
|
|
124
|
+
if choice == default:
|
|
125
|
+
self.selected_choice_index = index
|
|
126
|
+
processed_choices.append(
|
|
127
|
+
{"name": str(choice), "value": choice, "enabled": False}
|
|
128
|
+
)
|
|
129
|
+
except KeyError:
|
|
130
|
+
raise RequiredKeyNotFound(
|
|
131
|
+
"dictionary type of choice require a 'name' key and a 'value' key"
|
|
132
|
+
)
|
|
133
|
+
return processed_choices
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def selected_choice_index(self) -> int:
|
|
137
|
+
"""int: Current highlighted index."""
|
|
138
|
+
return self._selected_choice_index
|
|
139
|
+
|
|
140
|
+
@selected_choice_index.setter
|
|
141
|
+
def selected_choice_index(self, value: int) -> None:
|
|
142
|
+
self._selected_choice_index = value
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def choices(self) -> List[Dict[str, Any]]:
|
|
146
|
+
"""List[Dict[str, Any]]: Get all processed choices."""
|
|
147
|
+
return self._choices
|
|
148
|
+
|
|
149
|
+
@choices.setter
|
|
150
|
+
def choices(self, value: List[Dict[str, Any]]) -> None:
|
|
151
|
+
self._choices = value
|
|
152
|
+
|
|
153
|
+
def _safety_check(self) -> None:
|
|
154
|
+
"""Validate processed choices.
|
|
155
|
+
|
|
156
|
+
Check if the choices are empty or if it only contains :class:`~InquirerPy.separator.Separator`.
|
|
157
|
+
"""
|
|
158
|
+
if not self.choices:
|
|
159
|
+
raise InvalidArgument("argument choices cannot be empty")
|
|
160
|
+
should_proceed: bool = False
|
|
161
|
+
for choice in self.choices:
|
|
162
|
+
if not isinstance(choice["value"], Separator):
|
|
163
|
+
should_proceed = True
|
|
164
|
+
break
|
|
165
|
+
if not should_proceed:
|
|
166
|
+
raise InvalidArgument(
|
|
167
|
+
"argument choices should contain choices other than separator"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _get_formatted_choices(self) -> List[Tuple[str, str]]:
|
|
171
|
+
"""Get all choices in formatted text format.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of choices in formatted text form.
|
|
175
|
+
"""
|
|
176
|
+
display_choices = []
|
|
177
|
+
|
|
178
|
+
for index, choice in enumerate(self.choices):
|
|
179
|
+
if index == self.selected_choice_index:
|
|
180
|
+
display_choices += self._get_hover_text(choice)
|
|
181
|
+
else:
|
|
182
|
+
display_choices += self._get_normal_text(choice)
|
|
183
|
+
display_choices.append(("", "\n"))
|
|
184
|
+
if display_choices:
|
|
185
|
+
display_choices.pop()
|
|
186
|
+
return display_choices
|
|
187
|
+
|
|
188
|
+
def _format_choices(self) -> None:
|
|
189
|
+
"""Perform post processing on the choices.
|
|
190
|
+
|
|
191
|
+
Additional customisation to the choices after :meth:`.InquirerPyUIListControl._get_choices` call.
|
|
192
|
+
"""
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
@abstractmethod
|
|
196
|
+
def _get_hover_text(self, choice) -> List[Tuple[str, str]]:
|
|
197
|
+
"""Generate the formatted text for hovered choice.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Formatted text in list of tuple format.
|
|
201
|
+
"""
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
@abstractmethod
|
|
205
|
+
def _get_normal_text(self, choice) -> List[Tuple[str, str]]:
|
|
206
|
+
"""Generate the formatted text for non-hovered choices.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Formatted text in list of tuple format.
|
|
210
|
+
"""
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def choice_count(self) -> int:
|
|
215
|
+
"""int: Total count of choices."""
|
|
216
|
+
return len(self.choices)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def selection(self) -> Dict[str, Any]:
|
|
220
|
+
"""Dict[str, Any]: Current selected choice."""
|
|
221
|
+
return self.choices[self.selected_choice_index]
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def loading(self) -> bool:
|
|
225
|
+
"""bool: Indicate if the content control is loading."""
|
|
226
|
+
return self._loading
|
|
227
|
+
|
|
228
|
+
@loading.setter
|
|
229
|
+
def loading(self, value: bool) -> None:
|
|
230
|
+
self._loading = value
|