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.
Files changed (34) hide show
  1. inquirerprompt-0.3.0/InquirerPy/__init__.py +4 -0
  2. inquirerprompt-0.3.0/InquirerPy/base/__init__.py +24 -0
  3. inquirerprompt-0.3.0/InquirerPy/base/complex.py +295 -0
  4. inquirerprompt-0.3.0/InquirerPy/base/control.py +230 -0
  5. inquirerprompt-0.3.0/InquirerPy/base/list.py +239 -0
  6. inquirerprompt-0.3.0/InquirerPy/base/simple.py +379 -0
  7. inquirerprompt-0.3.0/InquirerPy/containers/__init__.py +3 -0
  8. inquirerprompt-0.3.0/InquirerPy/containers/instruction.py +38 -0
  9. inquirerprompt-0.3.0/InquirerPy/containers/message.py +42 -0
  10. inquirerprompt-0.3.0/InquirerPy/containers/spinner.py +109 -0
  11. inquirerprompt-0.3.0/InquirerPy/containers/validation.py +60 -0
  12. inquirerprompt-0.3.0/InquirerPy/enum.py +8 -0
  13. inquirerprompt-0.3.0/InquirerPy/exceptions.py +25 -0
  14. inquirerprompt-0.3.0/InquirerPy/inquirer.py +31 -0
  15. inquirerprompt-0.3.0/InquirerPy/prompts/__init__.py +25 -0
  16. inquirerprompt-0.3.0/InquirerPy/prompts/checkbox.py +249 -0
  17. inquirerprompt-0.3.0/InquirerPy/prompts/confirm.py +202 -0
  18. inquirerprompt-0.3.0/InquirerPy/prompts/expand.py +459 -0
  19. inquirerprompt-0.3.0/InquirerPy/prompts/filepath.py +193 -0
  20. inquirerprompt-0.3.0/InquirerPy/prompts/fuzzy.py +685 -0
  21. inquirerprompt-0.3.0/InquirerPy/prompts/input.py +256 -0
  22. inquirerprompt-0.3.0/InquirerPy/prompts/list.py +371 -0
  23. inquirerprompt-0.3.0/InquirerPy/prompts/number.py +621 -0
  24. inquirerprompt-0.3.0/InquirerPy/prompts/rawlist.py +286 -0
  25. inquirerprompt-0.3.0/InquirerPy/prompts/secret.py +137 -0
  26. inquirerprompt-0.3.0/InquirerPy/py.typed +1 -0
  27. inquirerprompt-0.3.0/InquirerPy/resolver.py +220 -0
  28. inquirerprompt-0.3.0/InquirerPy/separator.py +23 -0
  29. inquirerprompt-0.3.0/InquirerPy/utils.py +291 -0
  30. inquirerprompt-0.3.0/InquirerPy/validator.py +166 -0
  31. inquirerprompt-0.3.0/LICENSE +21 -0
  32. inquirerprompt-0.3.0/PKG-INFO +217 -0
  33. inquirerprompt-0.3.0/README.md +175 -0
  34. inquirerprompt-0.3.0/pyproject.toml +97 -0
@@ -0,0 +1,4 @@
1
+ __all__ = ["prompt", "prompt_async", "get_style"]
2
+
3
+ from InquirerPy.resolver import prompt, prompt_async
4
+ from InquirerPy.utils import get_style
@@ -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