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
« 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
7import datetime
8import time
9from abc import ABCMeta, abstractmethod
10from typing import TYPE_CHECKING
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
23if TYPE_CHECKING:
24 from .base import ProgressBar, ProgressBarCounter
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]
42class Formatter(metaclass=ABCMeta):
43 """
44 Base class for any formatter.
45 """
47 @abstractmethod
48 def format(
49 self,
50 progress_bar: ProgressBar,
51 progress: ProgressBarCounter[object],
52 width: int,
53 ) -> AnyFormattedText:
54 pass
56 def get_width(self, progress_bar: ProgressBar) -> AnyDimension:
57 return D()
60class Text(Formatter):
61 """
62 Display plain text.
63 """
65 def __init__(self, text: AnyFormattedText, style: str = "") -> None:
66 self.text = to_formatted_text(text, style=style)
68 def format(
69 self,
70 progress_bar: ProgressBar,
71 progress: ProgressBarCounter[object],
72 width: int,
73 ) -> AnyFormattedText:
74 return self.text
76 def get_width(self, progress_bar: ProgressBar) -> AnyDimension:
77 return fragment_list_width(self.text)
80class Label(Formatter):
81 """
82 Display the name of the current task.
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 """
90 def __init__(self, width: AnyDimension = None, suffix: str = "") -> None:
91 self.width = width
92 self.suffix = suffix
94 def _add_suffix(self, label: AnyFormattedText) -> StyleAndTextTuples:
95 label = to_formatted_text(label, style="class:label")
96 return label + [("", self.suffix)]
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)
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:]
114 return label
116 def get_width(self, progress_bar: ProgressBar) -> AnyDimension:
117 if self.width:
118 return self.width
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()
128class Percentage(Formatter):
129 """
130 Display the progress as a percentage.
131 """
133 template = "<percentage>{percentage:>5}%</percentage>"
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))
143 def get_width(self, progress_bar: ProgressBar) -> AnyDimension:
144 return D.exact(6)
147class Bar(Formatter):
148 """
149 Display the progress bar itself.
150 """
152 template = "<bar>{start}<bar-a>{bar_a}</bar-a><bar-b>{bar_b}</bar-b><bar-c>{bar_c}</bar-c>{end}</bar>"
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
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
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
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
193 # Compute percent based on the time.
194 percent = time.time() * 20 % 100 / 100
196 # Subtract left, sym_b, and right.
197 width -= get_cwidth(self.start + sym_b + self.end)
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)
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 )
209 def get_width(self, progress_bar: ProgressBar) -> AnyDimension:
210 return D(min=9)
213class Progress(Formatter):
214 """
215 Display the progress as text. E.g. "8/20"
216 """
218 template = "<current>{current:>3}</current>/<total>{total:>3}</total>"
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 )
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)
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
248class TimeElapsed(Formatter):
249 """
250 Display the elapsed time.
251 """
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 )
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
273class TimeLeft(Formatter):
274 """
275 Display the time left.
276 """
278 template = "<time-left>{time_left}</time-left>"
279 unknown = "?:??:??"
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
293 return HTML(self.template).format(time_left=formatted_time_left.rjust(width))
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
305class IterationsPerSecond(Formatter):
306 """
307 Display the iterations per second.
308 """
310 template = (
311 "<iterations-per-second>{iterations_per_second:.2f}</iterations-per-second>"
312 )
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))
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
333class SpinningWheel(Formatter):
334 """
335 Display a spinning wheel.
336 """
338 characters = r"/-\|"
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 )
351 def get_width(self, progress_bar: ProgressBar) -> AnyDimension:
352 return D.exact(1)
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
362 q = int(255 * (1.0 - f))
363 t = int(255 * (1.0 - (1.0 - f)))
365 i %= 6
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]
377class Rainbow(Formatter):
378 """
379 For the fun. Add rainbow colors to any of the other formatters.
380 """
382 colors = ["#%.2x%.2x%.2x" % _hue_to_rgb(h / 100.0) for h in range(0, 100)]
384 def __init__(self, formatter: Formatter) -> None:
385 self.formatter = formatter
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))
398 # Insert colors.
399 result2: StyleAndTextTuples = []
400 shift = int(time.time() * 3) % len(self.colors)
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
408 def get_width(self, progress_bar: ProgressBar) -> AnyDimension:
409 return self.formatter.get_width(progress_bar)
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 ]