Coverage for /Volumes/workspace/python-progressbar/.tox/py27/lib/python2.7/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 >>> create_wrapper('a{}b')
42 u'a{}b'
44 >>> create_wrapper(('a', 'b'))
45 u'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 __call__(self, progress, data, format=None):
118 '''Formats the widget into a string'''
119 try:
120 if self.new_style:
121 return (format or self.format).format(**data)
122 else:
123 return (format or self.format) % data
124 except (TypeError, KeyError):
125 print('Error while formatting %r' % self.format, file=sys.stderr)
126 pprint.pprint(data, stream=sys.stderr)
127 raise
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..
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
139 >>> class Progress(object):
140 ... term_width = 0
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 '''
155 def __init__(self, min_width=None, max_width=None, **kwargs):
156 self.min_width = min_width
157 self.max_width = max_width
159 def check_size(self, progress):
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
168class WidgetBase(WidthWidgetMixin):
169 __metaclass__ = abc.ABCMeta
170 '''The base class for all widgets
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.
176 The boolean INTERVAL informs the ProgressBar that it should be
177 updated more often because it is time sensitive.
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.
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.
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 '''
194 @abc.abstractmethod
195 def __call__(self, progress, data):
196 '''Updates the widget.
198 progress - a reference to the calling ProgressBar
199 '''
202class AutoWidthWidgetBase(WidgetBase):
203 '''The base class for all variable width widgets.
205 This widget is much like the \\hfill command in TeX, it will expand to
206 fill the line. You can use more than one in the same line, and they will
207 all have the same width, and together will fill the line.
208 '''
210 @abc.abstractmethod
211 def __call__(self, progress, data, width):
212 '''Updates the widget providing the total width the widget must fill.
214 progress - a reference to the calling ProgressBar
215 width - The total width the widget must fill
216 '''
219class TimeSensitiveWidgetBase(WidgetBase):
220 '''The base class for all time sensitive widgets.
222 Some widgets like timers would become out of date unless updated at least
223 every `INTERVAL`
224 '''
225 INTERVAL = datetime.timedelta(milliseconds=100)
228class FormatLabel(FormatWidgetMixin, WidgetBase):
229 '''Displays a formatted label
231 >>> label = FormatLabel('%(value)s', min_width=5, max_width=10)
232 >>> class Progress(object):
233 ... pass
234 >>> label = FormatLabel('{value} :: {value:^6}', new_style=True)
235 >>> str(label(Progress, dict(value='test')))
236 'test :: test '
238 '''
240 mapping = {
241 'finished': ('end_time', None),
242 'last_update': ('last_update_time', None),
243 'max': ('max_value', None),
244 'seconds': ('seconds_elapsed', None),
245 'start': ('start_time', None),
246 'elapsed': ('total_seconds_elapsed', utils.format_time),
247 'value': ('value', None),
248 }
250 def __init__(self, format, **kwargs):
251 FormatWidgetMixin.__init__(self, format=format, **kwargs)
252 WidgetBase.__init__(self, **kwargs)
254 def __call__(self, progress, data, **kwargs):
255 for name, (key, transform) in self.mapping.items():
256 try:
257 if transform is None:
258 data[name] = data[key]
259 else:
260 data[name] = transform(data[key])
261 except (KeyError, ValueError, IndexError): # pragma: no cover
262 pass
264 return FormatWidgetMixin.__call__(self, progress, data, **kwargs)
267class Timer(FormatLabel, TimeSensitiveWidgetBase):
268 '''WidgetBase which displays the elapsed seconds.'''
270 def __init__(self, format='Elapsed Time: %(elapsed)s', **kwargs):
271 FormatLabel.__init__(self, format=format, **kwargs)
272 TimeSensitiveWidgetBase.__init__(self, **kwargs)
274 # This is exposed as a static method for backwards compatibility
275 format_time = staticmethod(utils.format_time)
278class SamplesMixin(TimeSensitiveWidgetBase):
279 '''
280 Mixing for widgets that average multiple measurements
282 Note that samples can be either an integer or a timedelta to indicate a
283 certain amount of time
285 >>> class progress:
286 ... last_update_time = datetime.datetime.now()
287 ... value = 1
288 ... extra = dict()
290 >>> samples = SamplesMixin(samples=2)
291 >>> samples(progress, None, True)
292 (None, None)
293 >>> progress.last_update_time += datetime.timedelta(seconds=1)
294 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
295 True
297 >>> progress.last_update_time += datetime.timedelta(seconds=1)
298 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
299 True
301 >>> samples = SamplesMixin(samples=datetime.timedelta(seconds=1))
302 >>> _, value = samples(progress, None)
303 >>> value
304 [1, 1]
306 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
307 True
308 '''
310 def __init__(self, samples=datetime.timedelta(seconds=2), key_prefix=None,
311 **kwargs):
312 self.samples = samples
313 self.key_prefix = (self.__class__.__name__ or key_prefix) + '_'
314 TimeSensitiveWidgetBase.__init__(self, **kwargs)
316 def get_sample_times(self, progress, data):
317 return progress.extra.setdefault(self.key_prefix + 'sample_times', [])
319 def get_sample_values(self, progress, data):
320 return progress.extra.setdefault(self.key_prefix + 'sample_values', [])
322 def __call__(self, progress, data, delta=False):
323 sample_times = self.get_sample_times(progress, data)
324 sample_values = self.get_sample_values(progress, data)
326 if sample_times:
327 sample_time = sample_times[-1]
328 else:
329 sample_time = datetime.datetime.min
331 if progress.last_update_time - sample_time > self.INTERVAL:
332 # Add a sample but limit the size to `num_samples`
333 sample_times.append(progress.last_update_time)
334 sample_values.append(progress.value)
336 if isinstance(self.samples, datetime.timedelta):
337 minimum_time = progress.last_update_time - self.samples
338 minimum_value = sample_values[-1]
339 while (sample_times[2:] and
340 minimum_time > sample_times[1] and
341 minimum_value > sample_values[1]):
342 sample_times.pop(0)
343 sample_values.pop(0)
344 else:
345 if len(sample_times) > self.samples:
346 sample_times.pop(0)
347 sample_values.pop(0)
349 if delta:
350 delta_time = sample_times[-1] - sample_times[0]
351 delta_value = sample_values[-1] - sample_values[0]
352 if delta_time:
353 return delta_time, delta_value
354 else:
355 return None, None
356 else:
357 return sample_times, sample_values
360class ETA(Timer):
361 '''WidgetBase which attempts to estimate the time of arrival.'''
363 def __init__(
364 self,
365 format_not_started='ETA: --:--:--',
366 format_finished='Time: %(elapsed)8s',
367 format='ETA: %(eta)8s',
368 format_zero='ETA: 00:00:00',
369 format_NA='ETA: N/A',
370 **kwargs):
372 Timer.__init__(self, **kwargs)
373 self.format_not_started = format_not_started
374 self.format_finished = format_finished
375 self.format = format
376 self.format_zero = format_zero
377 self.format_NA = format_NA
379 def _calculate_eta(self, progress, data, value, elapsed):
380 '''Updates the widget to show the ETA or total time when finished.'''
381 if elapsed:
382 # The max() prevents zero division errors
383 per_item = elapsed.total_seconds() / max(value, 1e-6)
384 remaining = progress.max_value - data['value']
385 eta_seconds = remaining * per_item
386 else:
387 eta_seconds = 0
389 return eta_seconds
391 def __call__(self, progress, data, value=None, elapsed=None):
392 '''Updates the widget to show the ETA or total time when finished.'''
393 if value is None:
394 value = data['value']
396 if elapsed is None:
397 elapsed = data['time_elapsed']
399 ETA_NA = False
400 try:
401 data['eta_seconds'] = self._calculate_eta(
402 progress, data, value=value, elapsed=elapsed)
403 except TypeError:
404 data['eta_seconds'] = None
405 ETA_NA = True
407 data['eta'] = None
408 if data['eta_seconds']:
409 try:
410 data['eta'] = utils.format_time(data['eta_seconds'])
411 except (ValueError, OverflowError): # pragma: no cover
412 pass
414 if data['value'] == progress.min_value:
415 format = self.format_not_started
416 elif progress.end_time:
417 format = self.format_finished
418 elif data['eta']:
419 format = self.format
420 elif ETA_NA:
421 format = self.format_NA
422 else:
423 format = self.format_zero
425 return Timer.__call__(self, progress, data, format=format)
428class AbsoluteETA(ETA):
429 '''Widget which attempts to estimate the absolute time of arrival.'''
431 def _calculate_eta(self, progress, data, value, elapsed):
432 eta_seconds = ETA._calculate_eta(self, progress, data, value, elapsed)
433 now = datetime.datetime.now()
434 try:
435 return now + datetime.timedelta(seconds=eta_seconds)
436 except OverflowError: # pragma: no cover
437 return datetime.datetime.max
439 def __init__(
440 self,
441 format_not_started='Estimated finish time: ----/--/-- --:--:--',
442 format_finished='Finished at: %(elapsed)s',
443 format='Estimated finish time: %(eta)s',
444 **kwargs):
445 ETA.__init__(self, format_not_started=format_not_started,
446 format_finished=format_finished, format=format, **kwargs)
449class AdaptiveETA(ETA, SamplesMixin):
450 '''WidgetBase which attempts to estimate the time of arrival.
452 Uses a sampled average of the speed based on the 10 last updates.
453 Very convenient for resuming the progress halfway.
454 '''
456 def __init__(self, **kwargs):
457 ETA.__init__(self, **kwargs)
458 SamplesMixin.__init__(self, **kwargs)
460 def __call__(self, progress, data):
461 elapsed, value = SamplesMixin.__call__(self, progress, data,
462 delta=True)
463 if not elapsed:
464 value = None
465 elapsed = 0
467 return ETA.__call__(self, progress, data, value=value, elapsed=elapsed)
470class DataSize(FormatWidgetMixin, WidgetBase):
471 '''
472 Widget for showing an amount of data transferred/processed.
474 Automatically formats the value (assumed to be a count of bytes) with an
475 appropriate sized unit, based on the IEC binary prefixes (powers of 1024).
476 '''
478 def __init__(
479 self, variable='value',
480 format='%(scaled)5.1f %(prefix)s%(unit)s', unit='B',
481 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'),
482 **kwargs):
483 self.variable = variable
484 self.unit = unit
485 self.prefixes = prefixes
486 FormatWidgetMixin.__init__(self, format=format, **kwargs)
487 WidgetBase.__init__(self, **kwargs)
489 def __call__(self, progress, data):
490 value = data[self.variable]
491 if value is not None:
492 scaled, power = utils.scale_1024(value, len(self.prefixes))
493 else:
494 scaled = power = 0
496 data['scaled'] = scaled
497 data['prefix'] = self.prefixes[power]
498 data['unit'] = self.unit
500 return FormatWidgetMixin.__call__(self, progress, data)
503class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase):
504 '''
505 WidgetBase for showing the transfer speed (useful for file transfers).
506 '''
508 def __init__(
509 self, format='%(scaled)5.1f %(prefix)s%(unit)-s/s',
510 inverse_format='%(scaled)5.1f s/%(prefix)s%(unit)-s', unit='B',
511 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'),
512 **kwargs):
513 self.unit = unit
514 self.prefixes = prefixes
515 self.inverse_format = inverse_format
516 FormatWidgetMixin.__init__(self, format=format, **kwargs)
517 TimeSensitiveWidgetBase.__init__(self, **kwargs)
519 def _speed(self, value, elapsed):
520 speed = float(value) / elapsed
521 return utils.scale_1024(speed, len(self.prefixes))
523 def __call__(self, progress, data, value=None, total_seconds_elapsed=None):
524 '''Updates the widget with the current SI prefixed speed.'''
525 if value is None:
526 value = data['value']
528 elapsed = utils.deltas_to_seconds(
529 total_seconds_elapsed,
530 data['total_seconds_elapsed'])
532 if value is not None and elapsed is not None \
533 and elapsed > 2e-6 and value > 2e-6: # =~ 0
534 scaled, power = self._speed(value, elapsed)
535 else:
536 scaled = power = 0
538 data['unit'] = self.unit
539 if power == 0 and scaled < 0.1:
540 if scaled > 0:
541 scaled = 1 / scaled
542 data['scaled'] = scaled
543 data['prefix'] = self.prefixes[0]
544 return FormatWidgetMixin.__call__(self, progress, data,
545 self.inverse_format)
546 else:
547 data['scaled'] = scaled
548 data['prefix'] = self.prefixes[power]
549 return FormatWidgetMixin.__call__(self, progress, data)
552class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin):
553 '''WidgetBase for showing the transfer speed, based on the last X samples
554 '''
556 def __init__(self, **kwargs):
557 FileTransferSpeed.__init__(self, **kwargs)
558 SamplesMixin.__init__(self, **kwargs)
560 def __call__(self, progress, data):
561 elapsed, value = SamplesMixin.__call__(self, progress, data,
562 delta=True)
563 return FileTransferSpeed.__call__(self, progress, data, value, elapsed)
566class AnimatedMarker(TimeSensitiveWidgetBase):
567 '''An animated marker for the progress bar which defaults to appear as if
568 it were rotating.
569 '''
571 def __init__(self, markers='|/-\\', default=None, fill='',
572 marker_wrap=None, fill_wrap=None, **kwargs):
573 self.markers = markers
574 self.marker_wrap = create_wrapper(marker_wrap)
575 self.default = default or markers[0]
576 self.fill_wrap = create_wrapper(fill_wrap)
577 self.fill = create_marker(fill, self.fill_wrap) if fill else None
578 WidgetBase.__init__(self, **kwargs)
580 def __call__(self, progress, data, width=None):
581 '''Updates the widget to show the next marker or the first marker when
582 finished'''
584 if progress.end_time:
585 return self.default
587 marker = self.markers[data['updates'] % len(self.markers)]
588 if self.marker_wrap:
589 marker = self.marker_wrap.format(marker)
591 if self.fill:
592 # Cut the last character so we can replace it with our marker
593 fill = self.fill(progress, data, width - progress.custom_len(
594 marker))
595 else:
596 fill = ''
598 # Python 3 returns an int when indexing bytes
599 if isinstance(marker, int): # pragma: no cover
600 marker = bytes(marker)
601 fill = fill.encode()
602 else:
603 # cast fill to the same type as marker
604 fill = type(marker)(fill)
606 return fill + marker
609# Alias for backwards compatibility
610RotatingMarker = AnimatedMarker
613class Counter(FormatWidgetMixin, WidgetBase):
614 '''Displays the current count'''
616 def __init__(self, format='%(value)d', **kwargs):
617 FormatWidgetMixin.__init__(self, format=format, **kwargs)
618 WidgetBase.__init__(self, format=format, **kwargs)
620 def __call__(self, progress, data, format=None):
621 return FormatWidgetMixin.__call__(self, progress, data, format)
624class Percentage(FormatWidgetMixin, WidgetBase):
625 '''Displays the current percentage as a number with a percent sign.'''
627 def __init__(self, format='%(percentage)3d%%', **kwargs):
628 FormatWidgetMixin.__init__(self, format=format, **kwargs)
629 WidgetBase.__init__(self, format=format, **kwargs)
631 def __call__(self, progress, data, format=None):
632 # If percentage is not available, display N/A%
633 if 'percentage' in data and not data['percentage']:
634 return FormatWidgetMixin.__call__(self, progress, data,
635 format='N/A%%')
637 return FormatWidgetMixin.__call__(self, progress, data)
640class SimpleProgress(FormatWidgetMixin, WidgetBase):
641 '''Returns progress as a count of the total (e.g.: "5 of 47")'''
643 DEFAULT_FORMAT = '%(value_s)s of %(max_value_s)s'
645 def __init__(self, format=DEFAULT_FORMAT, **kwargs):
646 FormatWidgetMixin.__init__(self, format=format, **kwargs)
647 WidgetBase.__init__(self, format=format, **kwargs)
648 self.max_width_cache = dict(default=self.max_width)
650 def __call__(self, progress, data, format=None):
651 # If max_value is not available, display N/A
652 if data.get('max_value'):
653 data['max_value_s'] = data.get('max_value')
654 else:
655 data['max_value_s'] = 'N/A'
657 # if value is not available it's the zeroth iteration
658 if data.get('value'):
659 data['value_s'] = data['value']
660 else:
661 data['value_s'] = 0
663 formatted = FormatWidgetMixin.__call__(self, progress, data,
664 format=format)
666 # Guess the maximum width from the min and max value
667 key = progress.min_value, progress.max_value
668 max_width = self.max_width_cache.get(key, self.max_width)
669 if not max_width:
670 temporary_data = data.copy()
671 for value in key:
672 if value is None: # pragma: no cover
673 continue
675 temporary_data['value'] = value
676 width = progress.custom_len(FormatWidgetMixin.__call__(
677 self, progress, temporary_data, format=format))
678 if width: # pragma: no branch
679 max_width = max(max_width or 0, width)
681 self.max_width_cache[key] = max_width
683 # Adjust the output to have a consistent size in all cases
684 if max_width: # pragma: no branch
685 formatted = formatted.rjust(max_width)
687 return formatted
690class Bar(AutoWidthWidgetBase):
691 '''A progress bar which stretches to fill the line.'''
693 def __init__(self, marker='#', left='|', right='|', fill=' ',
694 fill_left=True, marker_wrap=None, **kwargs):
695 '''Creates a customizable progress bar.
697 The callable takes the same parameters as the `__call__` method
699 marker - string or callable object to use as a marker
700 left - string or callable object to use as a left border
701 right - string or callable object to use as a right border
702 fill - character to use for the empty part of the progress bar
703 fill_left - whether to fill from the left or the right
704 '''
706 self.marker = create_marker(marker, marker_wrap)
707 self.left = string_or_lambda(left)
708 self.right = string_or_lambda(right)
709 self.fill = string_or_lambda(fill)
710 self.fill_left = fill_left
712 AutoWidthWidgetBase.__init__(self, **kwargs)
714 def __call__(self, progress, data, width):
715 '''Updates the progress bar and its subcomponents'''
717 left = converters.to_unicode(self.left(progress, data, width))
718 right = converters.to_unicode(self.right(progress, data, width))
719 width -= progress.custom_len(left) + progress.custom_len(right)
720 marker = converters.to_unicode(self.marker(progress, data, width))
721 fill = converters.to_unicode(self.fill(progress, data, width))
723 # Make sure we ignore invisible characters when filling
724 width += len(marker) - progress.custom_len(marker)
726 if self.fill_left:
727 marker = marker.ljust(width, fill)
728 else:
729 marker = marker.rjust(width, fill)
731 return left + marker + right
734class ReverseBar(Bar):
735 '''A bar which has a marker that goes from right to left'''
737 def __init__(self, marker='#', left='|', right='|', fill=' ',
738 fill_left=False, **kwargs):
739 '''Creates a customizable progress bar.
741 marker - string or updatable object to use as a marker
742 left - string or updatable object to use as a left border
743 right - string or updatable object to use as a right border
744 fill - character to use for the empty part of the progress bar
745 fill_left - whether to fill from the left or the right
746 '''
747 Bar.__init__(self, marker=marker, left=left, right=right, fill=fill,
748 fill_left=fill_left, **kwargs)
751class BouncingBar(Bar, TimeSensitiveWidgetBase):
752 '''A bar which has a marker which bounces from side to side.'''
754 INTERVAL = datetime.timedelta(milliseconds=100)
756 def __call__(self, progress, data, width):
757 '''Updates the progress bar and its subcomponents'''
759 left = converters.to_unicode(self.left(progress, data, width))
760 right = converters.to_unicode(self.right(progress, data, width))
761 width -= progress.custom_len(left) + progress.custom_len(right)
762 marker = converters.to_unicode(self.marker(progress, data, width))
764 fill = converters.to_unicode(self.fill(progress, data, width))
766 if width: # pragma: no branch
767 value = int(
768 data['total_seconds_elapsed'] / self.INTERVAL.total_seconds())
770 a = value % width
771 b = width - a - 1
772 if value % (width * 2) >= width:
773 a, b = b, a
775 if self.fill_left:
776 marker = a * fill + marker + b * fill
777 else:
778 marker = b * fill + marker + a * fill
780 return left + marker + right
783class FormatCustomText(FormatWidgetMixin, WidgetBase):
784 mapping = {}
786 def __init__(self, format, mapping=mapping, **kwargs):
787 self.format = format
788 self.mapping = mapping
789 FormatWidgetMixin.__init__(self, format=format, **kwargs)
790 WidgetBase.__init__(self, **kwargs)
792 def update_mapping(self, **mapping):
793 self.mapping.update(mapping)
795 def __call__(self, progress, data):
796 return FormatWidgetMixin.__call__(
797 self, progress, self.mapping, self.format)
800class VariableMixin(object):
801 '''Mixin to display a custom user variable '''
803 def __init__(self, name, **kwargs):
804 if not isinstance(name, six.string_types):
805 raise TypeError('Variable(): argument must be a string')
806 if len(name.split()) > 1:
807 raise ValueError('Variable(): argument must be single word')
808 self.name = name
811class MultiRangeBar(Bar, VariableMixin):
812 '''
813 A bar with multiple sub-ranges, each represented by a different symbol
815 The various ranges are represented on a user-defined variable, formatted as
817 .. code-block:: python
819 [
820 ['Symbol1', amount1],
821 ['Symbol2', amount2],
822 ...
823 ]
824 '''
826 def __init__(self, name, markers, **kwargs):
827 VariableMixin.__init__(self, name)
828 Bar.__init__(self, **kwargs)
829 self.markers = [
830 string_or_lambda(marker)
831 for marker in markers
832 ]
834 def get_values(self, progress, data):
835 return data['variables'][self.name] or []
837 def __call__(self, progress, data, width):
838 '''Updates the progress bar and its subcomponents'''
840 left = converters.to_unicode(self.left(progress, data, width))
841 right = converters.to_unicode(self.right(progress, data, width))
842 width -= progress.custom_len(left) + progress.custom_len(right)
843 values = self.get_values(progress, data)
845 values_sum = sum(values)
846 if width and values_sum:
847 middle = ''
848 values_accumulated = 0
849 width_accumulated = 0
850 for marker, value in zip(self.markers, values):
851 marker = converters.to_unicode(marker(progress, data, width))
852 assert progress.custom_len(marker) == 1
854 values_accumulated += value
855 item_width = int(values_accumulated / values_sum * width)
856 item_width -= width_accumulated
857 width_accumulated += item_width
858 middle += item_width * marker
859 else:
860 fill = converters.to_unicode(self.fill(progress, data, width))
861 assert progress.custom_len(fill) == 1
862 middle = fill * width
864 return left + middle + right
867class MultiProgressBar(MultiRangeBar):
868 def __init__(self,
869 name,
870 # NOTE: the markers are not whitespace even though some
871 # terminals don't show the characters correctly!
872 markers=' ▁▂▃▄▅▆▇█',
873 **kwargs):
874 MultiRangeBar.__init__(self, name=name,
875 markers=list(reversed(markers)), **kwargs)
877 def get_values(self, progress, data):
878 ranges = [0] * len(self.markers)
879 for progress in data['variables'][self.name] or []:
880 if not isinstance(progress, (int, float)):
881 # Progress is (value, max)
882 progress_value, progress_max = progress
883 progress = float(progress_value) / float(progress_max)
885 if progress < 0 or progress > 1:
886 raise ValueError(
887 'Range value needs to be in the range [0..1], got %s' %
888 progress)
890 range_ = progress * (len(ranges) - 1)
891 pos = int(range_)
892 frac = range_ % 1
893 ranges[pos] += (1 - frac)
894 if (frac):
895 ranges[pos + 1] += (frac)
897 if self.fill_left:
898 ranges = list(reversed(ranges))
899 return ranges
902class Variable(FormatWidgetMixin, VariableMixin, WidgetBase):
903 '''Displays a custom variable.'''
905 def __init__(self, name, format='{name}: {formatted_value}',
906 width=6, precision=3, **kwargs):
907 '''Creates a Variable associated with the given name.'''
908 self.format = format
909 self.width = width
910 self.precision = precision
911 VariableMixin.__init__(self, name=name)
912 WidgetBase.__init__(self, **kwargs)
914 def __call__(self, progress, data):
915 value = data['variables'][self.name]
916 context = data.copy()
917 context['value'] = value
918 context['name'] = self.name
919 context['width'] = self.width
920 context['precision'] = self.precision
922 try:
923 # Make sure to try and cast the value first, otherwise the
924 # formatting will generate warnings/errors on newer Python releases
925 value = float(value)
926 fmt = '{value:{width}.{precision}}'
927 context['formatted_value'] = fmt.format(**context)
928 except (TypeError, ValueError):
929 if value:
930 context['formatted_value'] = '{value:{width}}'.format(
931 **context)
932 else:
933 context['formatted_value'] = '-' * self.width
935 return self.format.format(**context)
938class DynamicMessage(Variable):
939 '''Kept for backwards compatibility, please use `Variable` instead.'''
940 pass
943class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase):
944 '''Widget which displays the current (date)time with seconds resolution.'''
945 INTERVAL = datetime.timedelta(seconds=1)
947 def __init__(self, format='Current Time: %(current_time)s',
948 microseconds=False, **kwargs):
949 self.microseconds = microseconds
950 FormatWidgetMixin.__init__(self, format=format, **kwargs)
951 TimeSensitiveWidgetBase.__init__(self, **kwargs)
953 def __call__(self, progress, data):
954 data['current_time'] = self.current_time()
955 data['current_datetime'] = self.current_datetime()
957 return FormatWidgetMixin.__call__(self, progress, data)
959 def current_datetime(self):
960 now = datetime.datetime.now()
961 if not self.microseconds:
962 now = now.replace(microsecond=0)
964 return now
966 def current_time(self):
967 return self.current_datetime().time()