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