Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/shortcuts/progress_bar/formatters.py: 43%

159 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 06:09 +0000

1""" 

2Formatter classes for the progress bar. 

3Each progress bar consists of a list of these formatters. 

4""" 

5from __future__ import annotations 

6 

7import datetime 

8import time 

9from abc import ABCMeta, abstractmethod 

10from typing import TYPE_CHECKING 

11 

12from prompt_toolkit.formatted_text import ( 

13 HTML, 

14 AnyFormattedText, 

15 StyleAndTextTuples, 

16 to_formatted_text, 

17) 

18from prompt_toolkit.formatted_text.utils import fragment_list_width 

19from prompt_toolkit.layout.dimension import AnyDimension, D 

20from prompt_toolkit.layout.utils import explode_text_fragments 

21from prompt_toolkit.utils import get_cwidth 

22 

23if TYPE_CHECKING: 

24 from .base import ProgressBar, ProgressBarCounter 

25 

26__all__ = [ 

27 "Formatter", 

28 "Text", 

29 "Label", 

30 "Percentage", 

31 "Bar", 

32 "Progress", 

33 "TimeElapsed", 

34 "TimeLeft", 

35 "IterationsPerSecond", 

36 "SpinningWheel", 

37 "Rainbow", 

38 "create_default_formatters", 

39] 

40 

41 

42class Formatter(metaclass=ABCMeta): 

43 """ 

44 Base class for any formatter. 

45 """ 

46 

47 @abstractmethod 

48 def format( 

49 self, 

50 progress_bar: ProgressBar, 

51 progress: ProgressBarCounter[object], 

52 width: int, 

53 ) -> AnyFormattedText: 

54 pass 

55 

56 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: 

57 return D() 

58 

59 

60class Text(Formatter): 

61 """ 

62 Display plain text. 

63 """ 

64 

65 def __init__(self, text: AnyFormattedText, style: str = "") -> None: 

66 self.text = to_formatted_text(text, style=style) 

67 

68 def format( 

69 self, 

70 progress_bar: ProgressBar, 

71 progress: ProgressBarCounter[object], 

72 width: int, 

73 ) -> AnyFormattedText: 

74 return self.text 

75 

76 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: 

77 return fragment_list_width(self.text) 

78 

79 

80class Label(Formatter): 

81 """ 

82 Display the name of the current task. 

83 

84 :param width: If a `width` is given, use this width. Scroll the text if it 

85 doesn't fit in this width. 

86 :param suffix: String suffix to be added after the task name, e.g. ': '. 

87 If no task name was given, no suffix will be added. 

88 """ 

89 

90 def __init__(self, width: AnyDimension = None, suffix: str = "") -> None: 

91 self.width = width 

92 self.suffix = suffix 

93 

94 def _add_suffix(self, label: AnyFormattedText) -> StyleAndTextTuples: 

95 label = to_formatted_text(label, style="class:label") 

96 return label + [("", self.suffix)] 

97 

98 def format( 

99 self, 

100 progress_bar: ProgressBar, 

101 progress: ProgressBarCounter[object], 

102 width: int, 

103 ) -> AnyFormattedText: 

104 label = self._add_suffix(progress.label) 

105 cwidth = fragment_list_width(label) 

106 

107 if cwidth > width: 

108 # It doesn't fit -> scroll task name. 

109 label = explode_text_fragments(label) 

110 max_scroll = cwidth - width 

111 current_scroll = int(time.time() * 3 % max_scroll) 

112 label = label[current_scroll:] 

113 

114 return label 

115 

116 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: 

117 if self.width: 

118 return self.width 

119 

120 all_labels = [self._add_suffix(c.label) for c in progress_bar.counters] 

121 if all_labels: 

122 max_widths = max(fragment_list_width(l) for l in all_labels) 

123 return D(preferred=max_widths, max=max_widths) 

124 else: 

125 return D() 

126 

127 

128class Percentage(Formatter): 

129 """ 

130 Display the progress as a percentage. 

131 """ 

132 

133 template = "<percentage>{percentage:>5}%</percentage>" 

134 

135 def format( 

136 self, 

137 progress_bar: ProgressBar, 

138 progress: ProgressBarCounter[object], 

139 width: int, 

140 ) -> AnyFormattedText: 

141 return HTML(self.template).format(percentage=round(progress.percentage, 1)) 

142 

143 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: 

144 return D.exact(6) 

145 

146 

147class Bar(Formatter): 

148 """ 

149 Display the progress bar itself. 

150 """ 

151 

152 template = "<bar>{start}<bar-a>{bar_a}</bar-a><bar-b>{bar_b}</bar-b><bar-c>{bar_c}</bar-c>{end}</bar>" 

153 

154 def __init__( 

155 self, 

156 start: str = "[", 

157 end: str = "]", 

158 sym_a: str = "=", 

159 sym_b: str = ">", 

160 sym_c: str = " ", 

161 unknown: str = "#", 

162 ) -> None: 

163 assert len(sym_a) == 1 and get_cwidth(sym_a) == 1 

164 assert len(sym_c) == 1 and get_cwidth(sym_c) == 1 

165 

166 self.start = start 

167 self.end = end 

168 self.sym_a = sym_a 

169 self.sym_b = sym_b 

170 self.sym_c = sym_c 

171 self.unknown = unknown 

172 

173 def format( 

174 self, 

175 progress_bar: ProgressBar, 

176 progress: ProgressBarCounter[object], 

177 width: int, 

178 ) -> AnyFormattedText: 

179 if progress.done or progress.total or progress.stopped: 

180 sym_a, sym_b, sym_c = self.sym_a, self.sym_b, self.sym_c 

181 

182 # Compute pb_a based on done, total, or stopped states. 

183 if progress.done: 

184 # 100% completed irrelevant of how much was actually marked as completed. 

185 percent = 1.0 

186 else: 

187 # Show percentage completed. 

188 percent = progress.percentage / 100 

189 else: 

190 # Total is unknown and bar is still running. 

191 sym_a, sym_b, sym_c = self.sym_c, self.unknown, self.sym_c 

192 

193 # Compute percent based on the time. 

194 percent = time.time() * 20 % 100 / 100 

195 

196 # Subtract left, sym_b, and right. 

197 width -= get_cwidth(self.start + sym_b + self.end) 

198 

199 # Scale percent by width 

200 pb_a = int(percent * width) 

201 bar_a = sym_a * pb_a 

202 bar_b = sym_b 

203 bar_c = sym_c * (width - pb_a) 

204 

205 return HTML(self.template).format( 

206 start=self.start, end=self.end, bar_a=bar_a, bar_b=bar_b, bar_c=bar_c 

207 ) 

208 

209 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: 

210 return D(min=9) 

211 

212 

213class Progress(Formatter): 

214 """ 

215 Display the progress as text. E.g. "8/20" 

216 """ 

217 

218 template = "<current>{current:>3}</current>/<total>{total:>3}</total>" 

219 

220 def format( 

221 self, 

222 progress_bar: ProgressBar, 

223 progress: ProgressBarCounter[object], 

224 width: int, 

225 ) -> AnyFormattedText: 

226 return HTML(self.template).format( 

227 current=progress.items_completed, total=progress.total or "?" 

228 ) 

229 

230 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: 

231 all_lengths = [ 

232 len("{:>3}".format(c.total or "?")) for c in progress_bar.counters 

233 ] 

234 all_lengths.append(1) 

235 return D.exact(max(all_lengths) * 2 + 1) 

236 

237 

238def _format_timedelta(timedelta: datetime.timedelta) -> str: 

239 """ 

240 Return hh:mm:ss, or mm:ss if the amount of hours is zero. 

241 """ 

242 result = f"{timedelta}".split(".")[0] 

243 if result.startswith("0:"): 

244 result = result[2:] 

245 return result 

246 

247 

248class TimeElapsed(Formatter): 

249 """ 

250 Display the elapsed time. 

251 """ 

252 

253 def format( 

254 self, 

255 progress_bar: ProgressBar, 

256 progress: ProgressBarCounter[object], 

257 width: int, 

258 ) -> AnyFormattedText: 

259 text = _format_timedelta(progress.time_elapsed).rjust(width) 

260 return HTML("<time-elapsed>{time_elapsed}</time-elapsed>").format( 

261 time_elapsed=text 

262 ) 

263 

264 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: 

265 all_values = [ 

266 len(_format_timedelta(c.time_elapsed)) for c in progress_bar.counters 

267 ] 

268 if all_values: 

269 return max(all_values) 

270 return 0 

271 

272 

273class TimeLeft(Formatter): 

274 """ 

275 Display the time left. 

276 """ 

277 

278 template = "<time-left>{time_left}</time-left>" 

279 unknown = "?:??:??" 

280 

281 def format( 

282 self, 

283 progress_bar: ProgressBar, 

284 progress: ProgressBarCounter[object], 

285 width: int, 

286 ) -> AnyFormattedText: 

287 time_left = progress.time_left 

288 if time_left is not None: 

289 formatted_time_left = _format_timedelta(time_left) 

290 else: 

291 formatted_time_left = self.unknown 

292 

293 return HTML(self.template).format(time_left=formatted_time_left.rjust(width)) 

294 

295 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: 

296 all_values = [ 

297 len(_format_timedelta(c.time_left)) if c.time_left is not None else 7 

298 for c in progress_bar.counters 

299 ] 

300 if all_values: 

301 return max(all_values) 

302 return 0 

303 

304 

305class IterationsPerSecond(Formatter): 

306 """ 

307 Display the iterations per second. 

308 """ 

309 

310 template = ( 

311 "<iterations-per-second>{iterations_per_second:.2f}</iterations-per-second>" 

312 ) 

313 

314 def format( 

315 self, 

316 progress_bar: ProgressBar, 

317 progress: ProgressBarCounter[object], 

318 width: int, 

319 ) -> AnyFormattedText: 

320 value = progress.items_completed / progress.time_elapsed.total_seconds() 

321 return HTML(self.template.format(iterations_per_second=value)) 

322 

323 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: 

324 all_values = [ 

325 len(f"{c.items_completed / c.time_elapsed.total_seconds():.2f}") 

326 for c in progress_bar.counters 

327 ] 

328 if all_values: 

329 return max(all_values) 

330 return 0 

331 

332 

333class SpinningWheel(Formatter): 

334 """ 

335 Display a spinning wheel. 

336 """ 

337 

338 characters = r"/-\|" 

339 

340 def format( 

341 self, 

342 progress_bar: ProgressBar, 

343 progress: ProgressBarCounter[object], 

344 width: int, 

345 ) -> AnyFormattedText: 

346 index = int(time.time() * 3) % len(self.characters) 

347 return HTML("<spinning-wheel>{0}</spinning-wheel>").format( 

348 self.characters[index] 

349 ) 

350 

351 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: 

352 return D.exact(1) 

353 

354 

355def _hue_to_rgb(hue: float) -> tuple[int, int, int]: 

356 """ 

357 Take hue between 0 and 1, return (r, g, b). 

358 """ 

359 i = int(hue * 6.0) 

360 f = (hue * 6.0) - i 

361 

362 q = int(255 * (1.0 - f)) 

363 t = int(255 * (1.0 - (1.0 - f))) 

364 

365 i %= 6 

366 

367 return [ 

368 (255, t, 0), 

369 (q, 255, 0), 

370 (0, 255, t), 

371 (0, q, 255), 

372 (t, 0, 255), 

373 (255, 0, q), 

374 ][i] 

375 

376 

377class Rainbow(Formatter): 

378 """ 

379 For the fun. Add rainbow colors to any of the other formatters. 

380 """ 

381 

382 colors = ["#%.2x%.2x%.2x" % _hue_to_rgb(h / 100.0) for h in range(0, 100)] 

383 

384 def __init__(self, formatter: Formatter) -> None: 

385 self.formatter = formatter 

386 

387 def format( 

388 self, 

389 progress_bar: ProgressBar, 

390 progress: ProgressBarCounter[object], 

391 width: int, 

392 ) -> AnyFormattedText: 

393 # Get formatted text from nested formatter, and explode it in 

394 # text/style tuples. 

395 result = self.formatter.format(progress_bar, progress, width) 

396 result = explode_text_fragments(to_formatted_text(result)) 

397 

398 # Insert colors. 

399 result2: StyleAndTextTuples = [] 

400 shift = int(time.time() * 3) % len(self.colors) 

401 

402 for i, (style, text, *_) in enumerate(result): 

403 result2.append( 

404 (style + " " + self.colors[(i + shift) % len(self.colors)], text) 

405 ) 

406 return result2 

407 

408 def get_width(self, progress_bar: ProgressBar) -> AnyDimension: 

409 return self.formatter.get_width(progress_bar) 

410 

411 

412def create_default_formatters() -> list[Formatter]: 

413 """ 

414 Return the list of default formatters. 

415 """ 

416 return [ 

417 Label(), 

418 Text(" "), 

419 Percentage(), 

420 Text(" "), 

421 Bar(), 

422 Text(" "), 

423 Progress(), 

424 Text(" "), 

425 Text("eta [", style="class:time-left"), 

426 TimeLeft(), 

427 Text("]", style="class:time-left"), 

428 Text(" "), 

429 ]