1# -*- coding: utf-8 -*-
2#
3# progressbar  - Text progress bar library for Python.
4# Copyright (c) 2005 Nilton Volpato
5#
6# SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause-Clear
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"""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)
394