Coverage for /Volumes/workspace/python-progressbar/.tox/py37/lib/python3.7/site-packages/progressbar/widgets.py: 99%
509 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-01 16:14 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-01 16:14 +0100
1# -*- coding: utf-8 -*-
2from __future__ import annotations
4import abc
5import datetime
6import functools
7import pprint
8import sys
9import typing
11from python_utils import converters, types
13from . import base, utils
15if types.TYPE_CHECKING:
16 from .bar import ProgressBarMixinBase
18MAX_DATE = datetime.date.max
19MAX_TIME = datetime.time.max
20MAX_DATETIME = datetime.datetime.max
22Data = types.Dict[str, types.Any]
23FormatString = typing.Optional[str]
26def string_or_lambda(input_):
27 if isinstance(input_, str):
29 def render_input(progress, data, width):
30 return input_ % data
32 return render_input
33 else:
34 return input_
37def create_wrapper(wrapper):
38 '''Convert a wrapper tuple or format string to a format string
40 >>> create_wrapper('')
42 >>> print(create_wrapper('a{}b'))
43 a{}b
45 >>> print(create_wrapper(('a', 'b')))
46 a{}b
47 '''
48 if isinstance(wrapper, tuple) and len(wrapper) == 2:
49 a, b = wrapper
50 wrapper = (a or '') + '{}' + (b or '')
51 elif not wrapper:
52 return
54 if isinstance(wrapper, str):
55 assert '{}' in wrapper, 'Expected string with {} for formatting'
56 else:
57 raise RuntimeError(
58 'Pass either a begin/end string as a tuple or a'
59 ' template string with {}'
60 )
62 return wrapper
65def wrapper(function, wrapper_):
66 '''Wrap the output of a function in a template string or a tuple with
67 begin/end strings
69 '''
70 wrapper_ = create_wrapper(wrapper_)
71 if not wrapper_:
72 return function
74 @functools.wraps(function)
75 def wrap(*args, **kwargs):
76 return wrapper_.format(function(*args, **kwargs))
78 return wrap
81def create_marker(marker, wrap=None):
82 def _marker(progress, data, width):
83 if (
84 progress.max_value is not base.UnknownLength
85 and progress.max_value > 0
86 ):
87 length = int(progress.value / progress.max_value * width)
88 return marker * length
89 else:
90 return marker
92 if isinstance(marker, str):
93 marker = converters.to_unicode(marker)
94 assert (
95 utils.len_color(marker) == 1
96 ), 'Markers are required to be 1 char'
97 return wrapper(_marker, wrap)
98 else:
99 return wrapper(marker, wrap)
102class FormatWidgetMixin(abc.ABC):
103 '''Mixin to format widgets using a formatstring
105 Variables available:
106 - max_value: The maximum value (can be None with iterators)
107 - value: The current value
108 - total_seconds_elapsed: The seconds since the bar started
109 - seconds_elapsed: The seconds since the bar started modulo 60
110 - minutes_elapsed: The minutes since the bar started modulo 60
111 - hours_elapsed: The hours since the bar started modulo 24
112 - days_elapsed: The hours since the bar started
113 - time_elapsed: Shortcut for HH:MM:SS time since the bar started including
114 days
115 - percentage: Percentage as a float
116 '''
118 def __init__(self, format: str, new_style: bool = False, **kwargs):
119 self.new_style = new_style
120 self.format = format
122 def get_format(
123 self,
124 progress: ProgressBarMixinBase,
125 data: Data,
126 format: types.Optional[str] = None,
127 ) -> str:
128 return format or self.format
130 def __call__(
131 self,
132 progress: ProgressBarMixinBase,
133 data: Data,
134 format: types.Optional[str] = None,
135 ) -> str:
136 '''Formats the widget into a string'''
137 format = self.get_format(progress, data, format)
138 try:
139 if self.new_style:
140 return format.format(**data)
141 else:
142 return format % data
143 except (TypeError, KeyError):
144 print('Error while formatting %r' % format, file=sys.stderr)
145 pprint.pprint(data, stream=sys.stderr)
146 raise
149class WidthWidgetMixin(abc.ABC):
150 '''Mixing to make sure widgets are only visible if the screen is within a
151 specified size range so the progressbar fits on both large and small
152 screens.
154 Variables available:
155 - min_width: Only display the widget if at least `min_width` is left
156 - max_width: Only display the widget if at most `max_width` is left
158 >>> class Progress:
159 ... term_width = 0
161 >>> WidthWidgetMixin(5, 10).check_size(Progress)
162 False
163 >>> Progress.term_width = 5
164 >>> WidthWidgetMixin(5, 10).check_size(Progress)
165 True
166 >>> Progress.term_width = 10
167 >>> WidthWidgetMixin(5, 10).check_size(Progress)
168 True
169 >>> Progress.term_width = 11
170 >>> WidthWidgetMixin(5, 10).check_size(Progress)
171 False
172 '''
174 def __init__(self, min_width=None, max_width=None, **kwargs):
175 self.min_width = min_width
176 self.max_width = max_width
178 def check_size(self, progress: ProgressBarMixinBase):
179 if self.min_width and self.min_width > progress.term_width:
180 return False
181 elif self.max_width and self.max_width < progress.term_width:
182 return False
183 else:
184 return True
187class WidgetBase(WidthWidgetMixin, metaclass=abc.ABCMeta):
188 '''The base class for all widgets
190 The ProgressBar will call the widget's update value when the widget should
191 be updated. The widget's size may change between calls, but the widget may
192 display incorrectly if the size changes drastically and repeatedly.
194 The INTERVAL timedelta informs the ProgressBar that it should be
195 updated more often because it is time sensitive.
197 The widgets are only visible if the screen is within a
198 specified size range so the progressbar fits on both large and small
199 screens.
201 WARNING: Widgets can be shared between multiple progressbars so any state
202 information specific to a progressbar should be stored within the
203 progressbar instead of the widget.
205 Variables available:
206 - min_width: Only display the widget if at least `min_width` is left
207 - max_width: Only display the widget if at most `max_width` is left
208 - weight: Widgets with a higher `weigth` will be calculated before widgets
209 with a lower one
210 - copy: Copy this widget when initializing the progress bar so the
211 progressbar can be reused. Some widgets such as the FormatCustomText
212 require the shared state so this needs to be optional
214 '''
216 copy = True
218 @abc.abstractmethod
219 def __call__(self, progress: ProgressBarMixinBase, data: Data) -> str:
220 '''Updates the widget.
222 progress - a reference to the calling ProgressBar
223 '''
226class AutoWidthWidgetBase(WidgetBase, metaclass=abc.ABCMeta):
227 '''The base class for all variable width widgets.
229 This widget is much like the \\hfill command in TeX, it will expand to
230 fill the line. You can use more than one in the same line, and they will
231 all have the same width, and together will fill the line.
232 '''
234 @abc.abstractmethod
235 def __call__(
236 self,
237 progress: ProgressBarMixinBase,
238 data: Data,
239 width: int = 0,
240 ) -> str:
241 '''Updates the widget providing the total width the widget must fill.
243 progress - a reference to the calling ProgressBar
244 width - The total width the widget must fill
245 '''
248class TimeSensitiveWidgetBase(WidgetBase, metaclass=abc.ABCMeta):
249 '''The base class for all time sensitive widgets.
251 Some widgets like timers would become out of date unless updated at least
252 every `INTERVAL`
253 '''
255 INTERVAL = datetime.timedelta(milliseconds=100)
258class FormatLabel(FormatWidgetMixin, WidgetBase):
259 '''Displays a formatted label
261 >>> label = FormatLabel('%(value)s', min_width=5, max_width=10)
262 >>> class Progress:
263 ... pass
264 >>> label = FormatLabel('{value} :: {value:^6}', new_style=True)
265 >>> str(label(Progress, dict(value='test')))
266 'test :: test '
268 '''
270 mapping = {
271 'finished': ('end_time', None),
272 'last_update': ('last_update_time', None),
273 'max': ('max_value', None),
274 'seconds': ('seconds_elapsed', None),
275 'start': ('start_time', None),
276 'elapsed': ('total_seconds_elapsed', utils.format_time),
277 'value': ('value', None),
278 }
280 def __init__(self, format: str, **kwargs):
281 FormatWidgetMixin.__init__(self, format=format, **kwargs)
282 WidgetBase.__init__(self, **kwargs)
284 def __call__(
285 self,
286 progress: ProgressBarMixinBase,
287 data: Data,
288 format: types.Optional[str] = None,
289 ):
290 for name, (key, transform) in self.mapping.items():
291 try:
292 if transform is None:
293 data[name] = data[key]
294 else:
295 data[name] = transform(data[key])
296 except (KeyError, ValueError, IndexError): # pragma: no cover
297 pass
299 return FormatWidgetMixin.__call__(self, progress, data, format)
302class Timer(FormatLabel, TimeSensitiveWidgetBase):
303 '''WidgetBase which displays the elapsed seconds.'''
305 def __init__(self, format='Elapsed Time: %(elapsed)s', **kwargs):
306 if '%s' in format and '%(elapsed)s' not in format:
307 format = format.replace('%s', '%(elapsed)s')
309 FormatLabel.__init__(self, format=format, **kwargs)
310 TimeSensitiveWidgetBase.__init__(self, **kwargs)
312 # This is exposed as a static method for backwards compatibility
313 format_time = staticmethod(utils.format_time)
316class SamplesMixin(TimeSensitiveWidgetBase, metaclass=abc.ABCMeta):
317 '''
318 Mixing for widgets that average multiple measurements
320 Note that samples can be either an integer or a timedelta to indicate a
321 certain amount of time
323 >>> class progress:
324 ... last_update_time = datetime.datetime.now()
325 ... value = 1
326 ... extra = dict()
328 >>> samples = SamplesMixin(samples=2)
329 >>> samples(progress, None, True)
330 (None, None)
331 >>> progress.last_update_time += datetime.timedelta(seconds=1)
332 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
333 True
335 >>> progress.last_update_time += datetime.timedelta(seconds=1)
336 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
337 True
339 >>> samples = SamplesMixin(samples=datetime.timedelta(seconds=1))
340 >>> _, value = samples(progress, None)
341 >>> value
342 [1, 1]
344 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0)
345 True
346 '''
348 def __init__(
349 self,
350 samples=datetime.timedelta(seconds=2),
351 key_prefix=None,
352 **kwargs,
353 ):
354 self.samples = samples
355 self.key_prefix = (
356 key_prefix if key_prefix else self.__class__.__name__
357 ) + '_'
358 TimeSensitiveWidgetBase.__init__(self, **kwargs)
360 def get_sample_times(self, progress: ProgressBarMixinBase, data: Data):
361 return progress.extra.setdefault(self.key_prefix + 'sample_times', [])
363 def get_sample_values(self, progress: ProgressBarMixinBase, data: Data):
364 return progress.extra.setdefault(self.key_prefix + 'sample_values', [])
366 def __call__(
367 self, progress: ProgressBarMixinBase, data: Data, delta: bool = False
368 ):
369 sample_times = self.get_sample_times(progress, data)
370 sample_values = self.get_sample_values(progress, data)
372 if sample_times:
373 sample_time = sample_times[-1]
374 else:
375 sample_time = datetime.datetime.min
377 if progress.last_update_time - sample_time > self.INTERVAL:
378 # Add a sample but limit the size to `num_samples`
379 sample_times.append(progress.last_update_time)
380 sample_values.append(progress.value)
382 if isinstance(self.samples, datetime.timedelta):
383 minimum_time = progress.last_update_time - self.samples
384 minimum_value = sample_values[-1]
385 while (
386 sample_times[2:]
387 and minimum_time > sample_times[1]
388 and minimum_value > sample_values[1]
389 ):
390 sample_times.pop(0)
391 sample_values.pop(0)
392 else:
393 if len(sample_times) > self.samples:
394 sample_times.pop(0)
395 sample_values.pop(0)
397 if delta:
398 delta_time = sample_times[-1] - sample_times[0]
399 delta_value = sample_values[-1] - sample_values[0]
400 if delta_time:
401 return delta_time, delta_value
402 else:
403 return None, None
404 else:
405 return sample_times, sample_values
408class ETA(Timer):
409 '''WidgetBase which attempts to estimate the time of arrival.'''
411 def __init__(
412 self,
413 format_not_started='ETA: --:--:--',
414 format_finished='Time: %(elapsed)8s',
415 format='ETA: %(eta)8s',
416 format_zero='ETA: 00:00:00',
417 format_NA='ETA: N/A',
418 **kwargs,
419 ):
421 if '%s' in format and '%(eta)s' not in format:
422 format = format.replace('%s', '%(eta)s')
424 Timer.__init__(self, **kwargs)
425 self.format_not_started = format_not_started
426 self.format_finished = format_finished
427 self.format = format
428 self.format_zero = format_zero
429 self.format_NA = format_NA
431 def _calculate_eta(
432 self, progress: ProgressBarMixinBase, data: Data, value, elapsed
433 ):
434 '''Updates the widget to show the ETA or total time when finished.'''
435 if elapsed:
436 # The max() prevents zero division errors
437 per_item = elapsed.total_seconds() / max(value, 1e-6)
438 remaining = progress.max_value - data['value']
439 eta_seconds = remaining * per_item
440 else:
441 eta_seconds = 0
443 return eta_seconds
445 def __call__(
446 self,
447 progress: ProgressBarMixinBase,
448 data: Data,
449 value=None,
450 elapsed=None,
451 ):
452 '''Updates the widget to show the ETA or total time when finished.'''
453 if value is None:
454 value = data['value']
456 if elapsed is None:
457 elapsed = data['time_elapsed']
459 ETA_NA = False
460 try:
461 data['eta_seconds'] = self._calculate_eta(
462 progress, data, value=value, elapsed=elapsed
463 )
464 except TypeError:
465 data['eta_seconds'] = None
466 ETA_NA = True
468 data['eta'] = None
469 if data['eta_seconds']:
470 try:
471 data['eta'] = utils.format_time(data['eta_seconds'])
472 except (ValueError, OverflowError): # pragma: no cover
473 pass
475 if data['value'] == progress.min_value:
476 format = self.format_not_started
477 elif progress.end_time:
478 format = self.format_finished
479 elif data['eta']:
480 format = self.format
481 elif ETA_NA:
482 format = self.format_NA
483 else:
484 format = self.format_zero
486 return Timer.__call__(self, progress, data, format=format)
489class AbsoluteETA(ETA):
490 '''Widget which attempts to estimate the absolute time of arrival.'''
492 def _calculate_eta(
493 self, progress: ProgressBarMixinBase, data: Data, value, elapsed
494 ):
495 eta_seconds = ETA._calculate_eta(self, progress, data, value, elapsed)
496 now = datetime.datetime.now()
497 try:
498 return now + datetime.timedelta(seconds=eta_seconds)
499 except OverflowError: # pragma: no cover
500 return datetime.datetime.max
502 def __init__(
503 self,
504 format_not_started='Estimated finish time: ----/--/-- --:--:--',
505 format_finished='Finished at: %(elapsed)s',
506 format='Estimated finish time: %(eta)s',
507 **kwargs,
508 ):
509 ETA.__init__(
510 self,
511 format_not_started=format_not_started,
512 format_finished=format_finished,
513 format=format,
514 **kwargs,
515 )
518class AdaptiveETA(ETA, SamplesMixin):
519 '''WidgetBase which attempts to estimate the time of arrival.
521 Uses a sampled average of the speed based on the 10 last updates.
522 Very convenient for resuming the progress halfway.
523 '''
525 def __init__(self, **kwargs):
526 ETA.__init__(self, **kwargs)
527 SamplesMixin.__init__(self, **kwargs)
529 def __call__(
530 self,
531 progress: ProgressBarMixinBase,
532 data: Data,
533 value=None,
534 elapsed=None,
535 ):
536 elapsed, value = SamplesMixin.__call__(
537 self, progress, data, delta=True
538 )
539 if not elapsed:
540 value = None
541 elapsed = 0
543 return ETA.__call__(self, progress, data, value=value, elapsed=elapsed)
546class DataSize(FormatWidgetMixin, WidgetBase):
547 '''
548 Widget for showing an amount of data transferred/processed.
550 Automatically formats the value (assumed to be a count of bytes) with an
551 appropriate sized unit, based on the IEC binary prefixes (powers of 1024).
552 '''
554 def __init__(
555 self,
556 variable='value',
557 format='%(scaled)5.1f %(prefix)s%(unit)s',
558 unit='B',
559 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'),
560 **kwargs,
561 ):
562 self.variable = variable
563 self.unit = unit
564 self.prefixes = prefixes
565 FormatWidgetMixin.__init__(self, format=format, **kwargs)
566 WidgetBase.__init__(self, **kwargs)
568 def __call__(
569 self,
570 progress: ProgressBarMixinBase,
571 data: Data,
572 format: types.Optional[str] = None,
573 ):
574 value = data[self.variable]
575 if value is not None:
576 scaled, power = utils.scale_1024(value, len(self.prefixes))
577 else:
578 scaled = power = 0
580 data['scaled'] = scaled
581 data['prefix'] = self.prefixes[power]
582 data['unit'] = self.unit
584 return FormatWidgetMixin.__call__(self, progress, data, format)
587class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase):
588 '''
589 Widget for showing the current transfer speed (useful for file transfers).
590 '''
592 def __init__(
593 self,
594 format='%(scaled)5.1f %(prefix)s%(unit)-s/s',
595 inverse_format='%(scaled)5.1f s/%(prefix)s%(unit)-s',
596 unit='B',
597 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'),
598 **kwargs,
599 ):
600 self.unit = unit
601 self.prefixes = prefixes
602 self.inverse_format = inverse_format
603 FormatWidgetMixin.__init__(self, format=format, **kwargs)
604 TimeSensitiveWidgetBase.__init__(self, **kwargs)
606 def _speed(self, value, elapsed):
607 speed = float(value) / elapsed
608 return utils.scale_1024(speed, len(self.prefixes))
610 def __call__(
611 self,
612 progress: ProgressBarMixinBase,
613 data,
614 value=None,
615 total_seconds_elapsed=None,
616 ):
617 '''Updates the widget with the current SI prefixed speed.'''
618 if value is None:
619 value = data['value']
621 elapsed = utils.deltas_to_seconds(
622 total_seconds_elapsed, data['total_seconds_elapsed']
623 )
625 if (
626 value is not None
627 and elapsed is not None
628 and elapsed > 2e-6
629 and value > 2e-6
630 ): # =~ 0
631 scaled, power = self._speed(value, elapsed)
632 else:
633 scaled = power = 0
635 data['unit'] = self.unit
636 if power == 0 and scaled < 0.1:
637 if scaled > 0:
638 scaled = 1 / scaled
639 data['scaled'] = scaled
640 data['prefix'] = self.prefixes[0]
641 return FormatWidgetMixin.__call__(
642 self, progress, data, self.inverse_format
643 )
644 else:
645 data['scaled'] = scaled
646 data['prefix'] = self.prefixes[power]
647 return FormatWidgetMixin.__call__(self, progress, data)
650class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin):
651 '''Widget for showing the transfer speed based on the last X samples'''
653 def __init__(self, **kwargs):
654 FileTransferSpeed.__init__(self, **kwargs)
655 SamplesMixin.__init__(self, **kwargs)
657 def __call__(
658 self,
659 progress: ProgressBarMixinBase,
660 data,
661 value=None,
662 total_seconds_elapsed=None,
663 ):
664 elapsed, value = SamplesMixin.__call__(
665 self, progress, data, delta=True
666 )
667 return FileTransferSpeed.__call__(self, progress, data, value, elapsed)
670class AnimatedMarker(TimeSensitiveWidgetBase):
671 '''An animated marker for the progress bar which defaults to appear as if
672 it were rotating.
673 '''
675 def __init__(
676 self,
677 markers='|/-\\',
678 default=None,
679 fill='',
680 marker_wrap=None,
681 fill_wrap=None,
682 **kwargs,
683 ):
684 self.markers = markers
685 self.marker_wrap = create_wrapper(marker_wrap)
686 self.default = default or markers[0]
687 self.fill_wrap = create_wrapper(fill_wrap)
688 self.fill = create_marker(fill, self.fill_wrap) if fill else None
689 WidgetBase.__init__(self, **kwargs)
691 def __call__(self, progress: ProgressBarMixinBase, data: Data, width=None):
692 '''Updates the widget to show the next marker or the first marker when
693 finished'''
695 if progress.end_time:
696 return self.default
698 marker = self.markers[data['updates'] % len(self.markers)]
699 if self.marker_wrap:
700 marker = self.marker_wrap.format(marker)
702 if self.fill:
703 # Cut the last character so we can replace it with our marker
704 fill = self.fill(
705 progress,
706 data,
707 width - progress.custom_len(marker), # type: ignore
708 )
709 else:
710 fill = ''
712 # Python 3 returns an int when indexing bytes
713 if isinstance(marker, int): # pragma: no cover
714 marker = bytes(marker)
715 fill = fill.encode()
716 else:
717 # cast fill to the same type as marker
718 fill = type(marker)(fill)
720 return fill + marker # type: ignore
723# Alias for backwards compatibility
724RotatingMarker = AnimatedMarker
727class Counter(FormatWidgetMixin, WidgetBase):
728 '''Displays the current count'''
730 def __init__(self, format='%(value)d', **kwargs):
731 FormatWidgetMixin.__init__(self, format=format, **kwargs)
732 WidgetBase.__init__(self, format=format, **kwargs)
734 def __call__(
735 self, progress: ProgressBarMixinBase, data: Data, format=None
736 ):
737 return FormatWidgetMixin.__call__(self, progress, data, format)
740class Percentage(FormatWidgetMixin, WidgetBase):
741 '''Displays the current percentage as a number with a percent sign.'''
743 def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs):
744 self.na = na
745 FormatWidgetMixin.__init__(self, format=format, **kwargs)
746 WidgetBase.__init__(self, format=format, **kwargs)
748 def get_format(
749 self, progress: ProgressBarMixinBase, data: Data, format=None
750 ):
751 # If percentage is not available, display N/A%
752 percentage = data.get('percentage', base.Undefined)
753 if not percentage and percentage != 0:
754 return self.na
756 return FormatWidgetMixin.get_format(self, progress, data, format)
759class SimpleProgress(FormatWidgetMixin, WidgetBase):
760 '''Returns progress as a count of the total (e.g.: "5 of 47")'''
762 max_width_cache: dict[
763 types.Union[str, tuple[float, float | types.Type[base.UnknownLength]]],
764 types.Optional[int],
765 ]
767 DEFAULT_FORMAT = '%(value_s)s of %(max_value_s)s'
769 def __init__(self, format=DEFAULT_FORMAT, **kwargs):
770 FormatWidgetMixin.__init__(self, format=format, **kwargs)
771 WidgetBase.__init__(self, format=format, **kwargs)
772 self.max_width_cache = dict()
773 self.max_width_cache['default'] = self.max_width or 0
775 def __call__(
776 self, progress: ProgressBarMixinBase, data: Data, format=None
777 ):
778 # If max_value is not available, display N/A
779 if data.get('max_value'):
780 data['max_value_s'] = data.get('max_value')
781 else:
782 data['max_value_s'] = 'N/A'
784 # if value is not available it's the zeroth iteration
785 if data.get('value'):
786 data['value_s'] = data['value']
787 else:
788 data['value_s'] = 0
790 formatted = FormatWidgetMixin.__call__(
791 self, progress, data, format=format
792 )
794 # Guess the maximum width from the min and max value
795 key = progress.min_value, progress.max_value
796 max_width: types.Optional[int] = self.max_width_cache.get(
797 key, self.max_width
798 )
799 if not max_width:
800 temporary_data = data.copy()
801 for value in key:
802 if value is None: # pragma: no cover
803 continue
805 temporary_data['value'] = value
806 width = progress.custom_len(
807 FormatWidgetMixin.__call__(
808 self, progress, temporary_data, format=format
809 )
810 )
811 if width: # pragma: no branch
812 max_width = max(max_width or 0, width)
814 self.max_width_cache[key] = max_width
816 # Adjust the output to have a consistent size in all cases
817 if max_width: # pragma: no branch
818 formatted = formatted.rjust(max_width)
820 return formatted
823class Bar(AutoWidthWidgetBase):
824 '''A progress bar which stretches to fill the line.'''
826 def __init__(
827 self,
828 marker='#',
829 left='|',
830 right='|',
831 fill=' ',
832 fill_left=True,
833 marker_wrap=None,
834 **kwargs,
835 ):
836 '''Creates a customizable progress bar.
838 The callable takes the same parameters as the `__call__` method
840 marker - string or callable object to use as a marker
841 left - string or callable object to use as a left border
842 right - string or callable object to use as a right border
843 fill - character to use for the empty part of the progress bar
844 fill_left - whether to fill from the left or the right
845 '''
847 self.marker = create_marker(marker, marker_wrap)
848 self.left = string_or_lambda(left)
849 self.right = string_or_lambda(right)
850 self.fill = string_or_lambda(fill)
851 self.fill_left = fill_left
853 AutoWidthWidgetBase.__init__(self, **kwargs)
855 def __call__(
856 self,
857 progress: ProgressBarMixinBase,
858 data: Data,
859 width: int = 0,
860 ):
861 '''Updates the progress bar and its subcomponents'''
863 left = converters.to_unicode(self.left(progress, data, width))
864 right = converters.to_unicode(self.right(progress, data, width))
865 width -= progress.custom_len(left) + progress.custom_len(right)
866 marker = converters.to_unicode(self.marker(progress, data, width))
867 fill = converters.to_unicode(self.fill(progress, data, width))
869 # Make sure we ignore invisible characters when filling
870 width += len(marker) - progress.custom_len(marker)
872 if self.fill_left:
873 marker = marker.ljust(width, fill)
874 else:
875 marker = marker.rjust(width, fill)
877 return left + marker + right
880class ReverseBar(Bar):
881 '''A bar which has a marker that goes from right to left'''
883 def __init__(
884 self,
885 marker='#',
886 left='|',
887 right='|',
888 fill=' ',
889 fill_left=False,
890 **kwargs,
891 ):
892 '''Creates a customizable progress bar.
894 marker - string or updatable object to use as a marker
895 left - string or updatable object to use as a left border
896 right - string or updatable object to use as a right border
897 fill - character to use for the empty part of the progress bar
898 fill_left - whether to fill from the left or the right
899 '''
900 Bar.__init__(
901 self,
902 marker=marker,
903 left=left,
904 right=right,
905 fill=fill,
906 fill_left=fill_left,
907 **kwargs,
908 )
911class BouncingBar(Bar, TimeSensitiveWidgetBase):
912 '''A bar which has a marker which bounces from side to side.'''
914 INTERVAL = datetime.timedelta(milliseconds=100)
916 def __call__(
917 self,
918 progress: ProgressBarMixinBase,
919 data: Data,
920 width: int = 0,
921 ):
922 '''Updates the progress bar and its subcomponents'''
924 left = converters.to_unicode(self.left(progress, data, width))
925 right = converters.to_unicode(self.right(progress, data, width))
926 width -= progress.custom_len(left) + progress.custom_len(right)
927 marker = converters.to_unicode(self.marker(progress, data, width))
929 fill = converters.to_unicode(self.fill(progress, data, width))
931 if width: # pragma: no branch
932 value = int(
933 data['total_seconds_elapsed'] / self.INTERVAL.total_seconds()
934 )
936 a = value % width
937 b = width - a - 1
938 if value % (width * 2) >= width:
939 a, b = b, a
941 if self.fill_left:
942 marker = a * fill + marker + b * fill
943 else:
944 marker = b * fill + marker + a * fill
946 return left + marker + right
949class FormatCustomText(FormatWidgetMixin, WidgetBase):
950 mapping: types.Dict[str, types.Any] = {}
951 copy = False
953 def __init__(
954 self,
955 format: str,
956 mapping: types.Optional[types.Dict[str, types.Any]] = None,
957 **kwargs,
958 ):
959 self.format = format
960 self.mapping = mapping or self.mapping
961 FormatWidgetMixin.__init__(self, format=format, **kwargs)
962 WidgetBase.__init__(self, **kwargs)
964 def update_mapping(self, **mapping: types.Dict[str, types.Any]):
965 self.mapping.update(mapping)
967 def __call__(
968 self,
969 progress: ProgressBarMixinBase,
970 data: Data,
971 format: types.Optional[str] = None,
972 ):
973 return FormatWidgetMixin.__call__(
974 self, progress, self.mapping, format or self.format
975 )
978class VariableMixin:
979 '''Mixin to display a custom user variable'''
981 def __init__(self, name, **kwargs):
982 if not isinstance(name, str):
983 raise TypeError('Variable(): argument must be a string')
984 if len(name.split()) > 1:
985 raise ValueError('Variable(): argument must be single word')
986 self.name = name
989class MultiRangeBar(Bar, VariableMixin):
990 '''
991 A bar with multiple sub-ranges, each represented by a different symbol
993 The various ranges are represented on a user-defined variable, formatted as
995 .. code-block:: python
997 [
998 ['Symbol1', amount1],
999 ['Symbol2', amount2],
1000 ...
1001 ]
1002 '''
1004 def __init__(self, name, markers, **kwargs):
1005 VariableMixin.__init__(self, name)
1006 Bar.__init__(self, **kwargs)
1007 self.markers = [string_or_lambda(marker) for marker in markers]
1009 def get_values(self, progress: ProgressBarMixinBase, data: Data):
1010 return data['variables'][self.name] or []
1012 def __call__(
1013 self,
1014 progress: ProgressBarMixinBase,
1015 data: Data,
1016 width: int = 0,
1017 ):
1018 '''Updates the progress bar and its subcomponents'''
1020 left = converters.to_unicode(self.left(progress, data, width))
1021 right = converters.to_unicode(self.right(progress, data, width))
1022 width -= progress.custom_len(left) + progress.custom_len(right)
1023 values = self.get_values(progress, data)
1025 values_sum = sum(values)
1026 if width and values_sum:
1027 middle = ''
1028 values_accumulated = 0
1029 width_accumulated = 0
1030 for marker, value in zip(self.markers, values):
1031 marker = converters.to_unicode(marker(progress, data, width))
1032 assert progress.custom_len(marker) == 1
1034 values_accumulated += value
1035 item_width = int(values_accumulated / values_sum * width)
1036 item_width -= width_accumulated
1037 width_accumulated += item_width
1038 middle += item_width * marker
1039 else:
1040 fill = converters.to_unicode(self.fill(progress, data, width))
1041 assert progress.custom_len(fill) == 1
1042 middle = fill * width
1044 return left + middle + right
1047class MultiProgressBar(MultiRangeBar):
1048 def __init__(
1049 self,
1050 name,
1051 # NOTE: the markers are not whitespace even though some
1052 # terminals don't show the characters correctly!
1053 markers=' ▁▂▃▄▅▆▇█',
1054 **kwargs,
1055 ):
1056 MultiRangeBar.__init__(
1057 self,
1058 name=name,
1059 markers=list(reversed(markers)),
1060 **kwargs,
1061 )
1063 def get_values(self, progress: ProgressBarMixinBase, data: Data):
1064 ranges = [0.0] * len(self.markers)
1065 for value in data['variables'][self.name] or []:
1066 if not isinstance(value, (int, float)):
1067 # Progress is (value, max)
1068 progress_value, progress_max = value
1069 value = float(progress_value) / float(progress_max)
1071 if not 0 <= value <= 1:
1072 raise ValueError(
1073 'Range value needs to be in the range [0..1], got %s'
1074 % value
1075 )
1077 range_ = value * (len(ranges) - 1)
1078 pos = int(range_)
1079 frac = range_ % 1
1080 ranges[pos] += 1 - frac
1081 if frac:
1082 ranges[pos + 1] += frac
1084 if self.fill_left:
1085 ranges = list(reversed(ranges))
1087 return ranges
1090class GranularMarkers:
1091 smooth = ' ▏▎▍▌▋▊▉█'
1092 bar = ' ▁▂▃▄▅▆▇█'
1093 snake = ' ▖▌▛█'
1094 fade_in = ' ░▒▓█'
1095 dots = ' ⡀⡄⡆⡇⣇⣧⣷⣿'
1096 growing_circles = ' .oO'
1099class GranularBar(AutoWidthWidgetBase):
1100 '''A progressbar that can display progress at a sub-character granularity
1101 by using multiple marker characters.
1103 Examples of markers:
1104 - Smooth: ` ▏▎▍▌▋▊▉█` (default)
1105 - Bar: ` ▁▂▃▄▅▆▇█`
1106 - Snake: ` ▖▌▛█`
1107 - Fade in: ` ░▒▓█`
1108 - Dots: ` ⡀⡄⡆⡇⣇⣧⣷⣿`
1109 - Growing circles: ` .oO`
1111 The markers can be accessed through GranularMarkers. GranularMarkers.dots
1112 for example
1113 '''
1115 def __init__(
1116 self,
1117 markers=GranularMarkers.smooth,
1118 left='|',
1119 right='|',
1120 **kwargs,
1121 ):
1122 '''Creates a customizable progress bar.
1124 markers - string of characters to use as granular progress markers. The
1125 first character should represent 0% and the last 100%.
1126 Ex: ` .oO`.
1127 left - string or callable object to use as a left border
1128 right - string or callable object to use as a right border
1129 '''
1130 self.markers = markers
1131 self.left = string_or_lambda(left)
1132 self.right = string_or_lambda(right)
1134 AutoWidthWidgetBase.__init__(self, **kwargs)
1136 def __call__(
1137 self,
1138 progress: ProgressBarMixinBase,
1139 data: Data,
1140 width: int = 0,
1141 ):
1142 left = converters.to_unicode(self.left(progress, data, width))
1143 right = converters.to_unicode(self.right(progress, data, width))
1144 width -= progress.custom_len(left) + progress.custom_len(right)
1146 max_value = progress.max_value
1147 # mypy doesn't get that the first part of the if statement makes sure
1148 # we get the correct type
1149 if (
1150 max_value is not base.UnknownLength
1151 and max_value > 0 # type: ignore
1152 ):
1153 percent = progress.value / max_value # type: ignore
1154 else:
1155 percent = 0
1157 num_chars = percent * width
1159 marker = self.markers[-1] * int(num_chars)
1161 marker_idx = int((num_chars % 1) * (len(self.markers) - 1))
1162 if marker_idx:
1163 marker += self.markers[marker_idx]
1165 marker = converters.to_unicode(marker)
1167 # Make sure we ignore invisible characters when filling
1168 width += len(marker) - progress.custom_len(marker)
1169 marker = marker.ljust(width, self.markers[0])
1171 return left + marker + right
1174class FormatLabelBar(FormatLabel, Bar):
1175 '''A bar which has a formatted label in the center.'''
1177 def __init__(self, format, **kwargs):
1178 FormatLabel.__init__(self, format, **kwargs)
1179 Bar.__init__(self, **kwargs)
1181 def __call__( # type: ignore
1182 self,
1183 progress: ProgressBarMixinBase,
1184 data: Data,
1185 width: int = 0,
1186 format: FormatString = None,
1187 ):
1188 center = FormatLabel.__call__(self, progress, data, format=format)
1189 bar = Bar.__call__(self, progress, data, width)
1191 # Aligns the center of the label to the center of the bar
1192 center_len = progress.custom_len(center)
1193 center_left = int((width - center_len) / 2)
1194 center_right = center_left + center_len
1195 return bar[:center_left] + center + bar[center_right:]
1198class PercentageLabelBar(Percentage, FormatLabelBar):
1199 '''A bar which displays the current percentage in the center.'''
1201 # %3d adds an extra space that makes it look off-center
1202 # %2d keeps the label somewhat consistently in-place
1203 def __init__(self, format='%(percentage)2d%%', na='N/A%%', **kwargs):
1204 Percentage.__init__(self, format, na=na, **kwargs)
1205 FormatLabelBar.__init__(self, format, **kwargs)
1207 def __call__( # type: ignore
1208 self,
1209 progress: ProgressBarMixinBase,
1210 data: Data,
1211 width: int = 0,
1212 format: FormatString = None,
1213 ):
1214 return super().__call__(progress, data, width, format=format)
1217class Variable(FormatWidgetMixin, VariableMixin, WidgetBase):
1218 '''Displays a custom variable.'''
1220 def __init__(
1221 self,
1222 name,
1223 format='{name}: {formatted_value}',
1224 width=6,
1225 precision=3,
1226 **kwargs,
1227 ):
1228 '''Creates a Variable associated with the given name.'''
1229 self.format = format
1230 self.width = width
1231 self.precision = precision
1232 VariableMixin.__init__(self, name=name)
1233 WidgetBase.__init__(self, **kwargs)
1235 def __call__(
1236 self,
1237 progress: ProgressBarMixinBase,
1238 data: Data,
1239 format: types.Optional[str] = None,
1240 ):
1241 value = data['variables'][self.name]
1242 context = data.copy()
1243 context['value'] = value
1244 context['name'] = self.name
1245 context['width'] = self.width
1246 context['precision'] = self.precision
1248 try:
1249 # Make sure to try and cast the value first, otherwise the
1250 # formatting will generate warnings/errors on newer Python releases
1251 value = float(value)
1252 fmt = '{value:{width}.{precision}}'
1253 context['formatted_value'] = fmt.format(**context)
1254 except (TypeError, ValueError):
1255 if value:
1256 context['formatted_value'] = '{value:{width}}'.format(
1257 **context
1258 )
1259 else:
1260 context['formatted_value'] = '-' * self.width
1262 return self.format.format(**context)
1265class DynamicMessage(Variable):
1266 '''Kept for backwards compatibility, please use `Variable` instead.'''
1268 pass
1271class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase):
1272 '''Widget which displays the current (date)time with seconds resolution.'''
1274 INTERVAL = datetime.timedelta(seconds=1)
1276 def __init__(
1277 self,
1278 format='Current Time: %(current_time)s',
1279 microseconds=False,
1280 **kwargs,
1281 ):
1282 self.microseconds = microseconds
1283 FormatWidgetMixin.__init__(self, format=format, **kwargs)
1284 TimeSensitiveWidgetBase.__init__(self, **kwargs)
1286 def __call__(
1287 self,
1288 progress: ProgressBarMixinBase,
1289 data: Data,
1290 format: types.Optional[str] = None,
1291 ):
1292 data['current_time'] = self.current_time()
1293 data['current_datetime'] = self.current_datetime()
1295 return FormatWidgetMixin.__call__(self, progress, data, format=format)
1297 def current_datetime(self):
1298 now = datetime.datetime.now()
1299 if not self.microseconds:
1300 now = now.replace(microsecond=0)
1302 return now
1304 def current_time(self):
1305 return self.current_datetime().time()