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