~bzr-pqm/bzr/bzr.dev

649 by Martin Pool
- some cleanups for the progressbar method
1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
2
# Copyright (C) 2005 Canonical <canonical.com>
648 by Martin Pool
- import aaron's progress-indicator code
3
#
4
#    This program is free software; you can redistribute it and/or modify
5
#    it under the terms of the GNU General Public License as published by
6
#    the Free Software Foundation; either version 2 of the License, or
7
#    (at your option) any later version.
8
#
9
#    This program is distributed in the hope that it will be useful,
10
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
#    GNU General Public License for more details.
13
#
14
#    You should have received a copy of the GNU General Public License
15
#    along with this program; if not, write to the Free Software
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
649 by Martin Pool
- some cleanups for the progressbar method
18
19
"""
20
Simple text-mode progress indicator.
21
22
Everyone loves ascii art!
23
24
To display an indicator, create a ProgressBar object.  Call it,
25
passing Progress objects indicating the current state.  When done,
26
call clear().
27
28
Progress is suppressed when output is not sent to a terminal, so as
29
not to clutter log files.
30
"""
31
32
# TODO: remove functions in favour of keeping everything in one class
33
652 by Martin Pool
doc
34
# TODO: should be a global option e.g. --silent that disables progress
35
# indicators, preferably without needing to adjust all code that
36
# potentially calls them.
37
655 by Martin Pool
- better calculation of progress bar position
38
# TODO: Perhaps don't write updates faster than a certain rate, say
39
# 5/second.
40
649 by Martin Pool
- some cleanups for the progressbar method
41
648 by Martin Pool
- import aaron's progress-indicator code
42
import sys
660 by Martin Pool
- use plain unix time, not datetime module
43
import time
648 by Martin Pool
- import aaron's progress-indicator code
44
649 by Martin Pool
- some cleanups for the progressbar method
45
46
def _width():
47
    """Return estimated terminal width.
48
49
    TODO: Do something smart on Windows?
50
51
    TODO: Is there anything that gets a better update when the window
52
          is resized while the program is running?
53
    """
54
    import os
55
    try:
56
        return int(os.environ['COLUMNS'])
57
    except (IndexError, KeyError, ValueError):
58
        return 80
59
60
61
def _supports_progress(f):
695 by Martin Pool
- don't display progress bars on really dumb terminals
62
    if not hasattr(f, 'isatty'):
63
        return False
64
    if not f.isatty():
65
        return False
66
    import os
67
    if os.environ.get('TERM') == 'dumb':
68
        # e.g. emacs compile window
69
        return False
70
    return True
649 by Martin Pool
- some cleanups for the progressbar method
71
72
73
648 by Martin Pool
- import aaron's progress-indicator code
74
class ProgressBar(object):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
75
    """Progress bar display object.
76
77
    Several options are available to control the display.  These can
78
    be passed as parameters to the constructor or assigned at any time:
79
80
    show_pct
81
        Show percentage complete.
82
    show_spinner
83
        Show rotating baton.  This ticks over on every update even
84
        if the values don't change.
85
    show_eta
86
        Show predicted time-to-completion.
87
    show_bar
88
        Show bar graph.
89
    show_count
90
        Show numerical counts.
91
92
    The output file should be in line-buffered or unbuffered mode.
93
    """
94
    SPIN_CHARS = r'/-\|'
661 by Martin Pool
- limit rate at which progress bar is updated
95
    MIN_PAUSE = 0.1 # seconds
96
97
    start_time = None
98
    last_update = None
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
99
    
100
    def __init__(self,
101
                 to_file=sys.stderr,
102
                 show_pct=False,
103
                 show_spinner=False,
104
                 show_eta=True,
105
                 show_bar=True,
106
                 show_count=True):
649 by Martin Pool
- some cleanups for the progressbar method
107
        object.__init__(self)
108
        self.to_file = to_file
109
        self.suppressed = not _supports_progress(self.to_file)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
110
        self.spin_pos = 0
111
 
681 by Martin Pool
- assign missing fields in Progress object
112
        self.last_msg = None
113
        self.last_cnt = None
114
        self.last_total = None
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
115
        self.show_pct = show_pct
116
        self.show_spinner = show_spinner
117
        self.show_eta = show_eta
118
        self.show_bar = show_bar
119
        self.show_count = show_count
120
121
122
    def tick(self):
123
        self.update(self.last_msg, self.last_cnt, self.last_total)
124
                 
125
126
667 by Martin Pool
- allow for progressbar updates with no count, only a message
127
    def update(self, msg, current_cnt=None, total_cnt=None):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
128
        """Update and redraw progress bar."""
661 by Martin Pool
- limit rate at which progress bar is updated
129
        if self.suppressed:
130
            return
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
131
132
        # save these for the tick() function
133
        self.last_msg = msg
134
        self.last_cnt = current_cnt
135
        self.last_total = total_cnt
136
            
661 by Martin Pool
- limit rate at which progress bar is updated
137
        now = time.time()
138
        if self.start_time is None:
139
            self.start_time = now
140
        else:
141
            interval = now - self.last_update
142
            if interval > 0 and interval < self.MIN_PAUSE:
143
                return
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
144
661 by Martin Pool
- limit rate at which progress bar is updated
145
        self.last_update = now
146
        
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
147
        width = _width()
148
149
        if total_cnt:
150
            assert current_cnt <= total_cnt
151
        if current_cnt:
152
            assert current_cnt >= 0
153
        
154
        if self.show_eta and self.start_time and total_cnt:
155
            eta = get_eta(self.start_time, current_cnt, total_cnt)
156
            eta_str = " " + str_tdelta(eta)
157
        else:
158
            eta_str = ""
159
160
        if self.show_spinner:
161
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
162
        else:
163
            spin_str = ''
164
165
        # always update this; it's also used for the bar
166
        self.spin_pos += 1
167
168
        if self.show_pct and total_cnt and current_cnt:
169
            pct = 100.0 * current_cnt / total_cnt
170
            pct_str = ' (%5.1f%%)' % pct
171
        else:
172
            pct_str = ''
173
174
        if not self.show_count:
175
            count_str = ''
176
        elif current_cnt is None:
177
            count_str = ''
178
        elif total_cnt is None:
179
            count_str = ' %i' % (current_cnt)
180
        else:
181
            # make both fields the same size
182
            t = '%i' % (total_cnt)
183
            c = '%*i' % (len(t), current_cnt)
184
            count_str = ' ' + c + '/' + t 
185
186
        if self.show_bar:
187
            # progress bar, if present, soaks up all remaining space
188
            cols = width - 1 - len(msg) - len(spin_str) - len(pct_str) \
189
                   - len(eta_str) - len(count_str) - 3
190
191
            if total_cnt:
192
                # number of markers highlighted in bar
193
                markers = int(round(float(cols) * current_cnt / total_cnt))
194
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
195
            elif False:
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
196
                # don't know total, so can't show completion.
197
                # so just show an expanded spinning thingy
198
                m = self.spin_pos % cols
668 by Martin Pool
- fix sweeping bar progress indicator
199
                ms = (' ' * m + '*').ljust(cols)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
200
                
201
                bar_str = '[' + ms + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
202
            else:
203
                bar_str = ''
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
204
        else:
205
            bar_str = ''
206
207
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
208
209
        assert len(m) < width
210
        self.to_file.write('\r' + m.ljust(width - 1))
211
        #self.to_file.flush()
212
            
649 by Martin Pool
- some cleanups for the progressbar method
213
214
    def clear(self):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
215
        if self.suppressed:
216
            return
217
        
218
        self.to_file.write('\r%s\r' % (' ' * (_width() - 1)))
219
        #self.to_file.flush()        
649 by Martin Pool
- some cleanups for the progressbar method
220
    
221
648 by Martin Pool
- import aaron's progress-indicator code
222
        
223
def str_tdelta(delt):
224
    if delt is None:
225
        return "-:--:--"
660 by Martin Pool
- use plain unix time, not datetime module
226
    delt = int(round(delt))
227
    return '%d:%02d:%02d' % (delt/3600,
228
                             (delt/60) % 60,
229
                             delt % 60)
230
231
232
def get_eta(start_time, current, total, enough_samples=3):
233
    if start_time is None:
234
        return None
235
236
    if not total:
237
        return None
238
239
    if current < enough_samples:
240
        return None
241
242
    if current > total:
243
        return None                     # wtf?
244
245
    elapsed = time.time() - start_time
246
247
    if elapsed < 2.0:                   # not enough time to estimate
248
        return None
249
    
250
    total_duration = float(elapsed) * float(total) / float(current)
251
252
    assert total_duration >= elapsed
253
254
    return total_duration - elapsed
648 by Martin Pool
- import aaron's progress-indicator code
255
649 by Martin Pool
- some cleanups for the progressbar method
256
648 by Martin Pool
- import aaron's progress-indicator code
257
def run_tests():
258
    import doctest
259
    result = doctest.testmod()
260
    if result[1] > 0:
261
        if result[0] == 0:
262
            print "All tests passed"
263
    else:
264
        print "No tests to run"
649 by Martin Pool
- some cleanups for the progressbar method
265
266
267
def demo():
268
    from time import sleep
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
269
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
649 by Martin Pool
- some cleanups for the progressbar method
270
    for i in range(100):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
271
        pb.update('Elephanten', i, 99)
272
        sleep(0.1)
273
    sleep(2)
274
    pb.clear()
275
    sleep(1)
649 by Martin Pool
- some cleanups for the progressbar method
276
    print 'done!'
277
648 by Martin Pool
- import aaron's progress-indicator code
278
if __name__ == "__main__":
649 by Martin Pool
- some cleanups for the progressbar method
279
    demo()