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