Coverage for /Volumes/workspace/python-progressbar/.tox/py310/lib/python3.10/site-packages/progressbar/widgets.py: 99%
507 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-26 14:15 +0200
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-26 14:15 +0200
1# -*- coding: utf-8 -*-
2import abc
3import datetime
4import functools
5import pprint
6import sys
8from python_utils import converters
9from python_utils import types
11from . import base
12from . import utils
14if types.TYPE_CHECKING:
15 from .bar import ProgressBar
17MAX_DATE = datetime.date.max
18MAX_TIME = datetime.time.max
19MAX_DATETIME = datetime.datetime.max
22def string_or_lambda(input_):
23 if isinstance(input_, str):
24 def render_input(progress, data, width):
25 return input_ % data
27 return render_input
28 else:
29 return input_
32def create_wrapper(wrapper):
33 '''Convert a wrapper tuple or format string to a format string
35 >>> create_wrapper('')
37 >>> print(create_wrapper('a{}b'))
38 a{}b
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
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 {}')
55 return wrapper
58def wrapper(function, wrapper):
59 '''Wrap the output of a function in a template string or a tuple with
60 begin/end strings
62 '''
63 wrapper = create_wrapper(wrapper)
64 if not wrapper:
65 return function
67 @functools.wraps(function)
68 def wrap(*args, **kwargs):
69 return wrapper.format(function(*args, **kwargs))
71 return wrap
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
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)
92class FormatWidgetMixin(object):
93 '''Mixin to format widgets using a formatstring
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 = []
109 def __init__(self, format, new_style=False, **kwargs):
110 self.new_style = new_style
111 self.format = format
113 def get_format(self, progress, data, format=None):
114 return format or self.format
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
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: '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
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: str, **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 if '%s' in format and '%(elapsed)s' not in format:
276 format = format.replace('%s', '%(elapsed)s')
278 FormatLabel.__init__(self, format=format, **kwargs)
279 TimeSensitiveWidgetBase.__init__(self, **kwargs)
281 # This is exposed as a static method for backwards compatibility
282 format_time = staticmethod(utils.format_time)
285class SamplesMixin(TimeSensitiveWidgetBase):
286 '''
287 Mixing for widgets that average multiple measurements
289 Note that samples can be either an integer or a timedelta to indicate a
290 certain amount of time
292 >>> class progress:
293 ... last_update_time = datetime.datetime.now()
294 ... value = 1
295 ... extra = dict()
297 >>> samples = SamplesMixin(samples=2)
298 >>> samples(progress, None, True)
299 (None, None)
300 >>> progress.last_update_time += datetime.timedelta(seconds=1)
301 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
302 True
304 >>> progress.last_update_time += datetime.timedelta(seconds=1)
305 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
306 True
308 >>> samples = SamplesMixin(samples=datetime.timedelta(seconds=1))
309 >>> _, value = samples(progress, None)
310 >>> value
311 [1, 1]
313 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
314 True
315 '''
317 def __init__(self, samples=datetime.timedelta(seconds=2), key_prefix=None,
318 **kwargs):
319 self.samples = samples
320 self.key_prefix = (self.__class__.__name__ or key_prefix) + '_'
321 TimeSensitiveWidgetBase.__init__(self, **kwargs)
323 def get_sample_times(self, progress, data):
324 return progress.extra.setdefault(self.key_prefix + 'sample_times', [])
326 def get_sample_values(self, progress, data):
327 return progress.extra.setdefault(self.key_prefix + 'sample_values', [])
329 def __call__(self, progress, data, delta=False):
330 sample_times = self.get_sample_times(progress, data)
331 sample_values = self.get_sample_values(progress, data)
333 if sample_times:
334 sample_time = sample_times[-1]
335 else:
336 sample_time = datetime.datetime.min
338 if progress.last_update_time - sample_time > self.INTERVAL:
339 # Add a sample but limit the size to `num_samples`
340 sample_times.append(progress.last_update_time)
341 sample_values.append(progress.value)
343 if isinstance(self.samples, datetime.timedelta):
344 minimum_time = progress.last_update_time - self.samples
345 minimum_value = sample_values[-1]
346 while (sample_times[2:] and
347 minimum_time > sample_times[1] and
348 minimum_value > sample_values[1]):
349 sample_times.pop(0)
350 sample_values.pop(0)
351 else:
352 if len(sample_times) > self.samples:
353 sample_times.pop(0)
354 sample_values.pop(0)
356 if delta:
357 delta_time = sample_times[-1] - sample_times[0]
358 delta_value = sample_values[-1] - sample_values[0]
359 if delta_time:
360 return delta_time, delta_value
361 else:
362 return None, None
363 else:
364 return sample_times, sample_values
367class ETA(Timer):
368 '''WidgetBase which attempts to estimate the time of arrival.'''
370 def __init__(
371 self,
372 format_not_started='ETA: --:--:--',
373 format_finished='Time: %(elapsed)8s',
374 format='ETA: %(eta)8s',
375 format_zero='ETA: 00:00:00',
376 format_NA='ETA: N/A',
377 **kwargs):
379 if '%s' in format and '%(eta)s' not in format:
380 format = format.replace('%s', '%(eta)s')
382 Timer.__init__(self, **kwargs)
383 self.format_not_started = format_not_started
384 self.format_finished = format_finished
385 self.format = format
386 self.format_zero = format_zero
387 self.format_NA = format_NA
389 def _calculate_eta(self, progress, data, value, elapsed):
390 '''Updates the widget to show the ETA or total time when finished.'''
391 if elapsed:
392 # The max() prevents zero division errors
393 per_item = elapsed.total_seconds() / max(value, 1e-6)
394 remaining = progress.max_value - data['value']
395 eta_seconds = remaining * per_item
396 else:
397 eta_seconds = 0
399 return eta_seconds
401 def __call__(self, progress, data, value=None, elapsed=None):
402 '''Updates the widget to show the ETA or total time when finished.'''
403 if value is None:
404 value = data['value']
406 if elapsed is None:
407 elapsed = data['time_elapsed']
409 ETA_NA = False
410 try:
411 data['eta_seconds'] = self._calculate_eta(
412 progress, data, value=value, elapsed=elapsed)
413 except TypeError:
414 data['eta_seconds'] = None
415 ETA_NA = True
417 data['eta'] = None
418 if data['eta_seconds']:
419 try:
420 data['eta'] = utils.format_time(data['eta_seconds'])
421 except (ValueError, OverflowError): # pragma: no cover
422 pass
424 if data['value'] == progress.min_value:
425 format = self.format_not_started
426 elif progress.end_time:
427 format = self.format_finished
428 elif data['eta']:
429 format = self.format
430 elif ETA_NA:
431 format = self.format_NA
432 else:
433 format = self.format_zero
435 return Timer.__call__(self, progress, data, format=format)
438class AbsoluteETA(ETA):
439 '''Widget which attempts to estimate the absolute time of arrival.'''
441 def _calculate_eta(self, progress, data, value, elapsed):
442 eta_seconds = ETA._calculate_eta(self, progress, data, value, elapsed)
443 now = datetime.datetime.now()
444 try:
445 return now + datetime.timedelta(seconds=eta_seconds)
446 except OverflowError: # pragma: no cover
447 return datetime.datetime.max
449 def __init__(
450 self,
451 format_not_started='Estimated finish time: ----/--/-- --:--:--',
452 format_finished='Finished at: %(elapsed)s',
453 format='Estimated finish time: %(eta)s',
454 **kwargs):
455 ETA.__init__(self, format_not_started=format_not_started,
456 format_finished=format_finished, format=format, **kwargs)
459class AdaptiveETA(ETA, SamplesMixin):
460 '''WidgetBase which attempts to estimate the time of arrival.
462 Uses a sampled average of the speed based on the 10 last updates.
463 Very convenient for resuming the progress halfway.
464 '''
466 def __init__(self, **kwargs):
467 ETA.__init__(self, **kwargs)
468 SamplesMixin.__init__(self, **kwargs)
470 def __call__(self, progress, data):
471 elapsed, value = SamplesMixin.__call__(self, progress, data,
472 delta=True)
473 if not elapsed:
474 value = None
475 elapsed = 0
477 return ETA.__call__(self, progress, data, value=value, elapsed=elapsed)
480class DataSize(FormatWidgetMixin, WidgetBase):
481 '''
482 Widget for showing an amount of data transferred/processed.
484 Automatically formats the value (assumed to be a count of bytes) with an
485 appropriate sized unit, based on the IEC binary prefixes (powers of 1024).
486 '''
488 def __init__(
489 self, variable='value',
490 format='%(scaled)5.1f %(prefix)s%(unit)s', unit='B',
491 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'),
492 **kwargs):
493 self.variable = variable
494 self.unit = unit
495 self.prefixes = prefixes
496 FormatWidgetMixin.__init__(self, format=format, **kwargs)
497 WidgetBase.__init__(self, **kwargs)
499 def __call__(self, progress, data):
500 value = data[self.variable]
501 if value is not None:
502 scaled, power = utils.scale_1024(value, len(self.prefixes))
503 else:
504 scaled = power = 0
506 data['scaled'] = scaled
507 data['prefix'] = self.prefixes[power]
508 data['unit'] = self.unit
510 return FormatWidgetMixin.__call__(self, progress, data)
513class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase):
514 '''
515 WidgetBase for showing the transfer speed (useful for file transfers).
516 '''
518 def __init__(
519 self, format='%(scaled)5.1f %(prefix)s%(unit)-s/s',
520 inverse_format='%(scaled)5.1f s/%(prefix)s%(unit)-s', unit='B',
521 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'),
522 **kwargs):
523 self.unit = unit
524 self.prefixes = prefixes
525 self.inverse_format = inverse_format
526 FormatWidgetMixin.__init__(self, format=format, **kwargs)
527 TimeSensitiveWidgetBase.__init__(self, **kwargs)
529 def _speed(self, value, elapsed):
530 speed = float(value) / elapsed
531 return utils.scale_1024(speed, len(self.prefixes))
533 def __call__(self, progress, data, value=None, total_seconds_elapsed=None):
534 '''Updates the widget with the current SI prefixed speed.'''
535 if value is None:
536 value = data['value']
538 elapsed = utils.deltas_to_seconds(
539 total_seconds_elapsed,
540 data['total_seconds_elapsed'])
542 if value is not None and elapsed is not None \
543 and elapsed > 2e-6 and value > 2e-6: # =~ 0
544 scaled, power = self._speed(value, elapsed)
545 else:
546 scaled = power = 0
548 data['unit'] = self.unit
549 if power == 0 and scaled < 0.1:
550 if scaled > 0:
551 scaled = 1 / scaled
552 data['scaled'] = scaled
553 data['prefix'] = self.prefixes[0]
554 return FormatWidgetMixin.__call__(self, progress, data,
555 self.inverse_format)
556 else:
557 data['scaled'] = scaled
558 data['prefix'] = self.prefixes[power]
559 return FormatWidgetMixin.__call__(self, progress, data)
562class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin):
563 '''WidgetBase for showing the transfer speed, based on the last X samples
564 '''
566 def __init__(self, **kwargs):
567 FileTransferSpeed.__init__(self, **kwargs)
568 SamplesMixin.__init__(self, **kwargs)
570 def __call__(self, progress, data):
571 elapsed, value = SamplesMixin.__call__(self, progress, data,
572 delta=True)
573 return FileTransferSpeed.__call__(self, progress, data, value, elapsed)
576class AnimatedMarker(TimeSensitiveWidgetBase):
577 '''An animated marker for the progress bar which defaults to appear as if
578 it were rotating.
579 '''
581 def __init__(self, markers='|/-\\', default=None, fill='',
582 marker_wrap=None, fill_wrap=None, **kwargs):
583 self.markers = markers
584 self.marker_wrap = create_wrapper(marker_wrap)
585 self.default = default or markers[0]
586 self.fill_wrap = create_wrapper(fill_wrap)
587 self.fill = create_marker(fill, self.fill_wrap) if fill else None
588 WidgetBase.__init__(self, **kwargs)
590 def __call__(self, progress, data, width=None):
591 '''Updates the widget to show the next marker or the first marker when
592 finished'''
594 if progress.end_time:
595 return self.default
597 marker = self.markers[data['updates'] % len(self.markers)]
598 if self.marker_wrap:
599 marker = self.marker_wrap.format(marker)
601 if self.fill:
602 # Cut the last character so we can replace it with our marker
603 fill = self.fill(progress, data, width - progress.custom_len(
604 marker))
605 else:
606 fill = ''
608 # Python 3 returns an int when indexing bytes
609 if isinstance(marker, int): # pragma: no cover
610 marker = bytes(marker)
611 fill = fill.encode()
612 else:
613 # cast fill to the same type as marker
614 fill = type(marker)(fill)
616 return fill + marker
619# Alias for backwards compatibility
620RotatingMarker = AnimatedMarker
623class Counter(FormatWidgetMixin, WidgetBase):
624 '''Displays the current count'''
626 def __init__(self, format='%(value)d', **kwargs):
627 FormatWidgetMixin.__init__(self, format=format, **kwargs)
628 WidgetBase.__init__(self, format=format, **kwargs)
630 def __call__(self, progress, data, format=None):
631 return FormatWidgetMixin.__call__(self, progress, data, format)
634class Percentage(FormatWidgetMixin, WidgetBase):
635 '''Displays the current percentage as a number with a percent sign.'''
637 def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs):
638 self.na = na
639 FormatWidgetMixin.__init__(self, format=format, **kwargs)
640 WidgetBase.__init__(self, format=format, **kwargs)
642 def get_format(self, progress, data, format=None):
643 # If percentage is not available, display N/A%
644 percentage = data.get('percentage', base.Undefined)
645 if not percentage and percentage != 0:
646 return self.na
648 return FormatWidgetMixin.get_format(self, progress, data, format)
651class SimpleProgress(FormatWidgetMixin, WidgetBase):
652 '''Returns progress as a count of the total (e.g.: "5 of 47")'''
654 DEFAULT_FORMAT = '%(value_s)s of %(max_value_s)s'
656 def __init__(self, format=DEFAULT_FORMAT, **kwargs):
657 FormatWidgetMixin.__init__(self, format=format, **kwargs)
658 WidgetBase.__init__(self, format=format, **kwargs)
659 self.max_width_cache = dict(default=self.max_width)
661 def __call__(self, progress, data, format=None):
662 # If max_value is not available, display N/A
663 if data.get('max_value'):
664 data['max_value_s'] = data.get('max_value')
665 else:
666 data['max_value_s'] = 'N/A'
668 # if value is not available it's the zeroth iteration
669 if data.get('value'):
670 data['value_s'] = data['value']
671 else:
672 data['value_s'] = 0
674 formatted = FormatWidgetMixin.__call__(self, progress, data,
675 format=format)
677 # Guess the maximum width from the min and max value
678 key = progress.min_value, progress.max_value
679 max_width = self.max_width_cache.get(key, self.max_width)
680 if not max_width:
681 temporary_data = data.copy()
682 for value in key:
683 if value is None: # pragma: no cover
684 continue
686 temporary_data['value'] = value
687 width = progress.custom_len(FormatWidgetMixin.__call__(
688 self, progress, temporary_data, format=format))
689 if width: # pragma: no branch
690 max_width = max(max_width or 0, width)
692 self.max_width_cache[key] = max_width
694 # Adjust the output to have a consistent size in all cases
695 if max_width: # pragma: no branch
696 formatted = formatted.rjust(max_width)
698 return formatted
701class Bar(AutoWidthWidgetBase):
702 '''A progress bar which stretches to fill the line.'''
704 def __init__(self, marker='#', left='|', right='|', fill=' ',
705 fill_left=True, marker_wrap=None, **kwargs):
706 '''Creates a customizable progress bar.
708 The callable takes the same parameters as the `__call__` method
710 marker - string or callable object to use as a marker
711 left - string or callable object to use as a left border
712 right - string or callable object to use as a right border
713 fill - character to use for the empty part of the progress bar
714 fill_left - whether to fill from the left or the right
715 '''
717 self.marker = create_marker(marker, marker_wrap)
718 self.left = string_or_lambda(left)
719 self.right = string_or_lambda(right)
720 self.fill = string_or_lambda(fill)
721 self.fill_left = fill_left
723 AutoWidthWidgetBase.__init__(self, **kwargs)
725 def __call__(self, progress, data, width):
726 '''Updates the progress bar and its subcomponents'''
728 left = converters.to_unicode(self.left(progress, data, width))
729 right = converters.to_unicode(self.right(progress, data, width))
730 width -= progress.custom_len(left) + progress.custom_len(right)
731 marker = converters.to_unicode(self.marker(progress, data, width))
732 fill = converters.to_unicode(self.fill(progress, data, width))
734 # Make sure we ignore invisible characters when filling
735 width += len(marker) - progress.custom_len(marker)
737 if self.fill_left:
738 marker = marker.ljust(width, fill)
739 else:
740 marker = marker.rjust(width, fill)
742 return left + marker + right
745class ReverseBar(Bar):
746 '''A bar which has a marker that goes from right to left'''
748 def __init__(self, marker='#', left='|', right='|', fill=' ',
749 fill_left=False, **kwargs):
750 '''Creates a customizable progress bar.
752 marker - string or updatable object to use as a marker
753 left - string or updatable object to use as a left border
754 right - string or updatable object to use as a right border
755 fill - character to use for the empty part of the progress bar
756 fill_left - whether to fill from the left or the right
757 '''
758 Bar.__init__(self, marker=marker, left=left, right=right, fill=fill,
759 fill_left=fill_left, **kwargs)
762class BouncingBar(Bar, TimeSensitiveWidgetBase):
763 '''A bar which has a marker which bounces from side to side.'''
765 INTERVAL = datetime.timedelta(milliseconds=100)
767 def __call__(self, progress, data, width):
768 '''Updates the progress bar and its subcomponents'''
770 left = converters.to_unicode(self.left(progress, data, width))
771 right = converters.to_unicode(self.right(progress, data, width))
772 width -= progress.custom_len(left) + progress.custom_len(right)
773 marker = converters.to_unicode(self.marker(progress, data, width))
775 fill = converters.to_unicode(self.fill(progress, data, width))
777 if width: # pragma: no branch
778 value = int(
779 data['total_seconds_elapsed'] / self.INTERVAL.total_seconds())
781 a = value % width
782 b = width - a - 1
783 if value % (width * 2) >= width:
784 a, b = b, a
786 if self.fill_left:
787 marker = a * fill + marker + b * fill
788 else:
789 marker = b * fill + marker + a * fill
791 return left + marker + right
794class FormatCustomText(FormatWidgetMixin, WidgetBase):
795 mapping = {}
796 copy = False
798 def __init__(self, format, mapping=mapping, **kwargs):
799 self.format = format
800 self.mapping = mapping
801 FormatWidgetMixin.__init__(self, format=format, **kwargs)
802 WidgetBase.__init__(self, **kwargs)
804 def update_mapping(self, **mapping):
805 self.mapping.update(mapping)
807 def __call__(self, progress, data):
808 return FormatWidgetMixin.__call__(
809 self, progress, self.mapping, self.format)
812class VariableMixin(object):
813 '''Mixin to display a custom user variable '''
815 def __init__(self, name, **kwargs):
816 if not isinstance(name, str):
817 raise TypeError('Variable(): argument must be a string')
818 if len(name.split()) > 1:
819 raise ValueError('Variable(): argument must be single word')
820 self.name = name
823class MultiRangeBar(Bar, VariableMixin):
824 '''
825 A bar with multiple sub-ranges, each represented by a different symbol
827 The various ranges are represented on a user-defined variable, formatted as
829 .. code-block:: python
831 [
832 ['Symbol1', amount1],
833 ['Symbol2', amount2],
834 ...
835 ]
836 '''
838 def __init__(self, name, markers, **kwargs):
839 VariableMixin.__init__(self, name)
840 Bar.__init__(self, **kwargs)
841 self.markers = [
842 string_or_lambda(marker)
843 for marker in markers
844 ]
846 def get_values(self, progress, data):
847 return data['variables'][self.name] or []
849 def __call__(self, progress, data, width):
850 '''Updates the progress bar and its subcomponents'''
852 left = converters.to_unicode(self.left(progress, data, width))
853 right = converters.to_unicode(self.right(progress, data, width))
854 width -= progress.custom_len(left) + progress.custom_len(right)
855 values = self.get_values(progress, data)
857 values_sum = sum(values)
858 if width and values_sum:
859 middle = ''
860 values_accumulated = 0
861 width_accumulated = 0
862 for marker, value in zip(self.markers, values):
863 marker = converters.to_unicode(marker(progress, data, width))
864 assert progress.custom_len(marker) == 1
866 values_accumulated += value
867 item_width = int(values_accumulated / values_sum * width)
868 item_width -= width_accumulated
869 width_accumulated += item_width
870 middle += item_width * marker
871 else:
872 fill = converters.to_unicode(self.fill(progress, data, width))
873 assert progress.custom_len(fill) == 1
874 middle = fill * width
876 return left + middle + right
879class MultiProgressBar(MultiRangeBar):
880 def __init__(self,
881 name,
882 # NOTE: the markers are not whitespace even though some
883 # terminals don't show the characters correctly!
884 markers=' ▁▂▃▄▅▆▇█',
885 **kwargs):
886 MultiRangeBar.__init__(self, name=name,
887 markers=list(reversed(markers)), **kwargs)
889 def get_values(self, progress, data):
890 ranges = [0] * len(self.markers)
891 for progress in data['variables'][self.name] or []:
892 if not isinstance(progress, (int, float)):
893 # Progress is (value, max)
894 progress_value, progress_max = progress
895 progress = float(progress_value) / float(progress_max)
897 if progress < 0 or progress > 1:
898 raise ValueError(
899 'Range value needs to be in the range [0..1], got %s' %
900 progress)
902 range_ = progress * (len(ranges) - 1)
903 pos = int(range_)
904 frac = range_ % 1
905 ranges[pos] += (1 - frac)
906 if (frac):
907 ranges[pos + 1] += (frac)
909 if self.fill_left:
910 ranges = list(reversed(ranges))
911 return ranges
914class GranularMarkers:
915 smooth = ' ▏▎▍▌▋▊▉█'
916 bar = ' ▁▂▃▄▅▆▇█'
917 snake = ' ▖▌▛█'
918 fade_in = ' ░▒▓█'
919 dots = ' ⡀⡄⡆⡇⣇⣧⣷⣿'
920 growing_circles = ' .oO'
923class GranularBar(AutoWidthWidgetBase):
924 '''A progressbar that can display progress at a sub-character granularity
925 by using multiple marker characters.
927 Examples of markers:
928 - Smooth: ` ▏▎▍▌▋▊▉█` (default)
929 - Bar: ` ▁▂▃▄▅▆▇█`
930 - Snake: ` ▖▌▛█`
931 - Fade in: ` ░▒▓█`
932 - Dots: ` ⡀⡄⡆⡇⣇⣧⣷⣿`
933 - Growing circles: ` .oO`
935 The markers can be accessed through GranularMarkers. GranularMarkers.dots
936 for example
937 '''
939 def __init__(self, markers=GranularMarkers.smooth, left='|', right='|',
940 **kwargs):
941 '''Creates a customizable progress bar.
943 markers - string of characters to use as granular progress markers. The
944 first character should represent 0% and the last 100%.
945 Ex: ` .oO`.
946 left - string or callable object to use as a left border
947 right - string or callable object to use as a right border
948 '''
949 self.markers = markers
950 self.left = string_or_lambda(left)
951 self.right = string_or_lambda(right)
953 AutoWidthWidgetBase.__init__(self, **kwargs)
955 def __call__(self, progress, data, width):
956 left = converters.to_unicode(self.left(progress, data, width))
957 right = converters.to_unicode(self.right(progress, data, width))
958 width -= progress.custom_len(left) + progress.custom_len(right)
960 if progress.max_value is not base.UnknownLength \
961 and progress.max_value > 0:
962 percent = progress.value / progress.max_value
963 else:
964 percent = 0
966 num_chars = percent * width
968 marker = self.markers[-1] * int(num_chars)
970 marker_idx = int((num_chars % 1) * (len(self.markers) - 1))
971 if marker_idx:
972 marker += self.markers[marker_idx]
974 marker = converters.to_unicode(marker)
976 # Make sure we ignore invisible characters when filling
977 width += len(marker) - progress.custom_len(marker)
978 marker = marker.ljust(width, self.markers[0])
980 return left + marker + right
983class FormatLabelBar(FormatLabel, Bar):
984 '''A bar which has a formatted label in the center.'''
986 def __init__(self, format, **kwargs):
987 FormatLabel.__init__(self, format, **kwargs)
988 Bar.__init__(self, **kwargs)
990 def __call__(self, progress, data, width, format=None):
991 center = FormatLabel.__call__(self, progress, data, format=format)
992 bar = Bar.__call__(self, progress, data, width)
994 # Aligns the center of the label to the center of the bar
995 center_len = progress.custom_len(center)
996 center_left = int((width - center_len) / 2)
997 center_right = center_left + center_len
998 return bar[:center_left] + center + bar[center_right:]
1001class PercentageLabelBar(Percentage, FormatLabelBar):
1002 '''A bar which displays the current percentage in the center.'''
1004 # %3d adds an extra space that makes it look off-center
1005 # %2d keeps the label somewhat consistently in-place
1006 def __init__(self, format='%(percentage)2d%%', na='N/A%%', **kwargs):
1007 Percentage.__init__(self, format, na=na, **kwargs)
1008 FormatLabelBar.__init__(self, format, **kwargs)
1011class Variable(FormatWidgetMixin, VariableMixin, WidgetBase):
1012 '''Displays a custom variable.'''
1014 def __init__(self, name, format='{name}: {formatted_value}',
1015 width=6, precision=3, **kwargs):
1016 '''Creates a Variable associated with the given name.'''
1017 self.format = format
1018 self.width = width
1019 self.precision = precision
1020 VariableMixin.__init__(self, name=name)
1021 WidgetBase.__init__(self, **kwargs)
1023 def __call__(self, progress, data):
1024 value = data['variables'][self.name]
1025 context = data.copy()
1026 context['value'] = value
1027 context['name'] = self.name
1028 context['width'] = self.width
1029 context['precision'] = self.precision
1031 try:
1032 # Make sure to try and cast the value first, otherwise the
1033 # formatting will generate warnings/errors on newer Python releases
1034 value = float(value)
1035 fmt = '{value:{width}.{precision}}'
1036 context['formatted_value'] = fmt.format(**context)
1037 except (TypeError, ValueError):
1038 if value:
1039 context['formatted_value'] = '{value:{width}}'.format(
1040 **context)
1041 else:
1042 context['formatted_value'] = '-' * self.width
1044 return self.format.format(**context)
1047class DynamicMessage(Variable):
1048 '''Kept for backwards compatibility, please use `Variable` instead.'''
1049 pass
1052class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase):
1053 '''Widget which displays the current (date)time with seconds resolution.'''
1054 INTERVAL = datetime.timedelta(seconds=1)
1056 def __init__(self, format='Current Time: %(current_time)s',
1057 microseconds=False, **kwargs):
1058 self.microseconds = microseconds
1059 FormatWidgetMixin.__init__(self, format=format, **kwargs)
1060 TimeSensitiveWidgetBase.__init__(self, **kwargs)
1062 def __call__(self, progress, data):
1063 data['current_time'] = self.current_time()
1064 data['current_datetime'] = self.current_datetime()
1066 return FormatWidgetMixin.__call__(self, progress, data)
1068 def current_datetime(self):
1069 now = datetime.datetime.now()
1070 if not self.microseconds:
1071 now = now.replace(microsecond=0)
1073 return now
1075 def current_time(self):
1076 return self.current_datetime().time()