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