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 7 8import abc 9import sys 10import pprint 11import datetime 12import functools 13 14from python_utils import converters 15 16import six 17 18from . import base 19from . import utils 20 21MAX_DATE = datetime.date.max 22MAX_TIME = datetime.time.max 23MAX_DATETIME = datetime.datetime.max 24 25 26def string_or_lambda(input_): 27 if isinstance(input_, six.string_types): 28 def render_input(progress, data, width): 29 return input_ % data 30 31 return render_input 32 else: 33 return input_ 34 35 36def create_wrapper(wrapper): 37 '''Convert a wrapper tuple or format string to a format string 38 39 >>> create_wrapper('') 40 41 >>> print(create_wrapper('a{}b')) 42 a{}b 43 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 52 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 {}') 58 59 return wrapper 60 61 62def wrapper(function, wrapper): 63 '''Wrap the output of a function in a template string or a tuple with 64 begin/end strings 65 66 ''' 67 wrapper = create_wrapper(wrapper) 68 if not wrapper: 69 return function 70 71 @functools.wraps(function) 72 def wrap(*args, **kwargs): 73 return wrapper.format(function(*args, **kwargs)) 74 75 return wrap 76 77 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 86 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) 94 95 96class FormatWidgetMixin(object): 97 '''Mixin to format widgets using a formatstring 98 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 = [] 112 113 def __init__(self, format, new_style=False, **kwargs): 114 self.new_style = new_style 115 self.format = format 116 117 def get_format(self, progress, data, format=None): 118 return format or self.format 119 120 def __call__(self, progress, data, format=None): 121 '''Formats the widget into a string''' 122 format = self.get_format(progress, data, format) 123 try: 124 if self.new_style: 125 return format.format(**data) 126 else: 127 return format % data 128 except (TypeError, KeyError): 129 print('Error while formatting %r' % format, file=sys.stderr) 130 pprint.pprint(data, stream=sys.stderr) 131 raise 132 133 134class WidthWidgetMixin(object): 135 '''Mixing to make sure widgets are only visible if the screen is within a 136 specified size range so the progressbar fits on both large and small 137 screens.. 138 139 Variables available: 140 - min_width: Only display the widget if at least `min_width` is left 141 - max_width: Only display the widget if at most `max_width` is left 142 143 >>> class Progress(object): 144 ... term_width = 0 145 146 >>> WidthWidgetMixin(5, 10).check_size(Progress) 147 False 148 >>> Progress.term_width = 5 149 >>> WidthWidgetMixin(5, 10).check_size(Progress) 150 True 151 >>> Progress.term_width = 10 152 >>> WidthWidgetMixin(5, 10).check_size(Progress) 153 True 154 >>> Progress.term_width = 11 155 >>> WidthWidgetMixin(5, 10).check_size(Progress) 156 False 157 ''' 158 159 def __init__(self, min_width=None, max_width=None, **kwargs): 160 self.min_width = min_width 161 self.max_width = max_width 162 163 def check_size(self, progress): 164 if self.min_width and self.min_width > progress.term_width: 165 return False 166 elif self.max_width and self.max_width < progress.term_width: 167 return False 168 else: 169 return True 170 171 172class WidgetBase(WidthWidgetMixin): 173 __metaclass__ = abc.ABCMeta 174 '''The base class for all widgets 175 176 The ProgressBar will call the widget's update value when the widget should 177 be updated. The widget's size may change between calls, but the widget may 178 display incorrectly if the size changes drastically and repeatedly. 179 180 The boolean INTERVAL informs the ProgressBar that it should be 181 updated more often because it is time sensitive. 182 183 The widgets are only visible if the screen is within a 184 specified size range so the progressbar fits on both large and small 185 screens. 186 187 WARNING: Widgets can be shared between multiple progressbars so any state 188 information specific to a progressbar should be stored within the 189 progressbar instead of the widget. 190 191 Variables available: 192 - min_width: Only display the widget if at least `min_width` is left 193 - max_width: Only display the widget if at most `max_width` is left 194 - weight: Widgets with a higher `weigth` will be calculated before widgets 195 with a lower one 196 - copy: Copy this widget when initializing the progress bar so the 197 progressbar can be reused. Some widgets such as the FormatCustomText 198 require the shared state so this needs to be optional 199 ''' 200 copy = True 201 202 @abc.abstractmethod 203 def __call__(self, progress, data): 204 '''Updates the widget. 205 206 progress - a reference to the calling ProgressBar 207 ''' 208 209 210class AutoWidthWidgetBase(WidgetBase): 211 '''The base class for all variable width widgets. 212 213 This widget is much like the \\hfill command in TeX, it will expand to 214 fill the line. You can use more than one in the same line, and they will 215 all have the same width, and together will fill the line. 216 ''' 217 218 @abc.abstractmethod 219 def __call__(self, progress, data, width): 220 '''Updates the widget providing the total width the widget must fill. 221 222 progress - a reference to the calling ProgressBar 223 width - The total width the widget must fill 224 ''' 225 226 227class TimeSensitiveWidgetBase(WidgetBase): 228 '''The base class for all time sensitive widgets. 229 230 Some widgets like timers would become out of date unless updated at least 231 every `INTERVAL` 232 ''' 233 INTERVAL = datetime.timedelta(milliseconds=100) 234 235 236class FormatLabel(FormatWidgetMixin, WidgetBase): 237 '''Displays a formatted label 238 239 >>> label = FormatLabel('%(value)s', min_width=5, max_width=10) 240 >>> class Progress(object): 241 ... pass 242 >>> label = FormatLabel('{value} :: {value:^6}', new_style=True) 243 >>> str(label(Progress, dict(value='test'))) 244 'test :: test ' 245 246 ''' 247 248 mapping = { 249 'finished': ('end_time', None), 250 'last_update': ('last_update_time', None), 251 'max': ('max_value', None), 252 'seconds': ('seconds_elapsed', None), 253 'start': ('start_time', None), 254 'elapsed': ('total_seconds_elapsed', utils.format_time), 255 'value': ('value', None), 256 } 257 258 def __init__(self, format, **kwargs): 259 FormatWidgetMixin.__init__(self, format=format, **kwargs) 260 WidgetBase.__init__(self, **kwargs) 261 262 def __call__(self, progress, data, **kwargs): 263 for name, (key, transform) in self.mapping.items(): 264 try: 265 if transform is None: 266 data[name] = data[key] 267 else: 268 data[name] = transform(data[key]) 269 except (KeyError, ValueError, IndexError): # pragma: no cover 270 pass 271 272 return FormatWidgetMixin.__call__(self, progress, data, **kwargs) 273 274 275class Timer(FormatLabel, TimeSensitiveWidgetBase): 276 '''WidgetBase which displays the elapsed seconds.''' 277 278 def __init__(self, format='Elapsed Time: %(elapsed)s', **kwargs): 279 FormatLabel.__init__(self, format=format, **kwargs) 280 TimeSensitiveWidgetBase.__init__(self, **kwargs) 281 282 # This is exposed as a static method for backwards compatibility 283 format_time = staticmethod(utils.format_time) 284 285 286class SamplesMixin(TimeSensitiveWidgetBase): 287 ''' 288 Mixing for widgets that average multiple measurements 289 290 Note that samples can be either an integer or a timedelta to indicate a 291 certain amount of time 292 293 >>> class progress: 294 ... last_update_time = datetime.datetime.now() 295 ... value = 1 296 ... extra = dict() 297 298 >>> samples = SamplesMixin(samples=2) 299 >>> samples(progress, None, True) 300 (None, None) 301 >>> progress.last_update_time += datetime.timedelta(seconds=1) 302 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0) 303 True 304 305 >>> progress.last_update_time += datetime.timedelta(seconds=1) 306 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0) 307 True 308 309 >>> samples = SamplesMixin(samples=datetime.timedelta(seconds=1)) 310 >>> _, value = samples(progress, None) 311 >>> value 312 [1, 1] 313 314 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0) 315 True 316 ''' 317 318 def __init__(self, samples=datetime.timedelta(seconds=2), key_prefix=None, 319 **kwargs): 320 self.samples = samples 321 self.key_prefix = (self.__class__.__name__ or key_prefix) + '_' 322 TimeSensitiveWidgetBase.__init__(self, **kwargs) 323 324 def get_sample_times(self, progress, data): 325 return progress.extra.setdefault(self.key_prefix + 'sample_times', []) 326 327 def get_sample_values(self, progress, data): 328 return progress.extra.setdefault(self.key_prefix + 'sample_values', []) 329 330 def __call__(self, progress, data, delta=False): 331 sample_times = self.get_sample_times(progress, data) 332 sample_values = self.get_sample_values(progress, data) 333 334 if sample_times: 335 sample_time = sample_times[-1] 336 else: 337 sample_time = datetime.datetime.min 338 339 if progress.last_update_time - sample_time > self.INTERVAL: 340 # Add a sample but limit the size to `num_samples` 341 sample_times.append(progress.last_update_time) 342 sample_values.append(progress.value) 343 344 if isinstance(self.samples, datetime.timedelta): 345 minimum_time = progress.last_update_time - self.samples 346 minimum_value = sample_values[-1] 347 while (sample_times[2:] and 348 minimum_time > sample_times[1] and 349 minimum_value > sample_values[1]): 350 sample_times.pop(0) 351 sample_values.pop(0) 352 else: 353 if len(sample_times) > self.samples: 354 sample_times.pop(0) 355 sample_values.pop(0) 356 357 if delta: 358 delta_time = sample_times[-1] - sample_times[0] 359 delta_value = sample_values[-1] - sample_values[0] 360 if delta_time: 361 return delta_time, delta_value 362 else: 363 return None, None 364 else: 365 return sample_times, sample_values 366 367 368class ETA(Timer): 369 '''WidgetBase which attempts to estimate the time of arrival.''' 370 371 def __init__( 372 self, 373 format_not_started='ETA: --:--:--', 374 format_finished='Time: %(elapsed)8s', 375 format='ETA: %(eta)8s', 376 format_zero='ETA: 00:00:00', 377 format_NA='ETA: N/A', 378 **kwargs): 379 380 Timer.__init__(self, **kwargs) 381 self.format_not_started = format_not_started 382 self.format_finished = format_finished 383 self.format = format 384 self.format_zero = format_zero 385 self.format_NA = format_NA 386 387 def _calculate_eta(self, progress, data, value, elapsed): 388 '''Updates the widget to show the ETA or total time when finished.''' 389 if elapsed: 390 # The max() prevents zero division errors 391 per_item = elapsed.total_seconds() / max(value, 1e-6) 392 remaining = progress.max_value - data['value'] 393 eta_seconds = remaining * per_item 394 else: 395 eta_seconds = 0 396 397 return eta_seconds 398 399 def __call__(self, progress, data, value=None, elapsed=None): 400 '''Updates the widget to show the ETA or total time when finished.''' 401 if value is None: 402 value = data['value'] 403 404 if elapsed is None: 405 elapsed = data['time_elapsed'] 406 407 ETA_NA = False 408 try: 409 data['eta_seconds'] = self._calculate_eta( 410 progress, data, value=value, elapsed=elapsed) 411 except TypeError: 412 data['eta_seconds'] = None 413 ETA_NA = True 414 415 data['eta'] = None 416 if data['eta_seconds']: 417 try: 418 data['eta'] = utils.format_time(data['eta_seconds']) 419 except (ValueError, OverflowError): # pragma: no cover 420 pass 421 422 if data['value'] == progress.min_value: 423 format = self.format_not_started 424 elif progress.end_time: 425 format = self.format_finished 426 elif data['eta']: 427 format = self.format 428 elif ETA_NA: 429 format = self.format_NA 430 else: 431 format = self.format_zero 432 433 return Timer.__call__(self, progress, data, format=format) 434 435 436class AbsoluteETA(ETA): 437 '''Widget which attempts to estimate the absolute time of arrival.''' 438 439 def _calculate_eta(self, progress, data, value, elapsed): 440 eta_seconds = ETA._calculate_eta(self, progress, data, value, elapsed) 441 now = datetime.datetime.now() 442 try: 443 return now + datetime.timedelta(seconds=eta_seconds) 444 except OverflowError: # pragma: no cover 445 return datetime.datetime.max 446 447 def __init__( 448 self, 449 format_not_started='Estimated finish time: ----/--/-- --:--:--', 450 format_finished='Finished at: %(elapsed)s', 451 format='Estimated finish time: %(eta)s', 452 **kwargs): 453 ETA.__init__(self, format_not_started=format_not_started, 454 format_finished=format_finished, format=format, **kwargs) 455 456 457class AdaptiveETA(ETA, SamplesMixin): 458 '''WidgetBase which attempts to estimate the time of arrival. 459 460 Uses a sampled average of the speed based on the 10 last updates. 461 Very convenient for resuming the progress halfway. 462 ''' 463 464 def __init__(self, **kwargs): 465 ETA.__init__(self, **kwargs) 466 SamplesMixin.__init__(self, **kwargs) 467 468 def __call__(self, progress, data): 469 elapsed, value = SamplesMixin.__call__(self, progress, data, 470 delta=True) 471 if not elapsed: 472 value = None 473 elapsed = 0 474 475 return ETA.__call__(self, progress, data, value=value, elapsed=elapsed) 476 477 478class DataSize(FormatWidgetMixin, WidgetBase): 479 ''' 480 Widget for showing an amount of data transferred/processed. 481 482 Automatically formats the value (assumed to be a count of bytes) with an 483 appropriate sized unit, based on the IEC binary prefixes (powers of 1024). 484 ''' 485 486 def __init__( 487 self, variable='value', 488 format='%(scaled)5.1f %(prefix)s%(unit)s', unit='B', 489 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), 490 **kwargs): 491 self.variable = variable 492 self.unit = unit 493 self.prefixes = prefixes 494 FormatWidgetMixin.__init__(self, format=format, **kwargs) 495 WidgetBase.__init__(self, **kwargs) 496 497 def __call__(self, progress, data): 498 value = data[self.variable] 499 if value is not None: 500 scaled, power = utils.scale_1024(value, len(self.prefixes)) 501 else: 502 scaled = power = 0 503 504 data['scaled'] = scaled 505 data['prefix'] = self.prefixes[power] 506 data['unit'] = self.unit 507 508 return FormatWidgetMixin.__call__(self, progress, data) 509 510 511class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase): 512 ''' 513 WidgetBase for showing the transfer speed (useful for file transfers). 514 ''' 515 516 def __init__( 517 self, format='%(scaled)5.1f %(prefix)s%(unit)-s/s', 518 inverse_format='%(scaled)5.1f s/%(prefix)s%(unit)-s', unit='B', 519 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), 520 **kwargs): 521 self.unit = unit 522 self.prefixes = prefixes 523 self.inverse_format = inverse_format 524 FormatWidgetMixin.__init__(self, format=format, **kwargs) 525 TimeSensitiveWidgetBase.__init__(self, **kwargs) 526 527 def _speed(self, value, elapsed): 528 speed = float(value) / elapsed 529 return utils.scale_1024(speed, len(self.prefixes)) 530 531 def __call__(self, progress, data, value=None, total_seconds_elapsed=None): 532 '''Updates the widget with the current SI prefixed speed.''' 533 if value is None: 534 value = data['value'] 535 536 elapsed = utils.deltas_to_seconds( 537 total_seconds_elapsed, 538 data['total_seconds_elapsed']) 539 540 if value is not None and elapsed is not None \ 541 and elapsed > 2e-6 and value > 2e-6: # =~ 0 542 scaled, power = self._speed(value, elapsed) 543 else: 544 scaled = power = 0 545 546 data['unit'] = self.unit 547 if power == 0 and scaled < 0.1: 548 if scaled > 0: 549 scaled = 1 / scaled 550 data['scaled'] = scaled 551 data['prefix'] = self.prefixes[0] 552 return FormatWidgetMixin.__call__(self, progress, data, 553 self.inverse_format) 554 else: 555 data['scaled'] = scaled 556 data['prefix'] = self.prefixes[power] 557 return FormatWidgetMixin.__call__(self, progress, data) 558 559 560class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin): 561 '''WidgetBase for showing the transfer speed, based on the last X samples 562 ''' 563 564 def __init__(self, **kwargs): 565 FileTransferSpeed.__init__(self, **kwargs) 566 SamplesMixin.__init__(self, **kwargs) 567 568 def __call__(self, progress, data): 569 elapsed, value = SamplesMixin.__call__(self, progress, data, 570 delta=True) 571 return FileTransferSpeed.__call__(self, progress, data, value, elapsed) 572 573 574class AnimatedMarker(TimeSensitiveWidgetBase): 575 '''An animated marker for the progress bar which defaults to appear as if 576 it were rotating. 577 ''' 578 579 def __init__(self, markers='|/-\\', default=None, fill='', 580 marker_wrap=None, fill_wrap=None, **kwargs): 581 self.markers = markers 582 self.marker_wrap = create_wrapper(marker_wrap) 583 self.default = default or markers[0] 584 self.fill_wrap = create_wrapper(fill_wrap) 585 self.fill = create_marker(fill, self.fill_wrap) if fill else None 586 WidgetBase.__init__(self, **kwargs) 587 588 def __call__(self, progress, data, width=None): 589 '''Updates the widget to show the next marker or the first marker when 590 finished''' 591 592 if progress.end_time: 593 return self.default 594 595 marker = self.markers[data['updates'] % len(self.markers)] 596 if self.marker_wrap: 597 marker = self.marker_wrap.format(marker) 598 599 if self.fill: 600 # Cut the last character so we can replace it with our marker 601 fill = self.fill(progress, data, width - progress.custom_len( 602 marker)) 603 else: 604 fill = '' 605 606 # Python 3 returns an int when indexing bytes 607 if isinstance(marker, int): # pragma: no cover 608 marker = bytes(marker) 609 fill = fill.encode() 610 else: 611 # cast fill to the same type as marker 612 fill = type(marker)(fill) 613 614 return fill + marker 615 616 617# Alias for backwards compatibility 618RotatingMarker = AnimatedMarker 619 620 621class Counter(FormatWidgetMixin, WidgetBase): 622 '''Displays the current count''' 623 624 def __init__(self, format='%(value)d', **kwargs): 625 FormatWidgetMixin.__init__(self, format=format, **kwargs) 626 WidgetBase.__init__(self, format=format, **kwargs) 627 628 def __call__(self, progress, data, format=None): 629 return FormatWidgetMixin.__call__(self, progress, data, format) 630 631 632class Percentage(FormatWidgetMixin, WidgetBase): 633 '''Displays the current percentage as a number with a percent sign.''' 634 635 def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs): 636 self.na = na 637 FormatWidgetMixin.__init__(self, format=format, **kwargs) 638 WidgetBase.__init__(self, format=format, **kwargs) 639 640 def get_format(self, progress, data, format=None): 641 # If percentage is not available, display N/A% 642 percentage = data.get('percentage', base.Undefined) 643 if not percentage and percentage != 0: 644 return self.na 645 646 return FormatWidgetMixin.get_format(self, progress, data, format) 647 648 649class SimpleProgress(FormatWidgetMixin, WidgetBase): 650 '''Returns progress as a count of the total (e.g.: "5 of 47")''' 651 652 DEFAULT_FORMAT = '%(value_s)s of %(max_value_s)s' 653 654 def __init__(self, format=DEFAULT_FORMAT, **kwargs): 655 FormatWidgetMixin.__init__(self, format=format, **kwargs) 656 WidgetBase.__init__(self, format=format, **kwargs) 657 self.max_width_cache = dict(default=self.max_width) 658 659 def __call__(self, progress, data, format=None): 660 # If max_value is not available, display N/A 661 if data.get('max_value'): 662 data['max_value_s'] = data.get('max_value') 663 else: 664 data['max_value_s'] = 'N/A' 665 666 # if value is not available it's the zeroth iteration 667 if data.get('value'): 668 data['value_s'] = data['value'] 669 else: 670 data['value_s'] = 0 671 672 formatted = FormatWidgetMixin.__call__(self, progress, data, 673 format=format) 674 675 # Guess the maximum width from the min and max value 676 key = progress.min_value, progress.max_value 677 max_width = self.max_width_cache.get(key, self.max_width) 678 if not max_width: 679 temporary_data = data.copy() 680 for value in key: 681 if value is None: # pragma: no cover 682 continue 683 684 temporary_data['value'] = value 685 width = progress.custom_len(FormatWidgetMixin.__call__( 686 self, progress, temporary_data, format=format)) 687 if width: # pragma: no branch 688 max_width = max(max_width or 0, width) 689 690 self.max_width_cache[key] = max_width 691 692 # Adjust the output to have a consistent size in all cases 693 if max_width: # pragma: no branch 694 formatted = formatted.rjust(max_width) 695 696 return formatted 697 698 699class Bar(AutoWidthWidgetBase): 700 '''A progress bar which stretches to fill the line.''' 701 702 def __init__(self, marker='#', left='|', right='|', fill=' ', 703 fill_left=True, marker_wrap=None, **kwargs): 704 '''Creates a customizable progress bar. 705 706 The callable takes the same parameters as the `__call__` method 707 708 marker - string or callable object to use as a marker 709 left - string or callable object to use as a left border 710 right - string or callable object to use as a right border 711 fill - character to use for the empty part of the progress bar 712 fill_left - whether to fill from the left or the right 713 ''' 714 715 self.marker = create_marker(marker, marker_wrap) 716 self.left = string_or_lambda(left) 717 self.right = string_or_lambda(right) 718 self.fill = string_or_lambda(fill) 719 self.fill_left = fill_left 720 721 AutoWidthWidgetBase.__init__(self, **kwargs) 722 723 def __call__(self, progress, data, width): 724 '''Updates the progress bar and its subcomponents''' 725 726 left = converters.to_unicode(self.left(progress, data, width)) 727 right = converters.to_unicode(self.right(progress, data, width)) 728 width -= progress.custom_len(left) + progress.custom_len(right) 729 marker = converters.to_unicode(self.marker(progress, data, width)) 730 fill = converters.to_unicode(self.fill(progress, data, width)) 731 732 # Make sure we ignore invisible characters when filling 733 width += len(marker) - progress.custom_len(marker) 734 735 if self.fill_left: 736 marker = marker.ljust(width, fill) 737 else: 738 marker = marker.rjust(width, fill) 739 740 return left + marker + right 741 742 743class ReverseBar(Bar): 744 '''A bar which has a marker that goes from right to left''' 745 746 def __init__(self, marker='#', left='|', right='|', fill=' ', 747 fill_left=False, **kwargs): 748 '''Creates a customizable progress bar. 749 750 marker - string or updatable object to use as a marker 751 left - string or updatable object to use as a left border 752 right - string or updatable object to use as a right border 753 fill - character to use for the empty part of the progress bar 754 fill_left - whether to fill from the left or the right 755 ''' 756 Bar.__init__(self, marker=marker, left=left, right=right, fill=fill, 757 fill_left=fill_left, **kwargs) 758 759 760class BouncingBar(Bar, TimeSensitiveWidgetBase): 761 '''A bar which has a marker which bounces from side to side.''' 762 763 INTERVAL = datetime.timedelta(milliseconds=100) 764 765 def __call__(self, progress, data, width): 766 '''Updates the progress bar and its subcomponents''' 767 768 left = converters.to_unicode(self.left(progress, data, width)) 769 right = converters.to_unicode(self.right(progress, data, width)) 770 width -= progress.custom_len(left) + progress.custom_len(right) 771 marker = converters.to_unicode(self.marker(progress, data, width)) 772 773 fill = converters.to_unicode(self.fill(progress, data, width)) 774 775 if width: # pragma: no branch 776 value = int( 777 data['total_seconds_elapsed'] / self.INTERVAL.total_seconds()) 778 779 a = value % width 780 b = width - a - 1 781 if value % (width * 2) >= width: 782 a, b = b, a 783 784 if self.fill_left: 785 marker = a * fill + marker + b * fill 786 else: 787 marker = b * fill + marker + a * fill 788 789 return left + marker + right 790 791 792class FormatCustomText(FormatWidgetMixin, WidgetBase): 793 mapping = {} 794 copy = False 795 796 def __init__(self, format, mapping=mapping, **kwargs): 797 self.format = format 798 self.mapping = mapping 799 FormatWidgetMixin.__init__(self, format=format, **kwargs) 800 WidgetBase.__init__(self, **kwargs) 801 802 def update_mapping(self, **mapping): 803 self.mapping.update(mapping) 804 805 def __call__(self, progress, data): 806 return FormatWidgetMixin.__call__( 807 self, progress, self.mapping, self.format) 808 809 810class VariableMixin(object): 811 '''Mixin to display a custom user variable ''' 812 813 def __init__(self, name, **kwargs): 814 if not isinstance(name, six.string_types): 815 raise TypeError('Variable(): argument must be a string') 816 if len(name.split()) > 1: 817 raise ValueError('Variable(): argument must be single word') 818 self.name = name 819 820 821class MultiRangeBar(Bar, VariableMixin): 822 ''' 823 A bar with multiple sub-ranges, each represented by a different symbol 824 825 The various ranges are represented on a user-defined variable, formatted as 826 827 .. code-block:: python 828 829 [ 830 ['Symbol1', amount1], 831 ['Symbol2', amount2], 832 ... 833 ] 834 ''' 835 836 def __init__(self, name, markers, **kwargs): 837 VariableMixin.__init__(self, name) 838 Bar.__init__(self, **kwargs) 839 self.markers = [ 840 string_or_lambda(marker) 841 for marker in markers 842 ] 843 844 def get_values(self, progress, data): 845 return data['variables'][self.name] or [] 846 847 def __call__(self, progress, data, width): 848 '''Updates the progress bar and its subcomponents''' 849 850 left = converters.to_unicode(self.left(progress, data, width)) 851 right = converters.to_unicode(self.right(progress, data, width)) 852 width -= progress.custom_len(left) + progress.custom_len(right) 853 values = self.get_values(progress, data) 854 855 values_sum = sum(values) 856 if width and values_sum: 857 middle = '' 858 values_accumulated = 0 859 width_accumulated = 0 860 for marker, value in zip(self.markers, values): 861 marker = converters.to_unicode(marker(progress, data, width)) 862 assert progress.custom_len(marker) == 1 863 864 values_accumulated += value 865 item_width = int(values_accumulated / values_sum * width) 866 item_width -= width_accumulated 867 width_accumulated += item_width 868 middle += item_width * marker 869 else: 870 fill = converters.to_unicode(self.fill(progress, data, width)) 871 assert progress.custom_len(fill) == 1 872 middle = fill * width 873 874 return left + middle + right 875 876 877class MultiProgressBar(MultiRangeBar): 878 def __init__(self, 879 name, 880 # NOTE: the markers are not whitespace even though some 881 # terminals don't show the characters correctly! 882 markers=' ▁▂▃▄▅▆▇█', 883 **kwargs): 884 MultiRangeBar.__init__(self, name=name, 885 markers=list(reversed(markers)), **kwargs) 886 887 def get_values(self, progress, data): 888 ranges = [0] * len(self.markers) 889 for progress in data['variables'][self.name] or []: 890 if not isinstance(progress, (int, float)): 891 # Progress is (value, max) 892 progress_value, progress_max = progress 893 progress = float(progress_value) / float(progress_max) 894 895 if progress < 0 or progress > 1: 896 raise ValueError( 897 'Range value needs to be in the range [0..1], got %s' % 898 progress) 899 900 range_ = progress * (len(ranges) - 1) 901 pos = int(range_) 902 frac = range_ % 1 903 ranges[pos] += (1 - frac) 904 if (frac): 905 ranges[pos + 1] += (frac) 906 907 if self.fill_left: 908 ranges = list(reversed(ranges)) 909 return ranges 910 911 912class GranularMarkers: 913 smooth = ' ▏▎▍▌▋▊▉█' 914 bar = ' ▁▂▃▄▅▆▇█' 915 snake = ' ▖▌▛█' 916 fade_in = ' ░▒▓█' 917 dots = ' ⡀⡄⡆⡇⣇⣧⣷⣿' 918 growing_circles = ' .oO' 919 920 921class GranularBar(AutoWidthWidgetBase): 922 '''A progressbar that can display progress at a sub-character granularity 923 by using multiple marker characters. 924 925 Examples of markers: 926 - Smooth: ` ▏▎▍▌▋▊▉█` (default) 927 - Bar: ` ▁▂▃▄▅▆▇█` 928 - Snake: ` ▖▌▛█` 929 - Fade in: ` ░▒▓█` 930 - Dots: ` ⡀⡄⡆⡇⣇⣧⣷⣿` 931 - Growing circles: ` .oO` 932 933 The markers can be accessed through GranularMarkers. GranularMarkers.dots 934 for example 935 ''' 936 937 def __init__(self, markers=GranularMarkers.smooth, left='|', right='|', 938 **kwargs): 939 '''Creates a customizable progress bar. 940 941 markers - string of characters to use as granular progress markers. The 942 first character should represent 0% and the last 100%. 943 Ex: ` .oO`. 944 left - string or callable object to use as a left border 945 right - string or callable object to use as a right border 946 ''' 947 self.markers = markers 948 self.left = string_or_lambda(left) 949 self.right = string_or_lambda(right) 950 951 AutoWidthWidgetBase.__init__(self, **kwargs) 952 953 def __call__(self, progress, data, width): 954 left = converters.to_unicode(self.left(progress, data, width)) 955 right = converters.to_unicode(self.right(progress, data, width)) 956 width -= progress.custom_len(left) + progress.custom_len(right) 957 958 if progress.max_value is not base.UnknownLength \ 959 and progress.max_value > 0: 960 percent = progress.value / progress.max_value 961 else: 962 percent = 0 963 964 num_chars = percent * width 965 966 marker = self.markers[-1] * int(num_chars) 967 968 marker_idx = int((num_chars % 1) * (len(self.markers) - 1)) 969 if marker_idx: 970 marker += self.markers[marker_idx] 971 972 marker = converters.to_unicode(marker) 973 974 # Make sure we ignore invisible characters when filling 975 width += len(marker) - progress.custom_len(marker) 976 marker = marker.ljust(width, self.markers[0]) 977 978 return left + marker + right 979 980 981class FormatLabelBar(FormatLabel, Bar): 982 '''A bar which has a formatted label in the center.''' 983 def __init__(self, format, **kwargs): 984 FormatLabel.__init__(self, format, **kwargs) 985 Bar.__init__(self, **kwargs) 986 987 def __call__(self, progress, data, width, format=None): 988 center = FormatLabel.__call__(self, progress, data, format=format) 989 bar = Bar.__call__(self, progress, data, width) 990 991 # Aligns the center of the label to the center of the bar 992 center_len = progress.custom_len(center) 993 center_left = int((width - center_len) / 2) 994 center_right = center_left + center_len 995 return bar[:center_left] + center + bar[center_right:] 996 997 998class PercentageLabelBar(Percentage, FormatLabelBar): 999 '''A bar which displays the current percentage in the center.''' 1000 # %3d adds an extra space that makes it look off-center 1001 # %2d keeps the label somewhat consistently in-place 1002 def __init__(self, format='%(percentage)2d%%', na='N/A%%', **kwargs): 1003 Percentage.__init__(self, format, na=na, **kwargs) 1004 FormatLabelBar.__init__(self, format, **kwargs) 1005 1006 1007class Variable(FormatWidgetMixin, VariableMixin, WidgetBase): 1008 '''Displays a custom variable.''' 1009 1010 def __init__(self, name, format='{name}: {formatted_value}', 1011 width=6, precision=3, **kwargs): 1012 '''Creates a Variable associated with the given name.''' 1013 self.format = format 1014 self.width = width 1015 self.precision = precision 1016 VariableMixin.__init__(self, name=name) 1017 WidgetBase.__init__(self, **kwargs) 1018 1019 def __call__(self, progress, data): 1020 value = data['variables'][self.name] 1021 context = data.copy() 1022 context['value'] = value 1023 context['name'] = self.name 1024 context['width'] = self.width 1025 context['precision'] = self.precision 1026 1027 try: 1028 # Make sure to try and cast the value first, otherwise the 1029 # formatting will generate warnings/errors on newer Python releases 1030 value = float(value) 1031 fmt = '{value:{width}.{precision}}' 1032 context['formatted_value'] = fmt.format(**context) 1033 except (TypeError, ValueError): 1034 if value: 1035 context['formatted_value'] = '{value:{width}}'.format( 1036 **context) 1037 else: 1038 context['formatted_value'] = '-' * self.width 1039 1040 return self.format.format(**context) 1041 1042 1043class DynamicMessage(Variable): 1044 '''Kept for backwards compatibility, please use `Variable` instead.''' 1045 pass 1046 1047 1048class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase): 1049 '''Widget which displays the current (date)time with seconds resolution.''' 1050 INTERVAL = datetime.timedelta(seconds=1) 1051 1052 def __init__(self, format='Current Time: %(current_time)s', 1053 microseconds=False, **kwargs): 1054 self.microseconds = microseconds 1055 FormatWidgetMixin.__init__(self, format=format, **kwargs) 1056 TimeSensitiveWidgetBase.__init__(self, **kwargs) 1057 1058 def __call__(self, progress, data): 1059 data['current_time'] = self.current_time() 1060 data['current_datetime'] = self.current_datetime() 1061 1062 return FormatWidgetMixin.__call__(self, progress, data) 1063 1064 def current_datetime(self): 1065 now = datetime.datetime.now() 1066 if not self.microseconds: 1067 now = now.replace(microsecond=0) 1068 1069 return now 1070 1071 def current_time(self): 1072 return self.current_datetime().time() 1073 1074