Coverage for harbor_cli/output/prompts.py: 66%
80 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
1from __future__ import annotations
3import math
4from pathlib import Path
5from typing import Any
6from typing import overload
7from typing import Type
9from rich.prompt import Confirm
10from rich.prompt import FloatPrompt
11from rich.prompt import IntPrompt
12from rich.prompt import Prompt
14from .console import console
15from .console import err_console
16from .formatting import path_link
19def str_prompt(
20 prompt: str,
21 default: Any = ...,
22 password: bool = False,
23 show_default: bool = True,
24 choices: list[str] | None = None,
25 empty_ok: bool = False,
26 **kwargs: Any,
27) -> str:
28 """Prompts the user for a string input. Optionally controls
29 for empty input. Loops until a valid input is provided.
31 Parameters
32 ----------
33 prompt : str
34 Prompt to display to the user.
35 default : Any, optional
36 Default value to use if the user does not provide input.
37 If not provided, the user will be required to provide input.
38 password : bool, optional
39 Whether to hide the input, by default False
40 show_default : bool, optional
41 Whether to show the default value, by default True
42 `password=True` supercedes this option, and sets it to False.
43 empty_ok : bool, optional
44 Whether to allow input consisting of only whitespace, by default False
46 """
47 # Don't permit secrets to be shown ever
48 if password:
49 show_default = False
51 # Notify user that a default secret will be used,
52 # but don't actually show the secret
53 if password and default not in (None, ..., ""):
54 _add_str = " (leave empty to use existing value)"
55 else:
56 _add_str = ""
58 inp = None
60 while not inp:
61 inp = Prompt.ask(
62 f"{prompt}{_add_str}",
63 console=console,
64 password=password,
65 show_default=show_default,
66 default=default,
67 choices=choices,
68 **kwargs,
69 )
70 if empty_ok: # nothing else to check 70 ↛ 71line 70 didn't jump to line 71, because the condition on line 70 was never true
71 break
73 if not inp: 73 ↛ 74line 73 didn't jump to line 74, because the condition on line 73 was never true
74 err_console.print("Input cannot be empty.")
75 elif inp.isspace() and inp != default: 75 ↛ 76line 75 didn't jump to line 76, because the condition on line 75 was never true
76 err_console.print("Input cannot solely consist of whitespace.")
77 else:
78 break
79 return inp
82def int_prompt(
83 prompt: str,
84 default: int | None = None,
85 show_default: bool = True,
86 min: int | None = None,
87 max: int | None = None,
88 show_range: bool = True,
89 **kwargs: Any,
90) -> int:
91 return _number_prompt(
92 IntPrompt,
93 prompt,
94 default=default,
95 show_default=show_default,
96 min=min,
97 max=max,
98 show_range=show_range,
99 **kwargs,
100 )
103def float_prompt(
104 prompt: str,
105 default: float | None = None,
106 show_default: bool = True,
107 min: float | None = None,
108 max: float | None = None,
109 show_range: bool = True,
110 **kwargs: Any,
111) -> float:
112 val = _number_prompt(
113 FloatPrompt,
114 prompt,
115 default=default,
116 show_default=show_default,
117 min=min,
118 max=max,
119 show_range=show_range,
120 **kwargs,
121 )
122 # explicit cast to float since users might pass in int as default
123 # and we have no logic inside _number_prompt to handle that
124 return float(val)
127@overload
128def _number_prompt(
129 prompt_type: Type[IntPrompt],
130 prompt: str,
131 default: int | float | None = ...,
132 show_default: bool = ...,
133 min: int | float | None = ...,
134 max: int | float | None = ...,
135 show_range: bool = ...,
136 **kwargs: Any,
137) -> int:
138 ...
141@overload
142def _number_prompt(
143 prompt_type: Type[FloatPrompt],
144 prompt: str,
145 default: int | float | None = ...,
146 show_default: bool = ...,
147 min: int | float | None = ...,
148 max: int | float | None = ...,
149 show_range: bool = ...,
150 **kwargs: Any,
151) -> float:
152 ...
155def _number_prompt(
156 prompt_type: Type[IntPrompt] | Type[FloatPrompt],
157 prompt: str,
158 default: int | float | None = None,
159 show_default: bool = True,
160 min: int | float | None = None,
161 max: int | float | None = None,
162 show_range: bool = True,
163 **kwargs: Any,
164) -> int | float:
165 default_arg = ... if default is None else default
167 if show_range: 167 ↛ 180line 167 didn't jump to line 180, because the condition on line 167 was never false
168 _prompt_add = ""
169 if min is not None and max is not None: 169 ↛ 170line 169 didn't jump to line 170, because the condition on line 169 was never true
170 if min > max:
171 raise ValueError("min must be less than or equal to max")
172 _prompt_add = f"{min}<=x<={max}"
173 elif min is not None: 173 ↛ 174line 173 didn't jump to line 174, because the condition on line 173 was never true
174 _prompt_add = f"x>={min}"
175 elif max is not None: 175 ↛ 176line 175 didn't jump to line 176, because the condition on line 175 was never true
176 _prompt_add = f"x<={max}"
177 if _prompt_add: 177 ↛ 178line 177 didn't jump to line 178, because the condition on line 177 was never true
178 prompt = f"{prompt} [yellow][{_prompt_add}][/]"
180 while True:
181 val = prompt_type.ask(
182 prompt,
183 console=console,
184 default=default_arg,
185 show_default=show_default,
186 **kwargs,
187 )
189 # Shouldn't happen, but ask() returns DefaultType | int | float
190 # so it thinks we could have an ellipsis here
191 if not isinstance(val, (int, float)): 191 ↛ 192line 191 didn't jump to line 192, because the condition on line 191 was never true
192 err_console.print("Value must be a number")
193 continue
194 if math.isnan(val):
195 err_console.print("Value can't be NaN")
196 continue
197 if min is not None and val < min: 197 ↛ 198line 197 didn't jump to line 198, because the condition on line 197 was never true
198 err_console.print(f"Value must be greater or equal to {min}")
199 continue
200 if max is not None and val > max: 200 ↛ 201line 200 didn't jump to line 201, because the condition on line 200 was never true
201 err_console.print(f"Value must be less than or equal to {max}")
202 continue
203 return val
206def bool_prompt(
207 prompt: str,
208 default: Any = ...,
209 show_default: bool = True,
210 **kwargs: Any,
211) -> bool:
212 return Confirm.ask(
213 prompt,
214 console=console,
215 show_default=show_default,
216 default=default,
217 **kwargs,
218 )
221def path_prompt(
222 prompt: str,
223 default: Any = ...,
224 show_default: bool = True,
225 exist_ok: bool = True,
226 must_exist: bool = False,
227 **kwargs: Any,
228) -> Path:
229 if isinstance(default, Path): 229 ↛ 230line 229 didn't jump to line 230, because the condition on line 229 was never true
230 default_arg = str(default)
231 elif default is None: 231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true
232 default_arg = ... # type: ignore
233 else:
234 default_arg = default
236 while True:
237 path_str = str_prompt(
238 prompt,
239 default=default_arg,
240 show_default=show_default,
241 **kwargs,
242 )
243 path = Path(path_str)
245 if must_exist and not path.exists(): 245 ↛ 246line 245 didn't jump to line 246, because the condition on line 245 was never true
246 err_console.print(f"Path does not exist: {path_link(path)}")
247 elif not exist_ok and path.exists(): 247 ↛ 248line 247 didn't jump to line 248, because the condition on line 247 was never true
248 err_console.print(f"Path already exists: {path_link(path)}")
249 else:
250 return path