1# -*- coding: utf-8 -*- 2# 3# progressbar - Text progress bar library for Python. 4# Copyright (c) 2005 Nilton Volpato 5# 6# SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause-Clear 7# 8# This library is free software; you can redistribute it and/or 9# modify it under the terms of the GNU Lesser General Public 10# License as published by the Free Software Foundation; either 11# version 2.1 of the License, or (at your option) any later version. 12# 13# This library is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16# Lesser General Public License for more details. 17# 18# You should have received a copy of the GNU Lesser General Public 19# License along with this library; if not, write to the Free Software 20# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 21 22"""Default ProgressBar widgets.""" 23 24from __future__ import division 25 26import datetime 27import math 28 29try: 30 from abc import ABCMeta, abstractmethod 31except ImportError: 32 AbstractWidget = object 33 abstractmethod = lambda fn: fn 34else: 35 AbstractWidget = ABCMeta('AbstractWidget', (object,), {}) 36 37 38def format_updatable(updatable, pbar): 39 if hasattr(updatable, 'update'): return updatable.update(pbar) 40 else: return updatable 41 42 43class Widget(AbstractWidget): 44 """The base class for all widgets. 45 46 The ProgressBar will call the widget's update value when the widget should 47 be updated. The widget's size may change between calls, but the widget may 48 display incorrectly if the size changes drastically and repeatedly. 49 50 The boolean TIME_SENSITIVE informs the ProgressBar that it should be 51 updated more often because it is time sensitive. 52 """ 53 54 TIME_SENSITIVE = False 55 __slots__ = () 56 57 @abstractmethod 58 def update(self, pbar): 59 """Updates the widget. 60 61 pbar - a reference to the calling ProgressBar 62 """ 63 64 65class WidgetHFill(Widget): 66 """The base class for all variable width widgets. 67 68 This widget is much like the \\hfill command in TeX, it will expand to 69 fill the line. You can use more than one in the same line, and they will 70 all have the same width, and together will fill the line. 71 """ 72 73 @abstractmethod 74 def update(self, pbar, width): 75 """Updates the widget providing the total width the widget must fill. 76 77 pbar - a reference to the calling ProgressBar 78 width - The total width the widget must fill 79 """ 80 81 82class Timer(Widget): 83 """Widget which displays the elapsed seconds.""" 84 85 __slots__ = ('format_string',) 86 TIME_SENSITIVE = True 87 88 def __init__(self, format='Elapsed Time: %s'): 89 self.format_string = format 90 91 @staticmethod 92 def format_time(seconds): 93 """Formats time as the string "HH:MM:SS".""" 94 95 return str(datetime.timedelta(seconds=int(seconds))) 96 97 98 def update(self, pbar): 99 """Updates the widget to show the elapsed time.""" 100 101 return self.format_string % self.format_time(pbar.seconds_elapsed) 102 103 104class ETA(Timer): 105 """Widget which attempts to estimate the time of arrival.""" 106 107 TIME_SENSITIVE = True 108 109 def update(self, pbar): 110 """Updates the widget to show the ETA or total time when finished.""" 111 112 if pbar.currval == 0: 113 return 'ETA: --:--:--' 114 elif pbar.finished: 115 return 'Time: %s' % self.format_time(pbar.seconds_elapsed) 116 else: 117 elapsed = pbar.seconds_elapsed 118 eta = elapsed * pbar.maxval / pbar.currval - elapsed 119 return 'ETA: %s' % self.format_time(eta) 120 121 122class AdaptiveETA(Timer): 123 """Widget which attempts to estimate the time of arrival. 124 125 Uses a weighted average of two estimates: 126 1) ETA based on the total progress and time elapsed so far 127 2) ETA based on the progress as per the last 10 update reports 128 129 The weight depends on the current progress so that to begin with the 130 total progress is used and at the end only the most recent progress is 131 used. 132 """ 133 134 TIME_SENSITIVE = True 135 NUM_SAMPLES = 10 136 137 def _update_samples(self, currval, elapsed): 138 sample = (currval, elapsed) 139 if not hasattr(self, 'samples'): 140 self.samples = [sample] * (self.NUM_SAMPLES + 1) 141 else: 142 self.samples.append(sample) 143 return self.samples.pop(0) 144 145 def _eta(self, maxval, currval, elapsed): 146 return elapsed * maxval / float(currval) - elapsed 147 148 def update(self, pbar): 149 """Updates the widget to show the ETA or total time when finished.""" 150 if pbar.currval == 0: 151 return 'ETA: --:--:--' 152 elif pbar.finished: 153 return 'Time: %s' % self.format_time(pbar.seconds_elapsed) 154 else: 155 elapsed = pbar.seconds_elapsed 156 currval1, elapsed1 = self._update_samples(pbar.currval, elapsed) 157 eta = self._eta(pbar.maxval, pbar.currval, elapsed) 158 if pbar.currval > currval1: 159 etasamp = self._eta(pbar.maxval - currval1, 160 pbar.currval - currval1, 161 elapsed - elapsed1) 162 weight = (pbar.currval / float(pbar.maxval)) ** 0.5 163 eta = (1 - weight) * eta + weight * etasamp 164 return 'ETA: %s' % self.format_time(eta) 165 166 167class FileTransferSpeed(Widget): 168 """Widget for showing the transfer speed (useful for file transfers).""" 169 170 FORMAT = '%6.2f %s%s/s' 171 PREFIXES = ' kMGTPEZY' 172 __slots__ = ('unit',) 173 174 def __init__(self, unit='B'): 175 self.unit = unit 176 177 def update(self, pbar): 178 """Updates the widget with the current SI prefixed speed.""" 179 180 if pbar.seconds_elapsed < 2e-6 or pbar.currval < 2e-6: # =~ 0 181 scaled = power = 0 182 else: 183 speed = pbar.currval / pbar.seconds_elapsed 184 power = int(math.log(speed, 1000)) 185 scaled = speed / 1000.**power 186 187 return self.FORMAT % (scaled, self.PREFIXES[power], self.unit) 188 189 190class AnimatedMarker(Widget): 191 """An animated marker for the progress bar which defaults to appear as if 192 it were rotating. 193 """ 194 195 __slots__ = ('markers', 'curmark') 196 197 def __init__(self, markers='|/-\\'): 198 self.markers = markers 199 self.curmark = -1 200 201 def update(self, pbar): 202 """Updates the widget to show the next marker or the first marker when 203 finished""" 204 205 if pbar.finished: return self.markers[0] 206 207 self.curmark = (self.curmark + 1) % len(self.markers) 208 return self.markers[self.curmark] 209 210# Alias for backwards compatibility 211RotatingMarker = AnimatedMarker 212 213 214class Counter(Widget): 215 """Displays the current count.""" 216 217 __slots__ = ('format_string',) 218 219 def __init__(self, format='%d'): 220 self.format_string = format 221 222 def update(self, pbar): 223 return self.format_string % pbar.currval 224 225 226class Percentage(Widget): 227 """Displays the current percentage as a number with a percent sign.""" 228 229 def update(self, pbar): 230 return '%3d%%' % pbar.percentage() 231 232 233class FormatLabel(Timer): 234 """Displays a formatted label.""" 235 236 mapping = { 237 'elapsed': ('seconds_elapsed', Timer.format_time), 238 'finished': ('finished', None), 239 'last_update': ('last_update_time', None), 240 'max': ('maxval', None), 241 'seconds': ('seconds_elapsed', None), 242 'start': ('start_time', None), 243 'value': ('currval', None) 244 } 245 246 __slots__ = ('format_string',) 247 def __init__(self, format): 248 self.format_string = format 249 250 def update(self, pbar): 251 context = {} 252 for name, (key, transform) in self.mapping.items(): 253 try: 254 value = getattr(pbar, key) 255 256 if transform is None: 257 context[name] = value 258 else: 259 context[name] = transform(value) 260 except: pass 261 262 return self.format_string % context 263 264 265class SimpleProgress(Widget): 266 """Returns progress as a count of the total (e.g.: "5 of 47").""" 267 268 __slots__ = ('sep',) 269 270 def __init__(self, sep=' of '): 271 self.sep = sep 272 273 def update(self, pbar): 274 return '%d%s%d' % (pbar.currval, self.sep, pbar.maxval) 275 276 277class Bar(WidgetHFill): 278 """A progress bar which stretches to fill the line.""" 279 280 __slots__ = ('marker', 'left', 'right', 'fill', 'fill_left') 281 282 def __init__(self, marker='#', left='|', right='|', fill=' ', 283 fill_left=True): 284 """Creates a customizable progress bar. 285 286 marker - string or updatable object to use as a marker 287 left - string or updatable object to use as a left border 288 right - string or updatable object to use as a right border 289 fill - character to use for the empty part of the progress bar 290 fill_left - whether to fill from the left or the right 291 """ 292 self.marker = marker 293 self.left = left 294 self.right = right 295 self.fill = fill 296 self.fill_left = fill_left 297 298 299 def update(self, pbar, width): 300 """Updates the progress bar and its subcomponents.""" 301 302 left, marked, right = (format_updatable(i, pbar) for i in 303 (self.left, self.marker, self.right)) 304 305 width -= len(left) + len(right) 306 # Marked must *always* have length of 1 307 if pbar.maxval: 308 marked *= int(pbar.currval / pbar.maxval * width) 309 else: 310 marked = '' 311 312 if self.fill_left: 313 return '%s%s%s' % (left, marked.ljust(width, self.fill), right) 314 else: 315 return '%s%s%s' % (left, marked.rjust(width, self.fill), right) 316 317 318class ReverseBar(Bar): 319 """A bar which has a marker which bounces from side to side.""" 320 321 def __init__(self, marker='#', left='|', right='|', fill=' ', 322 fill_left=False): 323 """Creates a customizable progress bar. 324 325 marker - string or updatable object to use as a marker 326 left - string or updatable object to use as a left border 327 right - string or updatable object to use as a right border 328 fill - character to use for the empty part of the progress bar 329 fill_left - whether to fill from the left or the right 330 """ 331 self.marker = marker 332 self.left = left 333 self.right = right 334 self.fill = fill 335 self.fill_left = fill_left 336 337 338class BouncingBar(Bar): 339 def update(self, pbar, width): 340 """Updates the progress bar and its subcomponents.""" 341 342 left, marker, right = (format_updatable(i, pbar) for i in 343 (self.left, self.marker, self.right)) 344 345 width -= len(left) + len(right) 346 347 if pbar.finished: return '%s%s%s' % (left, width * marker, right) 348 349 position = int(pbar.currval % (width * 2 - 1)) 350 if position > width: position = width * 2 - position 351 lpad = self.fill * (position - 1) 352 rpad = self.fill * (width - len(marker) - len(lpad)) 353 354 # Swap if we want to bounce the other way 355 if not self.fill_left: rpad, lpad = lpad, rpad 356 357 return '%s%s%s%s%s' % (left, lpad, marker, rpad, right) 358 359 360class BouncingSlider(Bar): 361 """ 362 A slider that bounces back and forth in response to update() calls 363 without reference to the actual value. Based on a combination of 364 BouncingBar from a newer version of this module and RotatingMarker. 365 """ 366 def __init__(self, marker='<=>'): 367 self.curmark = -1 368 self.forward = True 369 Bar.__init__(self, marker=marker) 370 def update(self, pbar, width): 371 left, marker, right = (format_updatable(i, pbar) for i in 372 (self.left, self.marker, self.right)) 373 374 width -= len(left) + len(right) 375 if width < 0: 376 return '' 377 378 if pbar.finished: return '%s%s%s' % (left, width * '=', right) 379 380 self.curmark = self.curmark + 1 381 position = int(self.curmark % (width * 2 - 1)) 382 if position + len(marker) > width: 383 self.forward = not self.forward 384 self.curmark = 1 385 position = 1 386 lpad = ' ' * (position - 1) 387 rpad = ' ' * (width - len(marker) - len(lpad)) 388 389 if not self.forward: 390 temp = lpad 391 lpad = rpad 392 rpad = temp 393 return '%s%s%s%s%s' % (left, lpad, marker, rpad, right) 394