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