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# (With some small changes after importing into BitBake) 7*4882a593Smuzhiyun# 8*4882a593Smuzhiyun# SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause-Clear 9*4882a593Smuzhiyun# 10*4882a593Smuzhiyun# This library is free software; you can redistribute it and/or 11*4882a593Smuzhiyun# modify it under the terms of the GNU Lesser General Public 12*4882a593Smuzhiyun# License as published by the Free Software Foundation; either 13*4882a593Smuzhiyun# version 2.1 of the License, or (at your option) any later version. 14*4882a593Smuzhiyun# 15*4882a593Smuzhiyun# This library is distributed in the hope that it will be useful, 16*4882a593Smuzhiyun# but WITHOUT ANY WARRANTY; without even the implied warranty of 17*4882a593Smuzhiyun# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18*4882a593Smuzhiyun# Lesser General Public License for more details. 19*4882a593Smuzhiyun# 20*4882a593Smuzhiyun# You should have received a copy of the GNU Lesser General Public 21*4882a593Smuzhiyun# License along with this library; if not, write to the Free Software 22*4882a593Smuzhiyun# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 23*4882a593Smuzhiyun 24*4882a593Smuzhiyun"""Main ProgressBar class.""" 25*4882a593Smuzhiyun 26*4882a593Smuzhiyunfrom __future__ import division 27*4882a593Smuzhiyun 28*4882a593Smuzhiyunimport math 29*4882a593Smuzhiyunimport os 30*4882a593Smuzhiyunimport signal 31*4882a593Smuzhiyunimport sys 32*4882a593Smuzhiyunimport time 33*4882a593Smuzhiyun 34*4882a593Smuzhiyuntry: 35*4882a593Smuzhiyun from fcntl import ioctl 36*4882a593Smuzhiyun from array import array 37*4882a593Smuzhiyun import termios 38*4882a593Smuzhiyunexcept ImportError: 39*4882a593Smuzhiyun pass 40*4882a593Smuzhiyun 41*4882a593Smuzhiyunfrom .compat import * # for: any, next 42*4882a593Smuzhiyunfrom . import widgets 43*4882a593Smuzhiyun 44*4882a593Smuzhiyun 45*4882a593Smuzhiyunclass UnknownLength: pass 46*4882a593Smuzhiyun 47*4882a593Smuzhiyun 48*4882a593Smuzhiyunclass ProgressBar(object): 49*4882a593Smuzhiyun """The ProgressBar class which updates and prints the bar. 50*4882a593Smuzhiyun 51*4882a593Smuzhiyun A common way of using it is like: 52*4882a593Smuzhiyun >>> pbar = ProgressBar().start() 53*4882a593Smuzhiyun >>> for i in range(100): 54*4882a593Smuzhiyun ... # do something 55*4882a593Smuzhiyun ... pbar.update(i+1) 56*4882a593Smuzhiyun ... 57*4882a593Smuzhiyun >>> pbar.finish() 58*4882a593Smuzhiyun 59*4882a593Smuzhiyun You can also use a ProgressBar as an iterator: 60*4882a593Smuzhiyun >>> progress = ProgressBar() 61*4882a593Smuzhiyun >>> for i in progress(some_iterable): 62*4882a593Smuzhiyun ... # do something 63*4882a593Smuzhiyun ... 64*4882a593Smuzhiyun 65*4882a593Smuzhiyun Since the progress bar is incredibly customizable you can specify 66*4882a593Smuzhiyun different widgets of any type in any order. You can even write your own 67*4882a593Smuzhiyun widgets! However, since there are already a good number of widgets you 68*4882a593Smuzhiyun should probably play around with them before moving on to create your own 69*4882a593Smuzhiyun widgets. 70*4882a593Smuzhiyun 71*4882a593Smuzhiyun The term_width parameter represents the current terminal width. If the 72*4882a593Smuzhiyun parameter is set to an integer then the progress bar will use that, 73*4882a593Smuzhiyun otherwise it will attempt to determine the terminal width falling back to 74*4882a593Smuzhiyun 80 columns if the width cannot be determined. 75*4882a593Smuzhiyun 76*4882a593Smuzhiyun When implementing a widget's update method you are passed a reference to 77*4882a593Smuzhiyun the current progress bar. As a result, you have access to the 78*4882a593Smuzhiyun ProgressBar's methods and attributes. Although there is nothing preventing 79*4882a593Smuzhiyun you from changing the ProgressBar you should treat it as read only. 80*4882a593Smuzhiyun 81*4882a593Smuzhiyun Useful methods and attributes include (Public API): 82*4882a593Smuzhiyun - currval: current progress (0 <= currval <= maxval) 83*4882a593Smuzhiyun - maxval: maximum (and final) value 84*4882a593Smuzhiyun - finished: True if the bar has finished (reached 100%) 85*4882a593Smuzhiyun - start_time: the time when start() method of ProgressBar was called 86*4882a593Smuzhiyun - seconds_elapsed: seconds elapsed since start_time and last call to 87*4882a593Smuzhiyun update 88*4882a593Smuzhiyun - percentage(): progress in percent [0..100] 89*4882a593Smuzhiyun """ 90*4882a593Smuzhiyun 91*4882a593Smuzhiyun __slots__ = ('currval', 'fd', 'finished', 'last_update_time', 92*4882a593Smuzhiyun 'left_justify', 'maxval', 'next_update', 'num_intervals', 93*4882a593Smuzhiyun 'poll', 'seconds_elapsed', 'signal_set', 'start_time', 94*4882a593Smuzhiyun 'term_width', 'update_interval', 'widgets', '_time_sensitive', 95*4882a593Smuzhiyun '__iterable') 96*4882a593Smuzhiyun 97*4882a593Smuzhiyun _DEFAULT_MAXVAL = 100 98*4882a593Smuzhiyun _DEFAULT_TERMSIZE = 80 99*4882a593Smuzhiyun _DEFAULT_WIDGETS = [widgets.Percentage(), ' ', widgets.Bar()] 100*4882a593Smuzhiyun 101*4882a593Smuzhiyun def __init__(self, maxval=None, widgets=None, term_width=None, poll=1, 102*4882a593Smuzhiyun left_justify=True, fd=sys.stderr): 103*4882a593Smuzhiyun """Initializes a progress bar with sane defaults.""" 104*4882a593Smuzhiyun 105*4882a593Smuzhiyun # Don't share a reference with any other progress bars 106*4882a593Smuzhiyun if widgets is None: 107*4882a593Smuzhiyun widgets = list(self._DEFAULT_WIDGETS) 108*4882a593Smuzhiyun 109*4882a593Smuzhiyun self.maxval = maxval 110*4882a593Smuzhiyun self.widgets = widgets 111*4882a593Smuzhiyun self.fd = fd 112*4882a593Smuzhiyun self.left_justify = left_justify 113*4882a593Smuzhiyun 114*4882a593Smuzhiyun self.signal_set = False 115*4882a593Smuzhiyun if term_width is not None: 116*4882a593Smuzhiyun self.term_width = term_width 117*4882a593Smuzhiyun else: 118*4882a593Smuzhiyun try: 119*4882a593Smuzhiyun self._handle_resize(None, None) 120*4882a593Smuzhiyun signal.signal(signal.SIGWINCH, self._handle_resize) 121*4882a593Smuzhiyun self.signal_set = True 122*4882a593Smuzhiyun except (SystemExit, KeyboardInterrupt): raise 123*4882a593Smuzhiyun except Exception as e: 124*4882a593Smuzhiyun print("DEBUG 5 %s" % e) 125*4882a593Smuzhiyun self.term_width = self._env_size() 126*4882a593Smuzhiyun 127*4882a593Smuzhiyun self.__iterable = None 128*4882a593Smuzhiyun self._update_widgets() 129*4882a593Smuzhiyun self.currval = 0 130*4882a593Smuzhiyun self.finished = False 131*4882a593Smuzhiyun self.last_update_time = None 132*4882a593Smuzhiyun self.poll = poll 133*4882a593Smuzhiyun self.seconds_elapsed = 0 134*4882a593Smuzhiyun self.start_time = None 135*4882a593Smuzhiyun self.update_interval = 1 136*4882a593Smuzhiyun self.next_update = 0 137*4882a593Smuzhiyun 138*4882a593Smuzhiyun 139*4882a593Smuzhiyun def __call__(self, iterable): 140*4882a593Smuzhiyun """Use a ProgressBar to iterate through an iterable.""" 141*4882a593Smuzhiyun 142*4882a593Smuzhiyun try: 143*4882a593Smuzhiyun self.maxval = len(iterable) 144*4882a593Smuzhiyun except: 145*4882a593Smuzhiyun if self.maxval is None: 146*4882a593Smuzhiyun self.maxval = UnknownLength 147*4882a593Smuzhiyun 148*4882a593Smuzhiyun self.__iterable = iter(iterable) 149*4882a593Smuzhiyun return self 150*4882a593Smuzhiyun 151*4882a593Smuzhiyun 152*4882a593Smuzhiyun def __iter__(self): 153*4882a593Smuzhiyun return self 154*4882a593Smuzhiyun 155*4882a593Smuzhiyun 156*4882a593Smuzhiyun def __next__(self): 157*4882a593Smuzhiyun try: 158*4882a593Smuzhiyun value = next(self.__iterable) 159*4882a593Smuzhiyun if self.start_time is None: 160*4882a593Smuzhiyun self.start() 161*4882a593Smuzhiyun else: 162*4882a593Smuzhiyun self.update(self.currval + 1) 163*4882a593Smuzhiyun return value 164*4882a593Smuzhiyun except StopIteration: 165*4882a593Smuzhiyun if self.start_time is None: 166*4882a593Smuzhiyun self.start() 167*4882a593Smuzhiyun self.finish() 168*4882a593Smuzhiyun raise 169*4882a593Smuzhiyun 170*4882a593Smuzhiyun 171*4882a593Smuzhiyun # Create an alias so that Python 2.x won't complain about not being 172*4882a593Smuzhiyun # an iterator. 173*4882a593Smuzhiyun next = __next__ 174*4882a593Smuzhiyun 175*4882a593Smuzhiyun 176*4882a593Smuzhiyun def _env_size(self): 177*4882a593Smuzhiyun """Tries to find the term_width from the environment.""" 178*4882a593Smuzhiyun 179*4882a593Smuzhiyun return int(os.environ.get('COLUMNS', self._DEFAULT_TERMSIZE)) - 1 180*4882a593Smuzhiyun 181*4882a593Smuzhiyun 182*4882a593Smuzhiyun def _handle_resize(self, signum=None, frame=None): 183*4882a593Smuzhiyun """Tries to catch resize signals sent from the terminal.""" 184*4882a593Smuzhiyun 185*4882a593Smuzhiyun h, w = array('h', ioctl(self.fd, termios.TIOCGWINSZ, '\0' * 8))[:2] 186*4882a593Smuzhiyun self.term_width = w 187*4882a593Smuzhiyun 188*4882a593Smuzhiyun 189*4882a593Smuzhiyun def percentage(self): 190*4882a593Smuzhiyun """Returns the progress as a percentage.""" 191*4882a593Smuzhiyun if self.currval >= self.maxval: 192*4882a593Smuzhiyun return 100.0 193*4882a593Smuzhiyun return (self.currval * 100.0 / self.maxval) if self.maxval else 100.00 194*4882a593Smuzhiyun 195*4882a593Smuzhiyun percent = property(percentage) 196*4882a593Smuzhiyun 197*4882a593Smuzhiyun 198*4882a593Smuzhiyun def _format_widgets(self): 199*4882a593Smuzhiyun result = [] 200*4882a593Smuzhiyun expanding = [] 201*4882a593Smuzhiyun width = self.term_width 202*4882a593Smuzhiyun 203*4882a593Smuzhiyun for index, widget in enumerate(self.widgets): 204*4882a593Smuzhiyun if isinstance(widget, widgets.WidgetHFill): 205*4882a593Smuzhiyun result.append(widget) 206*4882a593Smuzhiyun expanding.insert(0, index) 207*4882a593Smuzhiyun else: 208*4882a593Smuzhiyun widget = widgets.format_updatable(widget, self) 209*4882a593Smuzhiyun result.append(widget) 210*4882a593Smuzhiyun width -= len(widget) 211*4882a593Smuzhiyun 212*4882a593Smuzhiyun count = len(expanding) 213*4882a593Smuzhiyun while count: 214*4882a593Smuzhiyun portion = max(int(math.ceil(width * 1. / count)), 0) 215*4882a593Smuzhiyun index = expanding.pop() 216*4882a593Smuzhiyun count -= 1 217*4882a593Smuzhiyun 218*4882a593Smuzhiyun widget = result[index].update(self, portion) 219*4882a593Smuzhiyun width -= len(widget) 220*4882a593Smuzhiyun result[index] = widget 221*4882a593Smuzhiyun 222*4882a593Smuzhiyun return result 223*4882a593Smuzhiyun 224*4882a593Smuzhiyun 225*4882a593Smuzhiyun def _format_line(self): 226*4882a593Smuzhiyun """Joins the widgets and justifies the line.""" 227*4882a593Smuzhiyun 228*4882a593Smuzhiyun widgets = ''.join(self._format_widgets()) 229*4882a593Smuzhiyun 230*4882a593Smuzhiyun if self.left_justify: return widgets.ljust(self.term_width) 231*4882a593Smuzhiyun else: return widgets.rjust(self.term_width) 232*4882a593Smuzhiyun 233*4882a593Smuzhiyun 234*4882a593Smuzhiyun def _need_update(self): 235*4882a593Smuzhiyun """Returns whether the ProgressBar should redraw the line.""" 236*4882a593Smuzhiyun if self.currval >= self.next_update or self.finished: return True 237*4882a593Smuzhiyun 238*4882a593Smuzhiyun delta = time.time() - self.last_update_time 239*4882a593Smuzhiyun return self._time_sensitive and delta > self.poll 240*4882a593Smuzhiyun 241*4882a593Smuzhiyun 242*4882a593Smuzhiyun def _update_widgets(self): 243*4882a593Smuzhiyun """Checks all widgets for the time sensitive bit.""" 244*4882a593Smuzhiyun 245*4882a593Smuzhiyun self._time_sensitive = any(getattr(w, 'TIME_SENSITIVE', False) 246*4882a593Smuzhiyun for w in self.widgets) 247*4882a593Smuzhiyun 248*4882a593Smuzhiyun 249*4882a593Smuzhiyun def update(self, value=None): 250*4882a593Smuzhiyun """Updates the ProgressBar to a new value.""" 251*4882a593Smuzhiyun 252*4882a593Smuzhiyun if value is not None and value is not UnknownLength: 253*4882a593Smuzhiyun if (self.maxval is not UnknownLength 254*4882a593Smuzhiyun and not 0 <= value <= self.maxval): 255*4882a593Smuzhiyun 256*4882a593Smuzhiyun raise ValueError('Value out of range') 257*4882a593Smuzhiyun 258*4882a593Smuzhiyun self.currval = value 259*4882a593Smuzhiyun 260*4882a593Smuzhiyun 261*4882a593Smuzhiyun if not self._need_update(): return 262*4882a593Smuzhiyun if self.start_time is None: 263*4882a593Smuzhiyun raise RuntimeError('You must call "start" before calling "update"') 264*4882a593Smuzhiyun 265*4882a593Smuzhiyun now = time.time() 266*4882a593Smuzhiyun self.seconds_elapsed = now - self.start_time 267*4882a593Smuzhiyun self.next_update = self.currval + self.update_interval 268*4882a593Smuzhiyun output = self._format_line() 269*4882a593Smuzhiyun self.fd.write(output + '\r') 270*4882a593Smuzhiyun self.fd.flush() 271*4882a593Smuzhiyun self.last_update_time = now 272*4882a593Smuzhiyun return output 273*4882a593Smuzhiyun 274*4882a593Smuzhiyun 275*4882a593Smuzhiyun def start(self, update=True): 276*4882a593Smuzhiyun """Starts measuring time, and prints the bar at 0%. 277*4882a593Smuzhiyun 278*4882a593Smuzhiyun It returns self so you can use it like this: 279*4882a593Smuzhiyun >>> pbar = ProgressBar().start() 280*4882a593Smuzhiyun >>> for i in range(100): 281*4882a593Smuzhiyun ... # do something 282*4882a593Smuzhiyun ... pbar.update(i+1) 283*4882a593Smuzhiyun ... 284*4882a593Smuzhiyun >>> pbar.finish() 285*4882a593Smuzhiyun """ 286*4882a593Smuzhiyun 287*4882a593Smuzhiyun if self.maxval is None: 288*4882a593Smuzhiyun self.maxval = self._DEFAULT_MAXVAL 289*4882a593Smuzhiyun 290*4882a593Smuzhiyun self.num_intervals = max(100, self.term_width) 291*4882a593Smuzhiyun self.next_update = 0 292*4882a593Smuzhiyun 293*4882a593Smuzhiyun if self.maxval is not UnknownLength: 294*4882a593Smuzhiyun if self.maxval < 0: raise ValueError('Value out of range') 295*4882a593Smuzhiyun self.update_interval = self.maxval / self.num_intervals 296*4882a593Smuzhiyun 297*4882a593Smuzhiyun 298*4882a593Smuzhiyun self.start_time = time.time() 299*4882a593Smuzhiyun if update: 300*4882a593Smuzhiyun self.last_update_time = self.start_time 301*4882a593Smuzhiyun self.update(0) 302*4882a593Smuzhiyun else: 303*4882a593Smuzhiyun self.last_update_time = 0 304*4882a593Smuzhiyun 305*4882a593Smuzhiyun return self 306*4882a593Smuzhiyun 307*4882a593Smuzhiyun 308*4882a593Smuzhiyun def finish(self): 309*4882a593Smuzhiyun """Puts the ProgressBar bar in the finished state.""" 310*4882a593Smuzhiyun 311*4882a593Smuzhiyun if self.finished: 312*4882a593Smuzhiyun return 313*4882a593Smuzhiyun self.finished = True 314*4882a593Smuzhiyun self.update(self.maxval) 315*4882a593Smuzhiyun self.fd.write('\n') 316*4882a593Smuzhiyun if self.signal_set: 317*4882a593Smuzhiyun signal.signal(signal.SIGWINCH, signal.SIG_DFL) 318