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