-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwidgets.py
More file actions
939 lines (799 loc) · 39.3 KB
/
widgets.py
File metadata and controls
939 lines (799 loc) · 39.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
from __future__ import annotations
import abc
import contextlib
import datetime
import functools
import logging
import typing
from typing import ClassVar
from python_utils import containers, converters, types
from . import algorithms, base, terminal, utils
from .terminal import colors
if types.TYPE_CHECKING:
from .bar import ProgressBarMixinBase
logger = logging.getLogger(__name__)
MAX_DATE = datetime.date.max
MAX_TIME = datetime.time.max
MAX_DATETIME = datetime.datetime.max
Data = types.Dict[str, types.Any]
FormatString = typing.Optional[str]
T = typing.TypeVar('T')
def string_or_lambda(input_value):
"""Convert a string to a lambda or return the callable."""
if callable(input_value):
return input_value
return lambda progress, data: input_value
def create_marker(marker, marker_wrap=None):
"""Create a marker function that wraps the marker string if needed."""
if callable(marker):
return marker
if marker_wrap:
return lambda progress, data: marker_wrap % marker
return lambda progress, data: marker
def create_wrapper(wrapper):
"""Convert a wrapper tuple or format string to a format string.
>>> create_wrapper('')
>>> print(create_wrapper('a{}b'))
a{}b
>>> print(create_wrapper(('a', 'b')))
a{}b
"""
if not wrapper:
return None
elif isinstance(wrapper, tuple):
return wrapper[0] + '{}' + wrapper[1]
else:
return wrapper
def wrapper(function, wrapper_):
"""Wrap the output of a function in a template string or a tuple with
begin/end strings.
"""
wrapper_format = create_wrapper(wrapper_)
def wrap_text(progress, data):
text = function(progress, data)
return wrapper_format.format(text) if wrapper_format else text
return wrap_text
class FormatWidgetMixin(abc.ABC):
"""Mixin to format widgets using a formatstring.
Variables available:
- max_value: The maximum value (can be None with iterators)
- value: The current value
- total_seconds_elapsed: The seconds since the bar started
- seconds_elapsed: The seconds since the bar started modulo 60
- minutes_elapsed: The minutes since the bar started modulo 60
- hours_elapsed: The hours since the bar started modulo 24
- days_elapsed: The hours since the bar started
- time_elapsed: Shortcut for HH:MM:SS time since the bar started including
days
- percentage: Percentage as a float
"""
def __init__(self, format: str, new_style: bool=False, **kwargs):
self.new_style = new_style
self.format = format
def __call__(self, progress: ProgressBarMixinBase, data: Data, format: types.Optional[str]=None) -> str:
"""Formats the widget into a string."""
format_ = self.get_format(progress, data, format)
try:
if self.new_style:
return format_.format(**data)
else:
return format_ % data
except (TypeError, KeyError):
logger.exception('Error while formatting %r with data: %r', format_, data)
raise
class WidthWidgetMixin(abc.ABC):
"""Mixing to make sure widgets are only visible if the screen is within a
specified size range so the progressbar fits on both large and small
screens.
Variables available:
- min_width: Only display the widget if at least `min_width` is left
- max_width: Only display the widget if at most `max_width` is left
>>> class Progress:
... term_width = 0
>>> WidthWidgetMixin(5, 10).check_size(Progress)
False
>>> Progress.term_width = 5
>>> WidthWidgetMixin(5, 10).check_size(Progress)
True
>>> Progress.term_width = 10
>>> WidthWidgetMixin(5, 10).check_size(Progress)
True
>>> Progress.term_width = 11
>>> WidthWidgetMixin(5, 10).check_size(Progress)
False
"""
def __init__(self, min_width=None, max_width=None, **kwargs):
self.min_width = min_width
self.max_width = max_width
class TGradientColors(typing.TypedDict):
fg: types.Optional[terminal.OptionalColor | None]
bg: types.Optional[terminal.OptionalColor | None]
class TFixedColors(typing.TypedDict):
fg_none: types.Optional[terminal.Color | None]
bg_none: types.Optional[terminal.Color | None]
class WidgetBase(WidthWidgetMixin, metaclass=abc.ABCMeta):
"""The base class for all widgets.
The ProgressBar will call the widget's update value when the widget should
be updated. The widget's size may change between calls, but the widget may
display incorrectly if the size changes drastically and repeatedly.
The INTERVAL timedelta informs the ProgressBar that it should be
updated more often because it is time sensitive.
The widgets are only visible if the screen is within a
specified size range so the progressbar fits on both large and small
screens.
WARNING: Widgets can be shared between multiple progressbars so any state
information specific to a progressbar should be stored within the
progressbar instead of the widget.
Variables available:
- min_width: Only display the widget if at least `min_width` is left
- max_width: Only display the widget if at most `max_width` is left
- weight: Widgets with a higher `weight` will be calculated before widgets
with a lower one
- copy: Copy this widget when initializing the progress bar so the
progressbar can be reused. Some widgets such as the FormatCustomText
require the shared state so this needs to be optional
"""
copy = True
@abc.abstractmethod
def __call__(self, progress: ProgressBarMixinBase, data: Data) -> str:
"""Updates the widget.
progress - a reference to the calling ProgressBar
"""
_fixed_colors: ClassVar[TFixedColors] = TFixedColors(fg_none=None, bg_none=None)
_gradient_colors: ClassVar[TGradientColors] = TGradientColors(fg=None, bg=None)
_len: typing.Callable[[str | bytes], int] = len
@property
def uses_colors(self) -> bool:
"""Check if this widget uses colors."""
return bool(self._fixed_colors.get('fg_none') or self._fixed_colors.get('bg_none') or
self._gradient_colors.get('fg') or self._gradient_colors.get('bg'))
def __init__(self, *args, fixed_colors=None, gradient_colors=None, **kwargs):
if fixed_colors is not None:
self._fixed_colors.update(fixed_colors)
if gradient_colors is not None:
self._gradient_colors.update(gradient_colors)
if self.uses_colors:
self._len = utils.len_color
super().__init__(*args, **kwargs)
class AutoWidthWidgetBase(WidgetBase, metaclass=abc.ABCMeta):
"""The base class for all variable width widgets.
This widget is much like the \\hfill command in TeX, it will expand to
fill the line. You can use more than one in the same line, and they will
all have the same width, and together will fill the line.
"""
@abc.abstractmethod
def __call__(self, progress: ProgressBarMixinBase, data: Data, width: int=0) -> str:
"""Updates the widget providing the total width the widget must fill.
progress - a reference to the calling ProgressBar
width - The total width the widget must fill
"""
class TimeSensitiveWidgetBase(WidgetBase, metaclass=abc.ABCMeta):
"""The base class for all time sensitive widgets.
Some widgets like timers would become out of date unless updated at least
every `INTERVAL`
"""
INTERVAL = datetime.timedelta(milliseconds=100)
class FormatLabel(FormatWidgetMixin, WidgetBase):
"""Displays a formatted label.
>>> label = FormatLabel('%(value)s', min_width=5, max_width=10)
>>> class Progress:
... pass
>>> label = FormatLabel('{value} :: {value:^6}', new_style=True)
>>> str(label(Progress, dict(value='test')))
'test :: test '
"""
mapping: ClassVar[types.Dict[str, types.Tuple[str, types.Any]]] = dict(finished=('end_time', None), last_update=('last_update_time', None), max=('max_value', None), seconds=('seconds_elapsed', None), start=('start_time', None), elapsed=('total_seconds_elapsed', utils.format_time), value=('value', None))
def __init__(self, format: str, **kwargs):
FormatWidgetMixin.__init__(self, format=format, **kwargs)
WidgetBase.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, format: types.Optional[str]=None):
for name, (key, transform) in self.mapping.items():
with contextlib.suppress(KeyError, ValueError, IndexError):
if transform is None:
data[name] = data[key]
else:
data[name] = transform(data[key])
return FormatWidgetMixin.__call__(self, progress, data, format)
class Timer(FormatLabel, TimeSensitiveWidgetBase):
"""WidgetBase which displays the elapsed seconds."""
def __init__(self, format='Elapsed Time: %(elapsed)s', **kwargs):
if '%s' in format and '%(elapsed)s' not in format:
format = format.replace('%s', '%(elapsed)s')
FormatLabel.__init__(self, format=format, **kwargs)
TimeSensitiveWidgetBase.__init__(self, **kwargs)
format_time = staticmethod(utils.format_time)
class SamplesMixin(TimeSensitiveWidgetBase, metaclass=abc.ABCMeta):
"""
Mixing for widgets that average multiple measurements.
Note that samples can be either an integer or a timedelta to indicate a
certain amount of time
>>> class progress:
... last_update_time = datetime.datetime.now()
... value = 1
... extra = dict()
>>> samples = SamplesMixin(samples=2)
>>> samples(progress, None, True)
(None, None)
>>> progress.last_update_time += datetime.timedelta(seconds=1)
>>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
True
>>> progress.last_update_time += datetime.timedelta(seconds=1)
>>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
True
>>> samples = SamplesMixin(samples=datetime.timedelta(seconds=1))
>>> _, value = samples(progress, None)
>>> value
SliceableDeque([1, 1])
>>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
True
"""
def __init__(self, samples=datetime.timedelta(seconds=2), key_prefix=None, **kwargs):
self.samples = samples
self.key_prefix = (key_prefix or self.__class__.__name__) + '_'
TimeSensitiveWidgetBase.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, delta: bool=False):
sample_times = self.get_sample_times(progress, data)
sample_values = self.get_sample_values(progress, data)
if sample_times:
sample_time = sample_times[-1]
else:
sample_time = datetime.datetime.min
if progress.last_update_time - sample_time > self.INTERVAL:
sample_times.append(progress.last_update_time)
sample_values.append(progress.value)
if isinstance(self.samples, datetime.timedelta):
minimum_time = progress.last_update_time - self.samples
minimum_value = sample_values[-1]
while sample_times[2:] and minimum_time > sample_times[1] and (minimum_value > sample_values[1]):
sample_times.pop(0)
sample_values.pop(0)
elif len(sample_times) > self.samples:
sample_times.pop(0)
sample_values.pop(0)
if delta:
if (delta_time := (sample_times[-1] - sample_times[0])):
delta_value = sample_values[-1] - sample_values[0]
return (delta_time, delta_value)
else:
return (None, None)
else:
return (sample_times, sample_values)
class ETA(Timer):
"""WidgetBase which attempts to estimate the time of arrival."""
def __init__(self, format_not_started='ETA: --:--:--', format_finished='Time: %(elapsed)8s', format='ETA: %(eta)8s', format_zero='ETA: 00:00:00', format_na='ETA: N/A', **kwargs):
if '%s' in format and '%(eta)s' not in format:
format = format.replace('%s', '%(eta)s')
Timer.__init__(self, **kwargs)
self.format_not_started = format_not_started
self.format_finished = format_finished
self.format = format
self.format_zero = format_zero
self.format_NA = format_na
def _calculate_eta(self, progress: ProgressBarMixinBase, data: Data, value, elapsed):
"""Updates the widget to show the ETA or total time when finished."""
pass
def __call__(self, progress: ProgressBarMixinBase, data: Data, value=None, elapsed=None):
"""Updates the widget to show the ETA or total time when finished."""
if value is None:
value = data['value']
if elapsed is None:
elapsed = data['time_elapsed']
eta_na = False
try:
data['eta_seconds'] = self._calculate_eta(progress, data, value=value, elapsed=elapsed)
except TypeError:
data['eta_seconds'] = None
eta_na = True
data['eta'] = None
if data['eta_seconds']:
with contextlib.suppress(ValueError, OverflowError):
data['eta'] = utils.format_time(data['eta_seconds'])
if data['value'] == progress.min_value:
fmt = self.format_not_started
elif progress.end_time:
fmt = self.format_finished
elif data['eta']:
fmt = self.format
elif eta_na:
fmt = self.format_NA
else:
fmt = self.format_zero
return Timer.__call__(self, progress, data, format=fmt)
class AbsoluteETA(ETA):
"""Widget which attempts to estimate the absolute time of arrival."""
def __init__(self, format_not_started='Estimated finish time: ----/--/-- --:--:--', format_finished='Finished at: %(elapsed)s', format='Estimated finish time: %(eta)s', **kwargs):
ETA.__init__(self, format_not_started=format_not_started, format_finished=format_finished, format=format, **kwargs)
class AdaptiveETA(ETA, SamplesMixin):
"""WidgetBase which attempts to estimate the time of arrival.
Uses a sampled average of the speed based on the 10 last updates.
Very convenient for resuming the progress halfway.
"""
exponential_smoothing: bool
exponential_smoothing_factor: float
def __init__(self, exponential_smoothing=True, exponential_smoothing_factor=0.1, **kwargs):
self.exponential_smoothing = exponential_smoothing
self.exponential_smoothing_factor = exponential_smoothing_factor
ETA.__init__(self, **kwargs)
SamplesMixin.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, value=None, elapsed=None):
elapsed, value = SamplesMixin.__call__(self, progress, data, delta=True)
if not elapsed:
value = None
elapsed = 0
return ETA.__call__(self, progress, data, value=value, elapsed=elapsed)
class SmoothingETA(ETA):
"""
WidgetBase which attempts to estimate the time of arrival using an
exponential moving average (EMA) of the speed.
EMA applies more weight to recent data points and less to older ones,
and doesn't require storing all past values. This approach works well
with varying data points and smooths out fluctuations effectively.
"""
smoothing_algorithm: algorithms.SmoothingAlgorithm
smoothing_parameters: dict[str, float]
def __init__(self, smoothing_algorithm: type[algorithms.SmoothingAlgorithm]=algorithms.ExponentialMovingAverage, smoothing_parameters: dict[str, float] | None=None, **kwargs):
self.smoothing_parameters = smoothing_parameters or {}
self.smoothing_algorithm = smoothing_algorithm(**self.smoothing_parameters or {})
ETA.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, value=None, elapsed=None):
if value is None:
value = data['value']
if elapsed is None:
elapsed = data['time_elapsed']
self.smoothing_algorithm.update(value, elapsed)
return ETA.__call__(self, progress, data, value=value, elapsed=elapsed)
class DataSize(FormatWidgetMixin, WidgetBase):
"""
Widget for showing an amount of data transferred/processed.
Automatically formats the value (assumed to be a count of bytes) with an
appropriate sized unit, based on the IEC binary prefixes (powers of 1024).
"""
def __init__(self, variable='value', format='%(scaled)5.1f %(prefix)s%(unit)s', unit='B', prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), **kwargs):
self.variable = variable
self.unit = unit
self.prefixes = prefixes
FormatWidgetMixin.__init__(self, format=format, **kwargs)
WidgetBase.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, format: types.Optional[str]=None):
value = data[self.variable]
if value is not None:
scaled, power = utils.scale_1024(value, len(self.prefixes))
else:
scaled = power = 0
data['scaled'] = scaled
data['prefix'] = self.prefixes[power]
data['unit'] = self.unit
return FormatWidgetMixin.__call__(self, progress, data, format)
class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase):
"""
Widget for showing the current transfer speed (useful for file transfers).
"""
def __init__(self, format='%(scaled)5.1f %(prefix)s%(unit)-s/s', inverse_format='%(scaled)5.1f s/%(prefix)s%(unit)-s', unit='B', prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), **kwargs):
self.unit = unit
self.prefixes = prefixes
self.inverse_format = inverse_format
FormatWidgetMixin.__init__(self, format=format, **kwargs)
TimeSensitiveWidgetBase.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data, value=None, total_seconds_elapsed=None):
"""Updates the widget with the current SI prefixed speed."""
if value is None:
value = data['value']
elapsed = utils.deltas_to_seconds(total_seconds_elapsed, data['total_seconds_elapsed'])
if value is not None and elapsed is not None and (elapsed > 2e-06) and (value > 2e-06):
scaled, power = self._speed(value, elapsed)
else:
scaled = power = 0
data['unit'] = self.unit
if power == 0 and scaled < 0.1:
if scaled > 0:
scaled = 1 / scaled
data['scaled'] = scaled
data['prefix'] = self.prefixes[0]
return FormatWidgetMixin.__call__(self, progress, data, self.inverse_format)
else:
data['scaled'] = scaled
data['prefix'] = self.prefixes[power]
return FormatWidgetMixin.__call__(self, progress, data)
class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin):
"""Widget for showing the transfer speed based on the last X samples."""
def __init__(self, **kwargs):
FileTransferSpeed.__init__(self, **kwargs)
SamplesMixin.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data, value=None, total_seconds_elapsed=None):
elapsed, value = SamplesMixin.__call__(self, progress, data, delta=True)
return FileTransferSpeed.__call__(self, progress, data, value, elapsed)
class AnimatedMarker(TimeSensitiveWidgetBase):
"""An animated marker for the progress bar which defaults to appear as if
it were rotating.
"""
def __init__(self, markers='|/-\\', default=None, fill='', marker_wrap=None, fill_wrap=None, **kwargs):
self.markers = markers
self.marker_wrap = create_wrapper(marker_wrap)
self.default = default or markers[0]
self.fill_wrap = create_wrapper(fill_wrap)
self.fill = create_marker(fill, self.fill_wrap) if fill else None
WidgetBase.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, width=None):
"""Updates the widget to show the next marker or the first marker when
finished.
"""
if progress.end_time:
return self.default
marker = self.markers[data['updates'] % len(self.markers)]
if self.marker_wrap:
marker = self.marker_wrap.format(marker)
if self.fill:
fill = self.fill(progress, data, width - progress.custom_len(marker))
else:
fill = ''
if isinstance(marker, int):
marker = bytes(marker)
fill = fill.encode()
else:
fill = type(marker)(fill)
return fill + marker
RotatingMarker = AnimatedMarker
class Counter(FormatWidgetMixin, WidgetBase):
"""Displays the current count."""
def __init__(self, format='%(value)d', **kwargs):
FormatWidgetMixin.__init__(self, format=format, **kwargs)
WidgetBase.__init__(self, format=format, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, format=None):
return FormatWidgetMixin.__call__(self, progress, data, format)
class ColoredMixin:
_fixed_colors: ClassVar[TFixedColors] = TFixedColors(fg_none=colors.yellow, bg_none=None)
_gradient_colors: ClassVar[TGradientColors] = TGradientColors(fg=colors.gradient, bg=None)
class Percentage(FormatWidgetMixin, ColoredMixin, WidgetBase):
"""Displays the current percentage as a number with a percent sign."""
def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs):
self.na = na
FormatWidgetMixin.__init__(self, format=format, **kwargs)
WidgetBase.__init__(self, format=format, **kwargs)
class SimpleProgress(FormatWidgetMixin, ColoredMixin, WidgetBase):
"""Returns progress as a count of the total (e.g.: "5 of 47")."""
max_width_cache: dict[types.Union[str, tuple[float, float | types.Type[base.UnknownLength]]], types.Optional[int]]
DEFAULT_FORMAT = '%(value_s)s of %(max_value_s)s'
def __init__(self, format=DEFAULT_FORMAT, **kwargs):
FormatWidgetMixin.__init__(self, format=format, **kwargs)
WidgetBase.__init__(self, format=format, **kwargs)
self.max_width_cache = dict(default=self.max_width or 0)
def __call__(self, progress: ProgressBarMixinBase, data: Data, format=None):
if data.get('max_value'):
data['max_value_s'] = data['max_value']
else:
data['max_value_s'] = 'N/A'
if data.get('value'):
data['value_s'] = data['value']
else:
data['value_s'] = 0
formatted = FormatWidgetMixin.__call__(self, progress, data, format=format)
key = (progress.min_value, progress.max_value)
max_width: types.Optional[int] = self.max_width_cache.get(key, self.max_width)
if not max_width:
temporary_data = data.copy()
for value in key:
if value is None:
continue
temporary_data['value'] = value
if (width := progress.custom_len(FormatWidgetMixin.__call__(self, progress, temporary_data, format=format))):
max_width = max(max_width or 0, width)
self.max_width_cache[key] = max_width
if max_width:
formatted = formatted.rjust(max_width)
return self._apply_colors(formatted, data)
class Bar(AutoWidthWidgetBase):
"""A progress bar which stretches to fill the line."""
fg: terminal.OptionalColor | None = colors.gradient
bg: terminal.OptionalColor | None = None
def __init__(self, marker='#', left='|', right='|', fill=' ', fill_left=True, marker_wrap=None, **kwargs):
"""Creates a customizable progress bar.
The callable takes the same parameters as the `__call__` method
marker - string or callable object to use as a marker
left - string or callable object to use as a left border
right - string or callable object to use as a right border
fill - character to use for the empty part of the progress bar
fill_left - whether to fill from the left or the right
"""
self.marker = create_marker(marker, marker_wrap)
self.left = string_or_lambda(left)
self.right = string_or_lambda(right)
self.fill = string_or_lambda(fill)
self.fill_left = fill_left
AutoWidthWidgetBase.__init__(self, **kwargs)
def _apply_colors(self, text: str, data: Data) -> str:
"""Apply colors to the text based on the progress percentage."""
percentage = data.get('percentage')
if percentage is None:
return text
return terminal.apply_colors(text, percentage / 100.0,
fg=self.fg, bg=self.bg,
**self._fixed_colors)
def __call__(self, progress: ProgressBarMixinBase, data: Data, width: int=0, color=True):
"""Updates the progress bar and its subcomponents."""
left = converters.to_unicode(self.left(progress, data, width))
right = converters.to_unicode(self.right(progress, data, width))
width -= progress.custom_len(left) + progress.custom_len(right)
marker = converters.to_unicode(self.marker(progress, data, width))
fill = converters.to_unicode(self.fill(progress, data, width))
width += len(marker) - progress.custom_len(marker)
if self.fill_left:
marker = marker.ljust(width, fill)
else:
marker = marker.rjust(width, fill)
if color:
marker = self._apply_colors(marker, data)
return left + marker + right
class ReverseBar(Bar):
"""A bar which has a marker that goes from right to left."""
def __init__(self, marker='#', left='|', right='|', fill=' ', fill_left=False, **kwargs):
"""Creates a customizable progress bar.
marker - string or updatable object to use as a marker
left - string or updatable object to use as a left border
right - string or updatable object to use as a right border
fill - character to use for the empty part of the progress bar
fill_left - whether to fill from the left or the right
"""
Bar.__init__(self, marker=marker, left=left, right=right, fill=fill, fill_left=fill_left, **kwargs)
class BouncingBar(Bar, TimeSensitiveWidgetBase):
"""A bar which has a marker which bounces from side to side."""
INTERVAL = datetime.timedelta(milliseconds=100)
def __call__(self, progress: ProgressBarMixinBase, data: Data, width: int=0, color=True):
"""Updates the progress bar and its subcomponents."""
left = converters.to_unicode(self.left(progress, data, width))
right = converters.to_unicode(self.right(progress, data, width))
width -= progress.custom_len(left) + progress.custom_len(right)
marker = converters.to_unicode(self.marker(progress, data, width))
fill = converters.to_unicode(self.fill(progress, data, width))
if width:
value = int(data['total_seconds_elapsed'] / self.INTERVAL.total_seconds())
a = value % width
b = width - a - 1
if value % (width * 2) >= width:
a, b = (b, a)
if self.fill_left:
marker = a * fill + marker + b * fill
else:
marker = b * fill + marker + a * fill
return left + marker + right
class FormatCustomText(FormatWidgetMixin, WidgetBase):
mapping: types.Dict[str, types.Any] = dict()
copy = False
def __init__(self, format: str, mapping: types.Optional[types.Dict[str, types.Any]]=None, **kwargs):
self.format = format
self.mapping = mapping or self.mapping
FormatWidgetMixin.__init__(self, format=format, **kwargs)
WidgetBase.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, format: types.Optional[str]=None):
return FormatWidgetMixin.__call__(self, progress, self.mapping, format or self.format)
class VariableMixin:
"""Mixin to display a custom user variable."""
def __init__(self, name, **kwargs):
if not isinstance(name, str):
raise TypeError('Variable(): argument must be a string')
if len(name.split()) > 1:
raise ValueError('Variable(): argument must be single word')
self.name = name
class MultiRangeBar(Bar, VariableMixin):
"""
A bar with multiple sub-ranges, each represented by a different symbol.
The various ranges are represented on a user-defined variable, formatted as
.. code-block:: python
[
['Symbol1', amount1],
['Symbol2', amount2],
...
]
"""
def __init__(self, name, markers, **kwargs):
VariableMixin.__init__(self, name)
Bar.__init__(self, **kwargs)
self.markers = [string_or_lambda(marker) for marker in markers]
def __call__(self, progress: ProgressBarMixinBase, data: Data, width: int=0, color=True):
"""Updates the progress bar and its subcomponents."""
left = converters.to_unicode(self.left(progress, data, width))
right = converters.to_unicode(self.right(progress, data, width))
width -= progress.custom_len(left) + progress.custom_len(right)
values = self.get_values(progress, data)
values_sum = sum(values)
if width and values_sum:
middle = ''
values_accumulated = 0
width_accumulated = 0
for marker, value in zip(self.markers, values):
marker = converters.to_unicode(marker(progress, data, width))
assert progress.custom_len(marker) == 1
values_accumulated += value
item_width = int(values_accumulated / values_sum * width)
item_width -= width_accumulated
width_accumulated += item_width
middle += item_width * marker
else:
fill = converters.to_unicode(self.fill(progress, data, width))
assert progress.custom_len(fill) == 1
middle = fill * width
return left + middle + right
class MultiProgressBar(MultiRangeBar):
def __init__(self, name, markers=' ▁▂▃▄▅▆▇█', **kwargs):
MultiRangeBar.__init__(self, name=name, markers=list(reversed(markers)), **kwargs)
class GranularMarkers:
smooth = ' ▏▎▍▌▋▊▉█'
bar = ' ▁▂▃▄▅▆▇█'
snake = ' ▖▌▛█'
fade_in = ' ░▒▓█'
dots = ' ⡀⡄⡆⡇⣇⣧⣷⣿'
growing_circles = ' .oO'
class GranularBar(AutoWidthWidgetBase):
"""A progressbar that can display progress at a sub-character granularity
by using multiple marker characters.
Examples of markers:
- Smooth: ` ▏▎▍▌▋▊▉█` (default)
- Bar: ` ▁▂▃▄▅▆▇█`
- Snake: ` ▖▌▛█`
- Fade in: ` ░▒▓█`
- Dots: ` ⡀⡄⡆⡇⣇⣧⣷⣿`
- Growing circles: ` .oO`
The markers can be accessed through GranularMarkers. GranularMarkers.dots
for example
"""
def __init__(self, markers=GranularMarkers.smooth, left='|', right='|', **kwargs):
"""Creates a customizable progress bar.
markers - string of characters to use as granular progress markers. The
first character should represent 0% and the last 100%.
Ex: ` .oO`.
left - string or callable object to use as a left border
right - string or callable object to use as a right border
"""
self.markers = markers
self.left = string_or_lambda(left)
self.right = string_or_lambda(right)
AutoWidthWidgetBase.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, width: int=0):
left = converters.to_unicode(self.left(progress, data, width))
right = converters.to_unicode(self.right(progress, data, width))
width -= progress.custom_len(left) + progress.custom_len(right)
max_value = progress.max_value
if max_value is not base.UnknownLength and max_value > 0:
percent = progress.value / max_value
else:
percent = 0
num_chars = percent * width
marker = self.markers[-1] * int(num_chars)
if (marker_idx := int(num_chars % 1 * (len(self.markers) - 1))):
marker += self.markers[marker_idx]
marker = converters.to_unicode(marker)
width += len(marker) - progress.custom_len(marker)
marker = marker.ljust(width, self.markers[0])
return left + marker + right
class FormatLabelBar(FormatLabel, Bar):
"""A bar which has a formatted label in the center."""
def __init__(self, format, **kwargs):
FormatLabel.__init__(self, format, **kwargs)
Bar.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, width: int=0, format: FormatString=None):
center = FormatLabel.__call__(self, progress, data, format=format)
bar = Bar.__call__(self, progress, data, width, color=False)
center_len = progress.custom_len(center)
center_left = int((width - center_len) / 2)
center_right = center_left + center_len
return self._apply_colors(bar[:center_left], data) + self._apply_colors(center, data) + self._apply_colors(bar[center_right:], data)
class PercentageLabelBar(Percentage, FormatLabelBar):
"""A bar which displays the current percentage in the center."""
def __init__(self, format='%(percentage)2d%%', na='N/A%%', **kwargs):
Percentage.__init__(self, format, na=na, **kwargs)
FormatLabelBar.__init__(self, format, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, width: int=0, format: FormatString=None):
return super().__call__(progress, data, width, format=format)
class Variable(FormatWidgetMixin, VariableMixin, WidgetBase):
"""Displays a custom variable."""
def __init__(self, name, format='{name}: {formatted_value}', width=6, precision=3, **kwargs):
"""Creates a Variable associated with the given name."""
self.format = format
self.width = width
self.precision = precision
VariableMixin.__init__(self, name=name)
WidgetBase.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, format: types.Optional[str]=None):
value = data['variables'][self.name]
context = data.copy()
context['value'] = value
context['name'] = self.name
context['width'] = self.width
context['precision'] = self.precision
try:
value = float(value)
fmt = '{value:{width}.{precision}}'
context['formatted_value'] = fmt.format(**context)
except (TypeError, ValueError):
if value:
context['formatted_value'] = '{value:{width}}'.format(**context)
else:
context['formatted_value'] = '-' * self.width
return self.format.format(**context)
class DynamicMessage(Variable):
"""Kept for backwards compatibility, please use `Variable` instead."""
class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase):
"""Widget which displays the current (date)time with seconds resolution."""
INTERVAL = datetime.timedelta(seconds=1)
def __init__(self, format='Current Time: %(current_time)s', microseconds=False, **kwargs):
self.microseconds = microseconds
FormatWidgetMixin.__init__(self, format=format, **kwargs)
TimeSensitiveWidgetBase.__init__(self, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, format: types.Optional[str]=None):
data['current_time'] = self.current_time()
data['current_datetime'] = self.current_datetime()
return FormatWidgetMixin.__call__(self, progress, data, format=format)
class JobStatusBar(Bar, VariableMixin):
"""
Widget which displays the job status as markers on the bar.
The status updates can be given either as a boolean or as a string. If it's
a string, it will be displayed as-is. If it's a boolean, it will be
displayed as a marker (default: '█' for success, 'X' for failure)
configurable through the `success_marker` and `failure_marker` parameters.
Args:
name: The name of the variable to use for the status updates.
left: The left border of the bar.
right: The right border of the bar.
fill: The fill character of the bar.
fill_left: Whether to fill the bar from the left or the right.
success_fg_color: The foreground color to use for successful jobs.
success_bg_color: The background color to use for successful jobs.
success_marker: The marker to use for successful jobs.
failure_fg_color: The foreground color to use for failed jobs.
failure_bg_color: The background color to use for failed jobs.
failure_marker: The marker to use for failed jobs.
"""
success_fg_color: terminal.Color | None = colors.green
success_bg_color: terminal.Color | None = None
success_marker: str = '█'
failure_fg_color: terminal.Color | None = colors.red
failure_bg_color: terminal.Color | None = None
failure_marker: str = 'X'
job_markers: list[str]
def __init__(self, name: str, left='|', right='|', fill=' ', fill_left=True, success_fg_color=colors.green, success_bg_color=None, success_marker='█', failure_fg_color=colors.red, failure_bg_color=None, failure_marker='X', **kwargs):
VariableMixin.__init__(self, name)
self.name = name
self.job_markers = []
self.left = string_or_lambda(left)
self.right = string_or_lambda(right)
self.fill = string_or_lambda(fill)
self.success_fg_color = success_fg_color
self.success_bg_color = success_bg_color
self.success_marker = success_marker
self.failure_fg_color = failure_fg_color
self.failure_bg_color = failure_bg_color
self.failure_marker = failure_marker
Bar.__init__(self, left=left, right=right, fill=fill, fill_left=fill_left, **kwargs)
def __call__(self, progress: ProgressBarMixinBase, data: Data, width: int=0, color=True):
left = converters.to_unicode(self.left(progress, data, width))
right = converters.to_unicode(self.right(progress, data, width))
width -= progress.custom_len(left) + progress.custom_len(right)
status: str | bool | None = data['variables'].get(self.name)
if width and status is not None:
if status is True:
marker = self.success_marker
fg_color = self.success_fg_color
bg_color = self.success_bg_color
elif status is False:
marker = self.failure_marker
fg_color = self.failure_fg_color
bg_color = self.failure_bg_color
else:
marker = status
fg_color = bg_color = None
marker = converters.to_unicode(marker)
if fg_color:
marker = fg_color.fg(marker)
if bg_color:
marker = bg_color.bg(marker)
self.job_markers.append(marker)
marker = ''.join(self.job_markers)
width -= progress.custom_len(marker)
fill = converters.to_unicode(self.fill(progress, data, width))
fill = self._apply_colors(fill * width, data)
if self.fill_left:
marker += fill
else:
marker = fill + marker
else:
marker = ''
return left + marker + right