~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
1104 by Martin Pool
- Add a simple UIFactory
97
98
99
100
class DummyProgress(_BaseProgressBar):
101
    """Progress-bar standin that does nothing.
102
103
    This can be used as the default argument for methods that
104
    take an optional progress indicator."""
105
    def tick(self):
106
        pass
107
108
    def update(self, msg=None, current=None, total=None):
109
        pass
110
111
    def clear(self):
112
        pass
964 by Martin Pool
- show progress on dumb terminals by printing dots
113
        
114
    
115
class DotsProgressBar(_BaseProgressBar):
116
    def __init__(self, **kwargs):
117
        _BaseProgressBar.__init__(self, **kwargs)
118
        self.last_msg = None
119
        self.need_nl = False
120
        
121
    def tick(self):
122
        self.update()
123
        
124
    def update(self, msg=None, current_cnt=None, total_cnt=None):
125
        if msg and msg != self.last_msg:
126
            if self.need_nl:
127
                self.to_file.write('\n')
128
            
129
            self.to_file.write(msg + ': ')
130
            self.last_msg = msg
131
        self.need_nl = True
132
        self.to_file.write('.')
133
        
134
    def clear(self):
135
        if self.need_nl:
136
            self.to_file.write('\n')
137
        
138
    
139
class TTYProgressBar(_BaseProgressBar):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
140
    """Progress bar display object.
141
142
    Several options are available to control the display.  These can
143
    be passed as parameters to the constructor or assigned at any time:
144
145
    show_pct
146
        Show percentage complete.
147
    show_spinner
148
        Show rotating baton.  This ticks over on every update even
149
        if the values don't change.
150
    show_eta
151
        Show predicted time-to-completion.
152
    show_bar
153
        Show bar graph.
154
    show_count
155
        Show numerical counts.
156
157
    The output file should be in line-buffered or unbuffered mode.
158
    """
159
    SPIN_CHARS = r'/-\|'
661 by Martin Pool
- limit rate at which progress bar is updated
160
    MIN_PAUSE = 0.1 # seconds
161
964 by Martin Pool
- show progress on dumb terminals by printing dots
162
163
    def __init__(self, **kwargs):
164
        _BaseProgressBar.__init__(self, **kwargs)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
165
        self.spin_pos = 0
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
166
        self.width = _width()
964 by Martin Pool
- show progress on dumb terminals by printing dots
167
        self.start_time = None
168
        self.last_update = None
169
    
170
171
    def throttle(self):
172
        """Return True if the bar was updated too recently"""
173
        now = time.time()
174
        if self.start_time is None:
175
            self.start_time = self.last_update = now
176
            return False
177
        else:
178
            interval = now - self.last_update
179
            if interval > 0 and interval < self.MIN_PAUSE:
180
                return True
181
182
        self.last_update = now
183
        return False
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
184
        
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
185
186
    def tick(self):
187
        self.update(self.last_msg, self.last_cnt, self.last_total)
188
                 
189
190
667 by Martin Pool
- allow for progressbar updates with no count, only a message
191
    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
192
        """Update and redraw progress bar."""
193
1308 by Martin Pool
- make progress bar more tolerant of out-of-range values
194
        if current_cnt < 0:
195
            current_cnt = 0
196
            
197
        if current_cnt > total_cnt:
198
            total_cnt = current_cnt
199
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
200
        # save these for the tick() function
201
        self.last_msg = msg
202
        self.last_cnt = current_cnt
203
        self.last_total = total_cnt
204
            
964 by Martin Pool
- show progress on dumb terminals by printing dots
205
        if self.throttle():
206
            return 
661 by Martin Pool
- limit rate at which progress bar is updated
207
        
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
208
        if self.show_eta and self.start_time and total_cnt:
209
            eta = get_eta(self.start_time, current_cnt, total_cnt)
210
            eta_str = " " + str_tdelta(eta)
211
        else:
212
            eta_str = ""
213
214
        if self.show_spinner:
215
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
216
        else:
217
            spin_str = ''
218
219
        # always update this; it's also used for the bar
220
        self.spin_pos += 1
221
222
        if self.show_pct and total_cnt and current_cnt:
223
            pct = 100.0 * current_cnt / total_cnt
224
            pct_str = ' (%5.1f%%)' % pct
225
        else:
226
            pct_str = ''
227
228
        if not self.show_count:
229
            count_str = ''
230
        elif current_cnt is None:
231
            count_str = ''
232
        elif total_cnt is None:
233
            count_str = ' %i' % (current_cnt)
234
        else:
235
            # make both fields the same size
236
            t = '%i' % (total_cnt)
237
            c = '%*i' % (len(t), current_cnt)
238
            count_str = ' ' + c + '/' + t 
239
240
        if self.show_bar:
241
            # progress bar, if present, soaks up all remaining space
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
242
            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
243
                   - len(eta_str) - len(count_str) - 3
244
245
            if total_cnt:
246
                # number of markers highlighted in bar
247
                markers = int(round(float(cols) * current_cnt / total_cnt))
248
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
249
            elif False:
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
250
                # don't know total, so can't show completion.
251
                # so just show an expanded spinning thingy
252
                m = self.spin_pos % cols
668 by Martin Pool
- fix sweeping bar progress indicator
253
                ms = (' ' * m + '*').ljust(cols)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
254
                
255
                bar_str = '[' + ms + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
256
            else:
257
                bar_str = ''
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
258
        else:
259
            bar_str = ''
260
261
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
262
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
263
        assert len(m) < self.width
264
        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
265
        #self.to_file.flush()
266
            
649 by Martin Pool
- some cleanups for the progressbar method
267
964 by Martin Pool
- show progress on dumb terminals by printing dots
268
    def clear(self):        
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
269
        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
270
        #self.to_file.flush()        
649 by Martin Pool
- some cleanups for the progressbar method
271
    
272
648 by Martin Pool
- import aaron's progress-indicator code
273
        
274
def str_tdelta(delt):
275
    if delt is None:
276
        return "-:--:--"
660 by Martin Pool
- use plain unix time, not datetime module
277
    delt = int(round(delt))
278
    return '%d:%02d:%02d' % (delt/3600,
279
                             (delt/60) % 60,
280
                             delt % 60)
281
282
283
def get_eta(start_time, current, total, enough_samples=3):
284
    if start_time is None:
285
        return None
286
287
    if not total:
288
        return None
289
290
    if current < enough_samples:
291
        return None
292
293
    if current > total:
294
        return None                     # wtf?
295
296
    elapsed = time.time() - start_time
297
298
    if elapsed < 2.0:                   # not enough time to estimate
299
        return None
300
    
301
    total_duration = float(elapsed) * float(total) / float(current)
302
303
    assert total_duration >= elapsed
304
305
    return total_duration - elapsed
648 by Martin Pool
- import aaron's progress-indicator code
306
649 by Martin Pool
- some cleanups for the progressbar method
307
648 by Martin Pool
- import aaron's progress-indicator code
308
def run_tests():
309
    import doctest
310
    result = doctest.testmod()
311
    if result[1] > 0:
312
        if result[0] == 0:
313
            print "All tests passed"
314
    else:
315
        print "No tests to run"
649 by Martin Pool
- some cleanups for the progressbar method
316
317
318
def demo():
964 by Martin Pool
- show progress on dumb terminals by printing dots
319
    sleep = time.sleep
320
    
321
    print 'dumb-terminal test:'
322
    pb = DotsProgressBar()
323
    for i in range(100):
324
        pb.update('Leoparden', i, 99)
325
        sleep(0.1)
326
    sleep(1.5)
327
    pb.clear()
328
    sleep(1.5)
329
    
330
    print 'smart-terminal test:'
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
331
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
649 by Martin Pool
- some cleanups for the progressbar method
332
    for i in range(100):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
333
        pb.update('Elephanten', i, 99)
334
        sleep(0.1)
335
    sleep(2)
336
    pb.clear()
337
    sleep(1)
964 by Martin Pool
- show progress on dumb terminals by printing dots
338
649 by Martin Pool
- some cleanups for the progressbar method
339
    print 'done!'
340
648 by Martin Pool
- import aaron's progress-indicator code
341
if __name__ == "__main__":
649 by Martin Pool
- some cleanups for the progressbar method
342
    demo()