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

1from __future__ import annotations 

2 

3import math 

4from pathlib import Path 

5from typing import Any 

6from typing import overload 

7from typing import Type 

8 

9from rich.prompt import Confirm 

10from rich.prompt import FloatPrompt 

11from rich.prompt import IntPrompt 

12from rich.prompt import Prompt 

13 

14from .console import console 

15from .console import err_console 

16from .formatting import path_link 

17 

18 

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. 

30 

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 

45 

46 """ 

47 # Don't permit secrets to be shown ever 

48 if password: 

49 show_default = False 

50 

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

57 

58 inp = None 

59 

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 

72 

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 

80 

81 

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 ) 

101 

102 

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) 

125 

126 

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

139 

140 

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

153 

154 

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 

166 

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}][/]" 

179 

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 ) 

188 

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 

204 

205 

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 ) 

219 

220 

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 

235 

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) 

244 

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