~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
889 by Martin Pool
- show progress bar during inventory conversion to weave, and make profiling optional
19
"""Simple text-mode progress indicator.
649 by Martin Pool
- some cleanups for the progressbar method
20
21
To display an indicator, create a ProgressBar object.  Call it,
22
passing Progress objects indicating the current state.  When done,
23
call clear().
24
25
Progress is suppressed when output is not sent to a terminal, so as
26
not to clutter log files.
27
"""
28
652 by Martin Pool
doc
29
# TODO: should be a global option e.g. --silent that disables progress
30
# indicators, preferably without needing to adjust all code that
31
# potentially calls them.
32
962 by Martin Pool
todo
33
# TODO: If not on a tty perhaps just print '......' for the benefit of IDEs, etc
655 by Martin Pool
- better calculation of progress bar position
34
934 by Martin Pool
todo
35
# TODO: Optionally show elapsed time instead/as well as ETA; nicer
36
# when the rate is unpredictable
37
649 by Martin Pool
- some cleanups for the progressbar method
38
648 by Martin Pool
- import aaron's progress-indicator code
39
import sys
660 by Martin Pool
- use plain unix time, not datetime module
40
import time
964 by Martin Pool
- show progress on dumb terminals by printing dots
41
import os
648 by Martin Pool
- import aaron's progress-indicator code
42
649 by Martin Pool
- some cleanups for the progressbar method
43
44
def _width():
45
    """Return estimated terminal width.
46
47
    TODO: Do something smart on Windows?
48
49
    TODO: Is there anything that gets a better update when the window
50
          is resized while the program is running?
51
    """
52
    try:
53
        return int(os.environ['COLUMNS'])
54
    except (IndexError, KeyError, ValueError):
55
        return 80
56
57
58
def _supports_progress(f):
695 by Martin Pool
- don't display progress bars on really dumb terminals
59
    if not hasattr(f, 'isatty'):
60
        return False
61
    if not f.isatty():
62
        return False
63
    if os.environ.get('TERM') == 'dumb':
64
        # e.g. emacs compile window
65
        return False
66
    return True
649 by Martin Pool
- some cleanups for the progressbar method
67
68
69
964 by Martin Pool
- show progress on dumb terminals by printing dots
70
def ProgressBar(to_file=sys.stderr, **kwargs):
71
    """Abstract factory"""
72
    if _supports_progress(to_file):
73
        return TTYProgressBar(to_file=to_file, **kwargs)
74
    else:
75
        return DotsProgressBar(to_file=to_file, **kwargs)
76
    
77
    
78
class _BaseProgressBar(object):
79
    def __init__(self,
80
                 to_file=sys.stderr,
81
                 show_pct=False,
82
                 show_spinner=False,
83
                 show_eta=True,
84
                 show_bar=True,
85
                 show_count=True):
86
        object.__init__(self)
87
        self.to_file = to_file
88
 
89
        self.last_msg = None
90
        self.last_cnt = None
91
        self.last_total = None
92
        self.show_pct = show_pct
93
        self.show_spinner = show_spinner
94
        self.show_eta = show_eta
95
        self.show_bar = show_bar
96
        self.show_count = show_count
97
        
98
        
99
    
100
class DotsProgressBar(_BaseProgressBar):
101
    def __init__(self, **kwargs):
102
        _BaseProgressBar.__init__(self, **kwargs)
103
        self.last_msg = None
104
        self.need_nl = False
105
        
106
    def tick(self):
107
        self.update()
108
        
109
    def update(self, msg=None, current_cnt=None, total_cnt=None):
110
        if msg and msg != self.last_msg:
111
            if self.need_nl:
112
                self.to_file.write('\n')
113
            
114
            self.to_file.write(msg + ': ')
115
            self.last_msg = msg
116
        self.need_nl = True
117
        self.to_file.write('.')
118
        
119
    def clear(self):
120
        if self.need_nl:
121
            self.to_file.write('\n')
122
        
123
    
124
class TTYProgressBar(_BaseProgressBar):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
125
    """Progress bar display object.
126
127
    Several options are available to control the display.  These can
128
    be passed as parameters to the constructor or assigned at any time:
129
130
    show_pct
131
        Show percentage complete.
132
    show_spinner
133
        Show rotating baton.  This ticks over on every update even
134
        if the values don't change.
135
    show_eta
136
        Show predicted time-to-completion.
137
    show_bar
138
        Show bar graph.
139
    show_count
140
        Show numerical counts.
141
142
    The output file should be in line-buffered or unbuffered mode.
143
    """
144
    SPIN_CHARS = r'/-\|'
661 by Martin Pool
- limit rate at which progress bar is updated
145
    MIN_PAUSE = 0.1 # seconds
146
964 by Martin Pool
- show progress on dumb terminals by printing dots
147
148
    def __init__(self, **kwargs):
149
        _BaseProgressBar.__init__(self, **kwargs)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
150
        self.spin_pos = 0
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
151
        self.width = _width()
964 by Martin Pool
- show progress on dumb terminals by printing dots
152
        self.start_time = None
153
        self.last_update = None
154
    
155
156
    def throttle(self):
157
        """Return True if the bar was updated too recently"""
158
        now = time.time()
159
        if self.start_time is None:
160
            self.start_time = self.last_update = now
161
            return False
162
        else:
163
            interval = now - self.last_update
164
            if interval > 0 and interval < self.MIN_PAUSE:
165
                return True
166
167
        self.last_update = now
168
        return False
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
169
        
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
170
171
    def tick(self):
172
        self.update(self.last_msg, self.last_cnt, self.last_total)
173
                 
174
175
667 by Martin Pool
- allow for progressbar updates with no count, only a message
176
    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
177
        """Update and redraw progress bar."""
178
179
        # save these for the tick() function
180
        self.last_msg = msg
181
        self.last_cnt = current_cnt
182
        self.last_total = total_cnt
183
            
964 by Martin Pool
- show progress on dumb terminals by printing dots
184
        if self.throttle():
185
            return 
661 by Martin Pool
- limit rate at which progress bar is updated
186
        
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
187
        if total_cnt:
188
            assert current_cnt <= total_cnt
189
        if current_cnt:
190
            assert current_cnt >= 0
191
        
192
        if self.show_eta and self.start_time and total_cnt:
193
            eta = get_eta(self.start_time, current_cnt, total_cnt)
194
            eta_str = " " + str_tdelta(eta)
195
        else:
196
            eta_str = ""
197
198
        if self.show_spinner:
199
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
200
        else:
201
            spin_str = ''
202
203
        # always update this; it's also used for the bar
204
        self.spin_pos += 1
205
206
        if self.show_pct and total_cnt and current_cnt:
207
            pct = 100.0 * current_cnt / total_cnt
208
            pct_str = ' (%5.1f%%)' % pct
209
        else:
210
            pct_str = ''
211
212
        if not self.show_count:
213
            count_str = ''
214
        elif current_cnt is None:
215
            count_str = ''
216
        elif total_cnt is None:
217
            count_str = ' %i' % (current_cnt)
218
        else:
219
            # make both fields the same size
220
            t = '%i' % (total_cnt)
221
            c = '%*i' % (len(t), current_cnt)
222
            count_str = ' ' + c + '/' + t 
223
224
        if self.show_bar:
225
            # progress bar, if present, soaks up all remaining space
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
226
            cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
227
                   - len(eta_str) - len(count_str) - 3
228
229
            if total_cnt:
230
                # number of markers highlighted in bar
231
                markers = int(round(float(cols) * current_cnt / total_cnt))
232
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
233
            elif False:
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
234
                # don't know total, so can't show completion.
235
                # so just show an expanded spinning thingy
236
                m = self.spin_pos % cols
668 by Martin Pool
- fix sweeping bar progress indicator
237
                ms = (' ' * m + '*').ljust(cols)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
238
                
239
                bar_str = '[' + ms + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
240
            else:
241
                bar_str = ''
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
242
        else:
243
            bar_str = ''
244
245
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
246
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
247
        assert len(m) < self.width
248
        self.to_file.write('\r' + m.ljust(self.width - 1))
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
249
        #self.to_file.flush()
250
            
649 by Martin Pool
- some cleanups for the progressbar method
251
964 by Martin Pool
- show progress on dumb terminals by printing dots
252
    def clear(self):        
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
253
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
254
        #self.to_file.flush()        
649 by Martin Pool
- some cleanups for the progressbar method
255
    
256
648 by Martin Pool
- import aaron's progress-indicator code
257
        
258
def str_tdelta(delt):
259
    if delt is None:
260
        return "-:--:--"
660 by Martin Pool
- use plain unix time, not datetime module
261
    delt = int(round(delt))
262
    return '%d:%02d:%02d' % (delt/3600,
263
                             (delt/60) % 60,
264
                             delt % 60)
265
266
267
def get_eta(start_time, current, total, enough_samples=3):
268
    if start_time is None:
269
        return None
270
271
    if not total:
272
        return None
273
274
    if current < enough_samples:
275
        return None
276
277
    if current > total:
278
        return None                     # wtf?
279
280
    elapsed = time.time() - start_time
281
282
    if elapsed < 2.0:                   # not enough time to estimate
283
        return None
284
    
285
    total_duration = float(elapsed) * float(total) / float(current)
286
287
    assert total_duration >= elapsed
288
289
    return total_duration - elapsed
648 by Martin Pool
- import aaron's progress-indicator code
290
649 by Martin Pool
- some cleanups for the progressbar method
291
648 by Martin Pool
- import aaron's progress-indicator code
292
def run_tests():
293
    import doctest
294
    result = doctest.testmod()
295
    if result[1] > 0:
296
        if result[0] == 0:
297
            print "All tests passed"
298
    else:
299
        print "No tests to run"
649 by Martin Pool
- some cleanups for the progressbar method
300
301
302
def demo():
964 by Martin Pool
- show progress on dumb terminals by printing dots
303
    sleep = time.sleep
304
    
305
    print 'dumb-terminal test:'
306
    pb = DotsProgressBar()
307
    for i in range(100):
308
        pb.update('Leoparden', i, 99)
309
        sleep(0.1)
310
    sleep(1.5)
311
    pb.clear()
312
    sleep(1.5)
313
    
314
    print 'smart-terminal test:'
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
315
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
649 by Martin Pool
- some cleanups for the progressbar method
316
    for i in range(100):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
317
        pb.update('Elephanten', i, 99)
318
        sleep(0.1)
319
    sleep(2)
320
    pb.clear()
321
    sleep(1)
964 by Martin Pool
- show progress on dumb terminals by printing dots
322
649 by Martin Pool
- some cleanups for the progressbar method
323
    print 'done!'
324
648 by Martin Pool
- import aaron's progress-indicator code
325
if __name__ == "__main__":
649 by Martin Pool
- some cleanups for the progressbar method
326
    demo()