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 >>> 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 __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 - 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
198 @abc.abstractmethod
199 def __call__(self, progress, data):
200 '''Updates the widget.
202 progress - a reference to the calling ProgressBar
203 '''
206class AutoWidthWidgetBase(WidgetBase):
207 '''The base class for all variable width widgets.
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 '''
214 @abc.abstractmethod
215 def __call__(self, progress, data, width):
216 '''Updates the widget providing the total width the widget must fill.
218 progress - a reference to the calling ProgressBar
219 width - The total width the widget must fill
220 '''
223class TimeSensitiveWidgetBase(WidgetBase):
224 '''The base class for all time sensitive widgets.
226 Some widgets like timers would become out of date unless updated at least
227 every `INTERVAL`
228 '''
229 INTERVAL = datetime.timedelta(milliseconds=100)
232class FormatLabel(FormatWidgetMixin, WidgetBase):
233 '''Displays a formatted label
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 '
242 '''
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 }
254 def __init__(self, format, **kwargs):
255 FormatWidgetMixin.__init__(self, format=format, **kwargs)
256 WidgetBase.__init__(self, **kwargs)
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
268 return FormatWidgetMixin.__call__(self, progress, data, **kwargs)
271class Timer(FormatLabel, TimeSensitiveWidgetBase):
272 '''WidgetBase which displays the elapsed seconds.'''
274 def __init__(self, format='Elapsed Time: %(elapsed)s', **kwargs):
275 FormatLabel.__init__(self, format=format, **kwargs)
276 TimeSensitiveWidgetBase.__init__(self, **kwargs)
278 # This is exposed as a static method for backwards compatibility
279 format_time = staticmethod(utils.format_time)
282class SamplesMixin(TimeSensitiveWidgetBase):
283 '''
284 Mixing for widgets that average multiple measurements
286 Note that samples can be either an integer or a timedelta to indicate a
287 certain amount of time
289 >>> class progress:
290 ... last_update_time = datetime.datetime.now()
291 ... value = 1
292 ... extra = dict()
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
301 >>> progress.last_update_time += datetime.timedelta(seconds=1)
302 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
303 True
305 >>> samples = SamplesMixin(samples=datetime.timedelta(seconds=1))
306 >>> _, value = samples(progress, None)
307 >>> value
308 [1, 1]
310 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
311 True
312 '''
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)
320 def get_sample_times(self, progress, data):
321 return progress.extra.setdefault(self.key_prefix + 'sample_times', [])
323 def get_sample_values(self, progress, data):
324 return progress.extra.setdefault(self.key_prefix + 'sample_values', [])
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)
330 if sample_times:
331 sample_time = sample_times[-1]
332 else:
333 sample_time = datetime.datetime.min
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)
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)
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
364class ETA(Timer):
365 '''WidgetBase which attempts to estimate the time of arrival.'''
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):
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
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
393 return eta_seconds
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']
400 if elapsed is None:
401 elapsed = data['time_elapsed']
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
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
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
429 return Timer.__call__(self, progress, data, format=format)
432class AbsoluteETA(ETA):
433 '''Widget which attempts to estimate the absolute time of arrival.'''
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
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)
453class AdaptiveETA(ETA, SamplesMixin):
454 '''WidgetBase which attempts to estimate the time of arrival.
456 Uses a sampled average of the speed based on the 10 last updates.
457 Very convenient for resuming the progress halfway.
458 '''
460 def __init__(self, **kwargs):
461 ETA.__init__(self, **kwargs)
462 SamplesMixin.__init__(self, **kwargs)
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
471 return ETA.__call__(self, progress, data, value=value, elapsed=elapsed)
474class DataSize(FormatWidgetMixin, WidgetBase):
475 '''
476 Widget for showing an amount of data transferred/processed.
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 '''
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)
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
500 data['scaled'] = scaled
501 data['prefix'] = self.prefixes[power]
502 data['unit'] = self.unit
504 return FormatWidgetMixin.__call__(self, progress, data)
507class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase):
508 '''
509 WidgetBase for showing the transfer speed (useful for file transfers).
510 '''
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)
523 def _speed(self, value, elapsed):
524 speed = float(value) / elapsed
525 return utils.scale_1024(speed, len(self.prefixes))
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']
532 elapsed = utils.deltas_to_seconds(
533 total_seconds_elapsed,
534 data['total_seconds_elapsed'])
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
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)
556class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin):
557 '''WidgetBase for showing the transfer speed, based on the last X samples
558 '''
560 def __init__(self, **kwargs):
561 FileTransferSpeed.__init__(self, **kwargs)
562 SamplesMixin.__init__(self, **kwargs)
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)
570class AnimatedMarker(TimeSensitiveWidgetBase):
571 '''An animated marker for the progress bar which defaults to appear as if
572 it were rotating.
573 '''
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)
584 def __call__(self, progress, data, width=None):
585 '''Updates the widget to show the next marker or the first marker when
586 finished'''
588 if progress.end_time:
589 return self.default
591 marker = self.markers[data['updates'] % len(self.markers)]
592 if self.marker_wrap:
593 marker = self.marker_wrap.format(marker)
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 = ''
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)
610 return fill + marker
613# Alias for backwards compatibility
614RotatingMarker = AnimatedMarker
617class Counter(FormatWidgetMixin, WidgetBase):
618 '''Displays the current count'''
620 def __init__(self, format='%(value)d', **kwargs):
621 FormatWidgetMixin.__init__(self, format=format, **kwargs)
622 WidgetBase.__init__(self, format=format, **kwargs)
624 def __call__(self, progress, data, format=None):
625 return FormatWidgetMixin.__call__(self, progress, data, format)
628class Percentage(FormatWidgetMixin, WidgetBase):
629 '''Displays the current percentage as a number with a percent sign.'''
631 def __init__(self, format='%(percentage)3d%%', **kwargs):
632 FormatWidgetMixin.__init__(self, format=format, **kwargs)
633 WidgetBase.__init__(self, format=format, **kwargs)
635 def __call__(self, progress, data, format=None):
636 # If percentage is not available, display N/A%
637 if 'percentage' in data and not data['percentage']:
638 return FormatWidgetMixin.__call__(self, progress, data,
639 format='N/A%%')
641 return FormatWidgetMixin.__call__(self, progress, data)
644class SimpleProgress(FormatWidgetMixin, WidgetBase):
645 '''Returns progress as a count of the total (e.g.: "5 of 47")'''
647 DEFAULT_FORMAT = '%(value_s)s of %(max_value_s)s'
649 def __init__(self, format=DEFAULT_FORMAT, **kwargs):
650 FormatWidgetMixin.__init__(self, format=format, **kwargs)
651 WidgetBase.__init__(self, format=format, **kwargs)
652 self.max_width_cache = dict(default=self.max_width)
654 def __call__(self, progress, data, format=None):
655 # If max_value is not available, display N/A
656 if data.get('max_value'):
657 data['max_value_s'] = data.get('max_value')
658 else:
659 data['max_value_s'] = 'N/A'
661 # if value is not available it's the zeroth iteration
662 if data.get('value'):
663 data['value_s'] = data['value']
664 else:
665 data['value_s'] = 0
667 formatted = FormatWidgetMixin.__call__(self, progress, data,
668 format=format)
670 # Guess the maximum width from the min and max value
671 key = progress.min_value, progress.max_value
672 max_width = self.max_width_cache.get(key, self.max_width)
673 if not max_width:
674 temporary_data = data.copy()
675 for value in key:
676 if value is None: # pragma: no cover
677 continue
679 temporary_data['value'] = value
680 width = progress.custom_len(FormatWidgetMixin.__call__(
681 self, progress, temporary_data, format=format))
682 if width: # pragma: no branch
683 max_width = max(max_width or 0, width)
685 self.max_width_cache[key] = max_width
687 # Adjust the output to have a consistent size in all cases
688 if max_width: # pragma: no branch
689 formatted = formatted.rjust(max_width)
691 return formatted
694class Bar(AutoWidthWidgetBase):
695 '''A progress bar which stretches to fill the line.'''
697 def __init__(self, marker='#', left='|', right='|', fill=' ',
698 fill_left=True, marker_wrap=None, **kwargs):
699 '''Creates a customizable progress bar.
701 The callable takes the same parameters as the `__call__` method
703 marker - string or callable object to use as a marker
704 left - string or callable object to use as a left border
705 right - string or callable object to use as a right border
706 fill - character to use for the empty part of the progress bar
707 fill_left - whether to fill from the left or the right
708 '''
710 self.marker = create_marker(marker, marker_wrap)
711 self.left = string_or_lambda(left)
712 self.right = string_or_lambda(right)
713 self.fill = string_or_lambda(fill)
714 self.fill_left = fill_left
716 AutoWidthWidgetBase.__init__(self, **kwargs)
718 def __call__(self, progress, data, width):
719 '''Updates the progress bar and its subcomponents'''
721 left = converters.to_unicode(self.left(progress, data, width))
722 right = converters.to_unicode(self.right(progress, data, width))
723 width -= progress.custom_len(left) + progress.custom_len(right)
724 marker = converters.to_unicode(self.marker(progress, data, width))
725 fill = converters.to_unicode(self.fill(progress, data, width))
727 # Make sure we ignore invisible characters when filling
728 width += len(marker) - progress.custom_len(marker)
730 if self.fill_left:
731 marker = marker.ljust(width, fill)
732 else:
733 marker = marker.rjust(width, fill)
735 return left + marker + right
738class ReverseBar(Bar):
739 '''A bar which has a marker that goes from right to left'''
741 def __init__(self, marker='#', left='|', right='|', fill=' ',
742 fill_left=False, **kwargs):
743 '''Creates a customizable progress bar.
745 marker - string or updatable object to use as a marker
746 left - string or updatable object to use as a left border
747 right - string or updatable object to use as a right border
748 fill - character to use for the empty part of the progress bar
749 fill_left - whether to fill from the left or the right
750 '''
751 Bar.__init__(self, marker=marker, left=left, right=right, fill=fill,
752 fill_left=fill_left, **kwargs)
755class BouncingBar(Bar, TimeSensitiveWidgetBase):
756 '''A bar which has a marker which bounces from side to side.'''
758 INTERVAL = datetime.timedelta(milliseconds=100)
760 def __call__(self, progress, data, width):
761 '''Updates the progress bar and its subcomponents'''
763 left = converters.to_unicode(self.left(progress, data, width))
764 right = converters.to_unicode(self.right(progress, data, width))
765 width -= progress.custom_len(left) + progress.custom_len(right)
766 marker = converters.to_unicode(self.marker(progress, data, width))
768 fill = converters.to_unicode(self.fill(progress, data, width))
770 if width: # pragma: no branch
771 value = int(
772 data['total_seconds_elapsed'] / self.INTERVAL.total_seconds())
774 a = value % width
775 b = width - a - 1
776 if value % (width * 2) >= width:
777 a, b = b, a
779 if self.fill_left:
780 marker = a * fill + marker + b * fill
781 else:
782 marker = b * fill + marker + a * fill
784 return left + marker + right
787class FormatCustomText(FormatWidgetMixin, WidgetBase):
788 mapping = {}
789 copy = False
791 def __init__(self, format, mapping=mapping, **kwargs):
792 self.format = format
793 self.mapping = mapping
794 FormatWidgetMixin.__init__(self, format=format, **kwargs)
795 WidgetBase.__init__(self, **kwargs)
797 def update_mapping(self, **mapping):
798 self.mapping.update(mapping)
800 def __call__(self, progress, data):
801 return FormatWidgetMixin.__call__(
802 self, progress, self.mapping, self.format)
805class VariableMixin(object):
806 '''Mixin to display a custom user variable '''
808 def __init__(self, name, **kwargs):
809 if not isinstance(name, six.string_types):
810 raise TypeError('Variable(): argument must be a string')
811 if len(name.split()) > 1:
812 raise ValueError('Variable(): argument must be single word')
813 self.name = name
816class MultiRangeBar(Bar, VariableMixin):
817 '''
818 A bar with multiple sub-ranges, each represented by a different symbol
820 The various ranges are represented on a user-defined variable, formatted as
822 .. code-block:: python
824 [
825 ['Symbol1', amount1],
826 ['Symbol2', amount2],
827 ...
828 ]
829 '''
831 def __init__(self, name, markers, **kwargs):
832 VariableMixin.__init__(self, name)
833 Bar.__init__(self, **kwargs)
834 self.markers = [
835 string_or_lambda(marker)
836 for marker in markers
837 ]
839 def get_values(self, progress, data):
840 return data['variables'][self.name] or []
842 def __call__(self, progress, data, width):
843 '''Updates the progress bar and its subcomponents'''
845 left = converters.to_unicode(self.left(progress, data, width))
846 right = converters.to_unicode(self.right(progress, data, width))
847 width -= progress.custom_len(left) + progress.custom_len(right)
848 values = self.get_values(progress, data)
850 values_sum = sum(values)
851 if width and values_sum:
852 middle = ''
853 values_accumulated = 0
854 width_accumulated = 0
855 for marker, value in zip(self.markers, values):
856 marker = converters.to_unicode(marker(progress, data, width))
857 assert progress.custom_len(marker) == 1
859 values_accumulated += value
860 item_width = int(values_accumulated / values_sum * width)
861 item_width -= width_accumulated
862 width_accumulated += item_width
863 middle += item_width * marker
864 else:
865 fill = converters.to_unicode(self.fill(progress, data, width))
866 assert progress.custom_len(fill) == 1
867 middle = fill * width
869 return left + middle + right
872class MultiProgressBar(MultiRangeBar):
873 def __init__(self,
874 name,
875 # NOTE: the markers are not whitespace even though some
876 # terminals don't show the characters correctly!
877 markers=' ▁▂▃▄▅▆▇█',
878 **kwargs):
879 MultiRangeBar.__init__(self, name=name,
880 markers=list(reversed(markers)), **kwargs)
882 def get_values(self, progress, data):
883 ranges = [0] * len(self.markers)
884 for progress in data['variables'][self.name] or []:
885 if not isinstance(progress, (int, float)):
886 # Progress is (value, max)
887 progress_value, progress_max = progress
888 progress = float(progress_value) / float(progress_max)
890 if progress < 0 or progress > 1:
891 raise ValueError(
892 'Range value needs to be in the range [0..1], got %s' %
893 progress)
895 range_ = progress * (len(ranges) - 1)
896 pos = int(range_)
897 frac = range_ % 1
898 ranges[pos] += (1 - frac)
899 if (frac):
900 ranges[pos + 1] += (frac)
902 if self.fill_left:
903 ranges = list(reversed(ranges))
904 return ranges
907class Variable(FormatWidgetMixin, VariableMixin, WidgetBase):
908 '''Displays a custom variable.'''
910 def __init__(self, name, format='{name}: {formatted_value}',
911 width=6, precision=3, **kwargs):
912 '''Creates a Variable associated with the given name.'''
913 self.format = format
914 self.width = width
915 self.precision = precision
916 VariableMixin.__init__(self, name=name)
917 WidgetBase.__init__(self, **kwargs)
919 def __call__(self, progress, data):
920 value = data['variables'][self.name]
921 context = data.copy()
922 context['value'] = value
923 context['name'] = self.name
924 context['width'] = self.width
925 context['precision'] = self.precision
927 try:
928 # Make sure to try and cast the value first, otherwise the
929 # formatting will generate warnings/errors on newer Python releases
930 value = float(value)
931 fmt = '{value:{width}.{precision}}'
932 context['formatted_value'] = fmt.format(**context)
933 except (TypeError, ValueError):
934 if value:
935 context['formatted_value'] = '{value:{width}}'.format(
936 **context)
937 else:
938 context['formatted_value'] = '-' * self.width
940 return self.format.format(**context)
943class DynamicMessage(Variable):
944 '''Kept for backwards compatibility, please use `Variable` instead.'''
945 pass
948class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase):
949 '''Widget which displays the current (date)time with seconds resolution.'''
950 INTERVAL = datetime.timedelta(seconds=1)
952 def __init__(self, format='Current Time: %(current_time)s',
953 microseconds=False, **kwargs):
954 self.microseconds = microseconds
955 FormatWidgetMixin.__init__(self, format=format, **kwargs)
956 TimeSensitiveWidgetBase.__init__(self, **kwargs)
958 def __call__(self, progress, data):
959 data['current_time'] = self.current_time()
960 data['current_datetime'] = self.current_datetime()
962 return FormatWidgetMixin.__call__(self, progress, data)
964 def current_datetime(self):
965 now = datetime.datetime.now()
966 if not self.microseconds:
967 now = now.replace(microsecond=0)
969 return now
971 def current_time(self):
972 return self.current_datetime().time()