blob: e2b6ba10836eb8fe161b003f2e78ab1e39383ffa [file] [log] [blame]
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001# -*- 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#
Brad Bishopc342db32019-05-15 21:57:59 -04008# SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause-Clear
9#
Patrick Williamsc0f7c042017-02-23 20:41:17 -060010# 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 raise ValueError('Value out of range')
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)