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