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