1# -*- 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# SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause-Clear
9#
10# 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                self.maxval = value
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)
318