Coverage for /Volumes/workspace/python-progressbar/.tox/py38/lib/python3.8/site-packages/progressbar/widgets.py : 99%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2from __future__ import absolute_import
3from __future__ import division
4from __future__ import print_function
5from __future__ import unicode_literals
6from __future__ import with_statement
8import abc
9import sys
10import pprint
11import datetime
12import functools
14from python_utils import converters
16import six
18from . import base
19from . import utils
21MAX_DATE = datetime.date.max
22MAX_TIME = datetime.time.max
23MAX_DATETIME = datetime.datetime.max
26def string_or_lambda(input_):
27 if isinstance(input_, six.string_types):
28 def render_input(progress, data, width):
29 return input_ % data
31 return render_input
32 else:
33 return input_
36def create_wrapper(wrapper):
37 '''Convert a wrapper tuple or format string to a format string
39 >>> create_wrapper('')
41 >>> print(create_wrapper('a{}b'))
42 a{}b
44 >>> print(create_wrapper(('a', 'b')))
45 a{}b
46 '''
47 if isinstance(wrapper, tuple) and len(wrapper) == 2:
48 a, b = wrapper
49 wrapper = (a or '') + '{}' + (b or '')
50 elif not wrapper:
51 return
53 if isinstance(wrapper, six.string_types):
54 assert '{}' in wrapper, 'Expected string with {} for formatting'
55 else:
56 raise RuntimeError('Pass either a begin/end string as a tuple or a'
57 ' template string with {}')
59 return wrapper
62def wrapper(function, wrapper):
63 '''Wrap the output of a function in a template string or a tuple with
64 begin/end strings
66 '''
67 wrapper = create_wrapper(wrapper)
68 if not wrapper:
69 return function
71 @functools.wraps(function)
72 def wrap(*args, **kwargs):
73 return wrapper.format(function(*args, **kwargs))
75 return wrap
78def create_marker(marker, wrap=None):
79 def _marker(progress, data, width):
80 if progress.max_value is not base.UnknownLength \
81 and progress.max_value > 0:
82 length = int(progress.value / progress.max_value * width)
83 return (marker * length)
84 else:
85 return marker
87 if isinstance(marker, six.string_types):
88 marker = converters.to_unicode(marker)
89 assert utils.len_color(marker) == 1, \
90 'Markers are required to be 1 char'
91 return wrapper(_marker, wrap)
92 else:
93 return wrapper(marker, wrap)
96class FormatWidgetMixin(object):
97 '''Mixin to format widgets using a formatstring
99 Variables available:
100 - max_value: The maximum value (can be None with iterators)
101 - value: The current value
102 - total_seconds_elapsed: The seconds since the bar started
103 - seconds_elapsed: The seconds since the bar started modulo 60
104 - minutes_elapsed: The minutes since the bar started modulo 60
105 - hours_elapsed: The hours since the bar started modulo 24
106 - days_elapsed: The hours since the bar started
107 - time_elapsed: Shortcut for HH:MM:SS time since the bar started including
108 days
109 - percentage: Percentage as a float
110 '''
111 required_values = []
113 def __init__(self, format, new_style=False, **kwargs):
114 self.new_style = new_style
115 self.format = format
117 def get_format(self, progress, data, format=None):
118 return format or self.format
120 def __call__(self, progress, data, format=None):
121 '''Formats the widget into a string'''
122 format = self.get_format(progress, data, format)
123 try:
124 if self.new_style:
125 return format.format(**data)
126 else:
127 return format % data
128 except (TypeError, KeyError):
129 print('Error while formatting %r' % format, file=sys.stderr)
130 pprint.pprint(data, stream=sys.stderr)
131 raise
134class WidthWidgetMixin(object):
135 '''Mixing to make sure widgets are only visible if the screen is within a
136 specified size range so the progressbar fits on both large and small
137 screens..
139 Variables available:
140 - min_width: Only display the widget if at least `min_width` is left
141 - max_width: Only display the widget if at most `max_width` is left
143 >>> class Progress(object):
144 ... term_width = 0
146 >>> WidthWidgetMixin(5, 10).check_size(Progress)
147 False
148 >>> Progress.term_width = 5
149 >>> WidthWidgetMixin(5, 10).check_size(Progress)
150 True
151 >>> Progress.term_width = 10
152 >>> WidthWidgetMixin(5, 10).check_size(Progress)
153 True
154 >>> Progress.term_width = 11
155 >>> WidthWidgetMixin(5, 10).check_size(Progress)
156 False
157 '''
159 def __init__(self, min_width=None, max_width=None, **kwargs):
160 self.min_width = min_width
161 self.max_width = max_width
163 def check_size(self, progress):
164 if self.min_width and self.min_width > progress.term_width:
165 return False
166 elif self.max_width and self.max_width < progress.term_width:
167 return False
168 else:
169 return True
172class WidgetBase(WidthWidgetMixin):
173 __metaclass__ = abc.ABCMeta
174 '''The base class for all widgets
176 The ProgressBar will call the widget's update value when the widget should
177 be updated. The widget's size may change between calls, but the widget may
178 display incorrectly if the size changes drastically and repeatedly.
180 The boolean INTERVAL informs the ProgressBar that it should be
181 updated more often because it is time sensitive.
183 The widgets are only visible if the screen is within a
184 specified size range so the progressbar fits on both large and small
185 screens.
187 WARNING: Widgets can be shared between multiple progressbars so any state
188 information specific to a progressbar should be stored within the
189 progressbar instead of the widget.
191 Variables available:
192 - min_width: Only display the widget if at least `min_width` is left
193 - max_width: Only display the widget if at most `max_width` is left
194 - weight: Widgets with a higher `weigth` will be calculated before widgets
195 with a lower one
196 - copy: Copy this widget when initializing the progress bar so the
197 progressbar can be reused. Some widgets such as the FormatCustomText
198 require the shared state so this needs to be optional
199 '''
200 copy = True
202 @abc.abstractmethod
203 def __call__(self, progress, data):
204 '''Updates the widget.
206 progress - a reference to the calling ProgressBar
207 '''
210class AutoWidthWidgetBase(WidgetBase):
211 '''The base class for all variable width widgets.
213 This widget is much like the \\hfill command in TeX, it will expand to
214 fill the line. You can use more than one in the same line, and they will
215 all have the same width, and together will fill the line.
216 '''
218 @abc.abstractmethod
219 def __call__(self, progress, data, width):
220 '''Updates the widget providing the total width the widget must fill.
222 progress - a reference to the calling ProgressBar
223 width - The total width the widget must fill
224 '''
227class TimeSensitiveWidgetBase(WidgetBase):
228 '''The base class for all time sensitive widgets.
230 Some widgets like timers would become out of date unless updated at least
231 every `INTERVAL`
232 '''
233 INTERVAL = datetime.timedelta(milliseconds=100)
236class FormatLabel(FormatWidgetMixin, WidgetBase):
237 '''Displays a formatted label
239 >>> label = FormatLabel('%(value)s', min_width=5, max_width=10)
240 >>> class Progress(object):
241 ... pass
242 >>> label = FormatLabel('{value} :: {value:^6}', new_style=True)
243 >>> str(label(Progress, dict(value='test')))
244 'test :: test '
246 '''
248 mapping = {
249 'finished': ('end_time', None),
250 'last_update': ('last_update_time', None),
251 'max': ('max_value', None),
252 'seconds': ('seconds_elapsed', None),
253 'start': ('start_time', None),
254 'elapsed': ('total_seconds_elapsed', utils.format_time),
255 'value': ('value', None),
256 }
258 def __init__(self, format, **kwargs):
259 FormatWidgetMixin.__init__(self, format=format, **kwargs)
260 WidgetBase.__init__(self, **kwargs)
262 def __call__(self, progress, data, **kwargs):
263 for name, (key, transform) in self.mapping.items():
264 try:
265 if transform is None:
266 data[name] = data[key]
267 else:
268 data[name] = transform(data[key])
269 except (KeyError, ValueError, IndexError): # pragma: no cover
270 pass
272 return FormatWidgetMixin.__call__(self, progress, data, **kwargs)
275class Timer(FormatLabel, TimeSensitiveWidgetBase):
276 '''WidgetBase which displays the elapsed seconds.'''
278 def __init__(self, format='Elapsed Time: %(elapsed)s', **kwargs):
279 FormatLabel.__init__(self, format=format, **kwargs)
280 TimeSensitiveWidgetBase.__init__(self, **kwargs)
282 # This is exposed as a static method for backwards compatibility
283 format_time = staticmethod(utils.format_time)
286class SamplesMixin(TimeSensitiveWidgetBase):
287 '''
288 Mixing for widgets that average multiple measurements
290 Note that samples can be either an integer or a timedelta to indicate a
291 certain amount of time
293 >>> class progress:
294 ... last_update_time = datetime.datetime.now()
295 ... value = 1
296 ... extra = dict()
298 >>> samples = SamplesMixin(samples=2)
299 >>> samples(progress, None, True)
300 (None, None)
301 >>> progress.last_update_time += datetime.timedelta(seconds=1)
302 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
303 True
305 >>> progress.last_update_time += datetime.timedelta(seconds=1)
306 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
307 True
309 >>> samples = SamplesMixin(samples=datetime.timedelta(seconds=1))
310 >>> _, value = samples(progress, None)
311 >>> value
312 [1, 1]
314 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
315 True
316 '''
318 def __init__(self, samples=datetime.timedelta(seconds=2), key_prefix=None,
319 **kwargs):
320 self.samples = samples
321 self.key_prefix = (self.__class__.__name__ or key_prefix) + '_'
322 TimeSensitiveWidgetBase.__init__(self, **kwargs)
324 def get_sample_times(self, progress, data):
325 return progress.extra.setdefault(self.key_prefix + 'sample_times', [])
327 def get_sample_values(self, progress, data):
328 return progress.extra.setdefault(self.key_prefix + 'sample_values', [])
330 def __call__(self, progress, data, delta=False):
331 sample_times = self.get_sample_times(progress, data)
332 sample_values = self.get_sample_values(progress, data)
334 if sample_times:
335 sample_time = sample_times[-1]
336 else:
337 sample_time = datetime.datetime.min
339 if progress.last_update_time - sample_time > self.INTERVAL:
340 # Add a sample but limit the size to `num_samples`
341 sample_times.append(progress.last_update_time)
342 sample_values.append(progress.value)
344 if isinstance(self.samples, datetime.timedelta):
345 minimum_time = progress.last_update_time - self.samples
346 minimum_value = sample_values[-1]
347 while (sample_times[2:] and
348 minimum_time > sample_times[1] and
349 minimum_value > sample_values[1]):
350 sample_times.pop(0)
351 sample_values.pop(0)
352 else:
353 if len(sample_times) > self.samples:
354 sample_times.pop(0)
355 sample_values.pop(0)
357 if delta:
358 delta_time = sample_times[-1] - sample_times[0]
359 delta_value = sample_values[-1] - sample_values[0]
360 if delta_time:
361 return delta_time, delta_value
362 else:
363 return None, None
364 else:
365 return sample_times, sample_values
368class ETA(Timer):
369 '''WidgetBase which attempts to estimate the time of arrival.'''
371 def __init__(
372 self,
373 format_not_started='ETA: --:--:--',
374 format_finished='Time: %(elapsed)8s',
375 format='ETA: %(eta)8s',
376 format_zero='ETA: 00:00:00',
377 format_NA='ETA: N/A',
378 **kwargs):
380 Timer.__init__(self, **kwargs)
381 self.format_not_started = format_not_started
382 self.format_finished = format_finished
383 self.format = format
384 self.format_zero = format_zero
385 self.format_NA = format_NA
387 def _calculate_eta(self, progress, data, value, elapsed):
388 '''Updates the widget to show the ETA or total time when finished.'''
389 if elapsed:
390 # The max() prevents zero division errors
391 per_item = elapsed.total_seconds() / max(value, 1e-6)
392 remaining = progress.max_value - data['value']
393 eta_seconds = remaining * per_item
394 else:
395 eta_seconds = 0
397 return eta_seconds
399 def __call__(self, progress, data, value=None, elapsed=None):
400 '''Updates the widget to show the ETA or total time when finished.'''
401 if value is None:
402 value = data['value']
404 if elapsed is None:
405 elapsed = data['time_elapsed']
407 ETA_NA = False
408 try:
409 data['eta_seconds'] = self._calculate_eta(
410 progress, data, value=value, elapsed=elapsed)
411 except TypeError:
412 data['eta_seconds'] = None
413 ETA_NA = True
415 data['eta'] = None
416 if data['eta_seconds']:
417 try:
418 data['eta'] = utils.format_time(data['eta_seconds'])
419 except (ValueError, OverflowError): # pragma: no cover
420 pass
422 if data['value'] == progress.min_value:
423 format = self.format_not_started
424 elif progress.end_time:
425 format = self.format_finished
426 elif data['eta']:
427 format = self.format
428 elif ETA_NA:
429 format = self.format_NA
430 else:
431 format = self.format_zero
433 return Timer.__call__(self, progress, data, format=format)
436class AbsoluteETA(ETA):
437 '''Widget which attempts to estimate the absolute time of arrival.'''
439 def _calculate_eta(self, progress, data, value, elapsed):
440 eta_seconds = ETA._calculate_eta(self, progress, data, value, elapsed)
441 now = datetime.datetime.now()
442 try:
443 return now + datetime.timedelta(seconds=eta_seconds)
444 except OverflowError: # pragma: no cover
445 return datetime.datetime.max
447 def __init__(
448 self,
449 format_not_started='Estimated finish time: ----/--/-- --:--:--',
450 format_finished='Finished at: %(elapsed)s',
451 format='Estimated finish time: %(eta)s',
452 **kwargs):
453 ETA.__init__(self, format_not_started=format_not_started,
454 format_finished=format_finished, format=format, **kwargs)
457class AdaptiveETA(ETA, SamplesMixin):
458 '''WidgetBase which attempts to estimate the time of arrival.
460 Uses a sampled average of the speed based on the 10 last updates.
461 Very convenient for resuming the progress halfway.
462 '''
464 def __init__(self, **kwargs):
465 ETA.__init__(self, **kwargs)
466 SamplesMixin.__init__(self, **kwargs)
468 def __call__(self, progress, data):
469 elapsed, value = SamplesMixin.__call__(self, progress, data,
470 delta=True)
471 if not elapsed:
472 value = None
473 elapsed = 0
475 return ETA.__call__(self, progress, data, value=value, elapsed=elapsed)
478class DataSize(FormatWidgetMixin, WidgetBase):
479 '''
480 Widget for showing an amount of data transferred/processed.
482 Automatically formats the value (assumed to be a count of bytes) with an
483 appropriate sized unit, based on the IEC binary prefixes (powers of 1024).
484 '''
486 def __init__(
487 self, variable='value',
488 format='%(scaled)5.1f %(prefix)s%(unit)s', unit='B',
489 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'),
490 **kwargs):
491 self.variable = variable
492 self.unit = unit
493 self.prefixes = prefixes
494 FormatWidgetMixin.__init__(self, format=format, **kwargs)
495 WidgetBase.__init__(self, **kwargs)
497 def __call__(self, progress, data):
498 value = data[self.variable]
499 if value is not None:
500 scaled, power = utils.scale_1024(value, len(self.prefixes))
501 else:
502 scaled = power = 0
504 data['scaled'] = scaled
505 data['prefix'] = self.prefixes[power]
506 data['unit'] = self.unit
508 return FormatWidgetMixin.__call__(self, progress, data)
511class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase):
512 '''
513 WidgetBase for showing the transfer speed (useful for file transfers).
514 '''
516 def __init__(
517 self, format='%(scaled)5.1f %(prefix)s%(unit)-s/s',
518 inverse_format='%(scaled)5.1f s/%(prefix)s%(unit)-s', unit='B',
519 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'),
520 **kwargs):
521 self.unit = unit
522 self.prefixes = prefixes
523 self.inverse_format = inverse_format
524 FormatWidgetMixin.__init__(self, format=format, **kwargs)
525 TimeSensitiveWidgetBase.__init__(self, **kwargs)
527 def _speed(self, value, elapsed):
528 speed = float(value) / elapsed
529 return utils.scale_1024(speed, len(self.prefixes))
531 def __call__(self, progress, data, value=None, total_seconds_elapsed=None):
532 '''Updates the widget with the current SI prefixed speed.'''
533 if value is None:
534 value = data['value']
536 elapsed = utils.deltas_to_seconds(
537 total_seconds_elapsed,
538 data['total_seconds_elapsed'])
540 if value is not None and elapsed is not None \
541 and elapsed > 2e-6 and value > 2e-6: # =~ 0
542 scaled, power = self._speed(value, elapsed)
543 else:
544 scaled = power = 0
546 data['unit'] = self.unit
547 if power == 0 and scaled < 0.1:
548 if scaled > 0:
549 scaled = 1 / scaled
550 data['scaled'] = scaled
551 data['prefix'] = self.prefixes[0]
552 return FormatWidgetMixin.__call__(self, progress, data,
553 self.inverse_format)
554 else:
555 data['scaled'] = scaled
556 data['prefix'] = self.prefixes[power]
557 return FormatWidgetMixin.__call__(self, progress, data)
560class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin):
561 '''WidgetBase for showing the transfer speed, based on the last X samples
562 '''
564 def __init__(self, **kwargs):
565 FileTransferSpeed.__init__(self, **kwargs)
566 SamplesMixin.__init__(self, **kwargs)
568 def __call__(self, progress, data):
569 elapsed, value = SamplesMixin.__call__(self, progress, data,
570 delta=True)
571 return FileTransferSpeed.__call__(self, progress, data, value, elapsed)
574class AnimatedMarker(TimeSensitiveWidgetBase):
575 '''An animated marker for the progress bar which defaults to appear as if
576 it were rotating.
577 '''
579 def __init__(self, markers='|/-\\', default=None, fill='',
580 marker_wrap=None, fill_wrap=None, **kwargs):
581 self.markers = markers
582 self.marker_wrap = create_wrapper(marker_wrap)
583 self.default = default or markers[0]
584 self.fill_wrap = create_wrapper(fill_wrap)
585 self.fill = create_marker(fill, self.fill_wrap) if fill else None
586 WidgetBase.__init__(self, **kwargs)
588 def __call__(self, progress, data, width=None):
589 '''Updates the widget to show the next marker or the first marker when
590 finished'''
592 if progress.end_time:
593 return self.default
595 marker = self.markers[data['updates'] % len(self.markers)]
596 if self.marker_wrap:
597 marker = self.marker_wrap.format(marker)
599 if self.fill:
600 # Cut the last character so we can replace it with our marker
601 fill = self.fill(progress, data, width - progress.custom_len(
602 marker))
603 else:
604 fill = ''
606 # Python 3 returns an int when indexing bytes
607 if isinstance(marker, int): # pragma: no cover
608 marker = bytes(marker)
609 fill = fill.encode()
610 else:
611 # cast fill to the same type as marker
612 fill = type(marker)(fill)
614 return fill + marker
617# Alias for backwards compatibility
618RotatingMarker = AnimatedMarker
621class Counter(FormatWidgetMixin, WidgetBase):
622 '''Displays the current count'''
624 def __init__(self, format='%(value)d', **kwargs):
625 FormatWidgetMixin.__init__(self, format=format, **kwargs)
626 WidgetBase.__init__(self, format=format, **kwargs)
628 def __call__(self, progress, data, format=None):
629 return FormatWidgetMixin.__call__(self, progress, data, format)
632class Percentage(FormatWidgetMixin, WidgetBase):
633 '''Displays the current percentage as a number with a percent sign.'''
635 def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs):
636 self.na = na
637 FormatWidgetMixin.__init__(self, format=format, **kwargs)
638 WidgetBase.__init__(self, format=format, **kwargs)
640 def get_format(self, progress, data, format=None):
641 # If percentage is not available, display N/A%
642 percentage = data.get('percentage', base.Undefined)
643 if not percentage and percentage != 0:
644 return self.na
646 return FormatWidgetMixin.get_format(self, progress, data, format)
649class SimpleProgress(FormatWidgetMixin, WidgetBase):
650 '''Returns progress as a count of the total (e.g.: "5 of 47")'''
652 DEFAULT_FORMAT = '%(value_s)s of %(max_value_s)s'
654 def __init__(self, format=DEFAULT_FORMAT, **kwargs):
655 FormatWidgetMixin.__init__(self, format=format, **kwargs)
656 WidgetBase.__init__(self, format=format, **kwargs)
657 self.max_width_cache = dict(default=self.max_width)
659 def __call__(self, progress, data, format=None):
660 # If max_value is not available, display N/A
661 if data.get('max_value'):
662 data['max_value_s'] = data.get('max_value')
663 else:
664 data['max_value_s'] = 'N/A'
666 # if value is not available it's the zeroth iteration
667 if data.get('value'):
668 data['value_s'] = data['value']
669 else:
670 data['value_s'] = 0
672 formatted = FormatWidgetMixin.__call__(self, progress, data,
673 format=format)
675 # Guess the maximum width from the min and max value
676 key = progress.min_value, progress.max_value
677 max_width = self.max_width_cache.get(key, self.max_width)
678 if not max_width:
679 temporary_data = data.copy()
680 for value in key:
681 if value is None: # pragma: no cover
682 continue
684 temporary_data['value'] = value
685 width = progress.custom_len(FormatWidgetMixin.__call__(
686 self, progress, temporary_data, format=format))
687 if width: # pragma: no branch
688 max_width = max(max_width or 0, width)
690 self.max_width_cache[key] = max_width
692 # Adjust the output to have a consistent size in all cases
693 if max_width: # pragma: no branch
694 formatted = formatted.rjust(max_width)
696 return formatted
699class Bar(AutoWidthWidgetBase):
700 '''A progress bar which stretches to fill the line.'''
702 def __init__(self, marker='#', left='|', right='|', fill=' ',
703 fill_left=True, marker_wrap=None, **kwargs):
704 '''Creates a customizable progress bar.
706 The callable takes the same parameters as the `__call__` method
708 marker - string or callable object to use as a marker
709 left - string or callable object to use as a left border
710 right - string or callable object to use as a right border
711 fill - character to use for the empty part of the progress bar
712 fill_left - whether to fill from the left or the right
713 '''
715 self.marker = create_marker(marker, marker_wrap)
716 self.left = string_or_lambda(left)
717 self.right = string_or_lambda(right)
718 self.fill = string_or_lambda(fill)
719 self.fill_left = fill_left
721 AutoWidthWidgetBase.__init__(self, **kwargs)
723 def __call__(self, progress, data, width):
724 '''Updates the progress bar and its subcomponents'''
726 left = converters.to_unicode(self.left(progress, data, width))
727 right = converters.to_unicode(self.right(progress, data, width))
728 width -= progress.custom_len(left) + progress.custom_len(right)
729 marker = converters.to_unicode(self.marker(progress, data, width))
730 fill = converters.to_unicode(self.fill(progress, data, width))
732 # Make sure we ignore invisible characters when filling
733 width += len(marker) - progress.custom_len(marker)
735 if self.fill_left:
736 marker = marker.ljust(width, fill)
737 else:
738 marker = marker.rjust(width, fill)
740 return left + marker + right
743class ReverseBar(Bar):
744 '''A bar which has a marker that goes from right to left'''
746 def __init__(self, marker='#', left='|', right='|', fill=' ',
747 fill_left=False, **kwargs):
748 '''Creates a customizable progress bar.
750 marker - string or updatable object to use as a marker
751 left - string or updatable object to use as a left border
752 right - string or updatable object to use as a right border
753 fill - character to use for the empty part of the progress bar
754 fill_left - whether to fill from the left or the right
755 '''
756 Bar.__init__(self, marker=marker, left=left, right=right, fill=fill,
757 fill_left=fill_left, **kwargs)
760class BouncingBar(Bar, TimeSensitiveWidgetBase):
761 '''A bar which has a marker which bounces from side to side.'''
763 INTERVAL = datetime.timedelta(milliseconds=100)
765 def __call__(self, progress, data, width):
766 '''Updates the progress bar and its subcomponents'''
768 left = converters.to_unicode(self.left(progress, data, width))
769 right = converters.to_unicode(self.right(progress, data, width))
770 width -= progress.custom_len(left) + progress.custom_len(right)
771 marker = converters.to_unicode(self.marker(progress, data, width))
773 fill = converters.to_unicode(self.fill(progress, data, width))
775 if width: # pragma: no branch
776 value = int(
777 data['total_seconds_elapsed'] / self.INTERVAL.total_seconds())
779 a = value % width
780 b = width - a - 1
781 if value % (width * 2) >= width:
782 a, b = b, a
784 if self.fill_left:
785 marker = a * fill + marker + b * fill
786 else:
787 marker = b * fill + marker + a * fill
789 return left + marker + right
792class FormatCustomText(FormatWidgetMixin, WidgetBase):
793 mapping = {}
794 copy = False
796 def __init__(self, format, mapping=mapping, **kwargs):
797 self.format = format
798 self.mapping = mapping
799 FormatWidgetMixin.__init__(self, format=format, **kwargs)
800 WidgetBase.__init__(self, **kwargs)
802 def update_mapping(self, **mapping):
803 self.mapping.update(mapping)
805 def __call__(self, progress, data):
806 return FormatWidgetMixin.__call__(
807 self, progress, self.mapping, self.format)
810class VariableMixin(object):
811 '''Mixin to display a custom user variable '''
813 def __init__(self, name, **kwargs):
814 if not isinstance(name, six.string_types):
815 raise TypeError('Variable(): argument must be a string')
816 if len(name.split()) > 1:
817 raise ValueError('Variable(): argument must be single word')
818 self.name = name
821class MultiRangeBar(Bar, VariableMixin):
822 '''
823 A bar with multiple sub-ranges, each represented by a different symbol
825 The various ranges are represented on a user-defined variable, formatted as
827 .. code-block:: python
829 [
830 ['Symbol1', amount1],
831 ['Symbol2', amount2],
832 ...
833 ]
834 '''
836 def __init__(self, name, markers, **kwargs):
837 VariableMixin.__init__(self, name)
838 Bar.__init__(self, **kwargs)
839 self.markers = [
840 string_or_lambda(marker)
841 for marker in markers
842 ]
844 def get_values(self, progress, data):
845 return data['variables'][self.name] or []
847 def __call__(self, progress, data, width):
848 '''Updates the progress bar and its subcomponents'''
850 left = converters.to_unicode(self.left(progress, data, width))
851 right = converters.to_unicode(self.right(progress, data, width))
852 width -= progress.custom_len(left) + progress.custom_len(right)
853 values = self.get_values(progress, data)
855 values_sum = sum(values)
856 if width and values_sum:
857 middle = ''
858 values_accumulated = 0
859 width_accumulated = 0
860 for marker, value in zip(self.markers, values):
861 marker = converters.to_unicode(marker(progress, data, width))
862 assert progress.custom_len(marker) == 1
864 values_accumulated += value
865 item_width = int(values_accumulated / values_sum * width)
866 item_width -= width_accumulated
867 width_accumulated += item_width
868 middle += item_width * marker
869 else:
870 fill = converters.to_unicode(self.fill(progress, data, width))
871 assert progress.custom_len(fill) == 1
872 middle = fill * width
874 return left + middle + right
877class MultiProgressBar(MultiRangeBar):
878 def __init__(self,
879 name,
880 # NOTE: the markers are not whitespace even though some
881 # terminals don't show the characters correctly!
882 markers=' ▁▂▃▄▅▆▇█',
883 **kwargs):
884 MultiRangeBar.__init__(self, name=name,
885 markers=list(reversed(markers)), **kwargs)
887 def get_values(self, progress, data):
888 ranges = [0] * len(self.markers)
889 for progress in data['variables'][self.name] or []:
890 if not isinstance(progress, (int, float)):
891 # Progress is (value, max)
892 progress_value, progress_max = progress
893 progress = float(progress_value) / float(progress_max)
895 if progress < 0 or progress > 1:
896 raise ValueError(
897 'Range value needs to be in the range [0..1], got %s' %
898 progress)
900 range_ = progress * (len(ranges) - 1)
901 pos = int(range_)
902 frac = range_ % 1
903 ranges[pos] += (1 - frac)
904 if (frac):
905 ranges[pos + 1] += (frac)
907 if self.fill_left:
908 ranges = list(reversed(ranges))
909 return ranges
912class FormatLabelBar(FormatLabel, Bar):
913 '''A bar which has a formatted label in the center.'''
914 def __init__(self, format, **kwargs):
915 FormatLabel.__init__(self, format, **kwargs)
916 Bar.__init__(self, **kwargs)
918 def __call__(self, progress, data, width, format=None):
919 center = FormatLabel.__call__(self, progress, data, format=format)
920 bar = Bar.__call__(self, progress, data, width)
922 # Aligns the center of the label to the center of the bar
923 center_len = progress.custom_len(center)
924 center_left = int((width - center_len) / 2)
925 center_right = center_left + center_len
926 return bar[:center_left] + center + bar[center_right:]
929class PercentageLabelBar(Percentage, FormatLabelBar):
930 '''A bar which displays the current percentage in the center.'''
931 # %3d adds an extra space that makes it look off-center
932 # %2d keeps the label somewhat consistently in-place
933 def __init__(self, format='%(percentage)2d%%', na='N/A%%', **kwargs):
934 Percentage.__init__(self, format, na=na, **kwargs)
935 FormatLabelBar.__init__(self, format, **kwargs)
938class Variable(FormatWidgetMixin, VariableMixin, WidgetBase):
939 '''Displays a custom variable.'''
941 def __init__(self, name, format='{name}: {formatted_value}',
942 width=6, precision=3, **kwargs):
943 '''Creates a Variable associated with the given name.'''
944 self.format = format
945 self.width = width
946 self.precision = precision
947 VariableMixin.__init__(self, name=name)
948 WidgetBase.__init__(self, **kwargs)
950 def __call__(self, progress, data):
951 value = data['variables'][self.name]
952 context = data.copy()
953 context['value'] = value
954 context['name'] = self.name
955 context['width'] = self.width
956 context['precision'] = self.precision
958 try:
959 # Make sure to try and cast the value first, otherwise the
960 # formatting will generate warnings/errors on newer Python releases
961 value = float(value)
962 fmt = '{value:{width}.{precision}}'
963 context['formatted_value'] = fmt.format(**context)
964 except (TypeError, ValueError):
965 if value:
966 context['formatted_value'] = '{value:{width}}'.format(
967 **context)
968 else:
969 context['formatted_value'] = '-' * self.width
971 return self.format.format(**context)
974class DynamicMessage(Variable):
975 '''Kept for backwards compatibility, please use `Variable` instead.'''
976 pass
979class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase):
980 '''Widget which displays the current (date)time with seconds resolution.'''
981 INTERVAL = datetime.timedelta(seconds=1)
983 def __init__(self, format='Current Time: %(current_time)s',
984 microseconds=False, **kwargs):
985 self.microseconds = microseconds
986 FormatWidgetMixin.__init__(self, format=format, **kwargs)
987 TimeSensitiveWidgetBase.__init__(self, **kwargs)
989 def __call__(self, progress, data):
990 data['current_time'] = self.current_time()
991 data['current_datetime'] = self.current_datetime()
993 return FormatWidgetMixin.__call__(self, progress, data)
995 def current_datetime(self):
996 now = datetime.datetime.now()
997 if not self.microseconds:
998 now = now.replace(microsecond=0)
1000 return now
1002 def current_time(self):
1003 return self.current_datetime().time()