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