/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/progress.py

 * The internal storage of history, and logical branch identity have now
   been split into Branch, and Repository. The common locking and file 
   management routines are now in bzrlib.lockablefiles. 
   (Aaron Bentley, Robert Collins, Martin Pool)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
 
2
# Copyright (C) 2005 Canonical <canonical.com>
 
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
 
 
18
 
 
19
"""Simple text-mode progress indicator.
 
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
 
 
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
 
 
33
# TODO: If not on a tty perhaps just print '......' for the benefit of IDEs, etc
 
34
 
 
35
# TODO: Optionally show elapsed time instead/as well as ETA; nicer
 
36
# when the rate is unpredictable
 
37
 
 
38
 
 
39
import sys
 
40
import time
 
41
import os
 
42
from collections import deque
 
43
 
 
44
 
 
45
def _supports_progress(f):
 
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
 
54
 
 
55
 
 
56
 
 
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
 
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
 
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):
 
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'/-\|'
 
147
    MIN_PAUSE = 0.1 # seconds
 
148
 
 
149
 
 
150
    def __init__(self, **kwargs):
 
151
        from bzrlib.osutils import terminal_width
 
152
        _BaseProgressBar.__init__(self, **kwargs)
 
153
        self.spin_pos = 0
 
154
        self.width = terminal_width()
 
155
        self.start_time = None
 
156
        self.last_update = None
 
157
        self.last_updates = deque()
 
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
 
 
171
        self.last_updates.append(now - self.last_update)
 
172
        self.last_update = now
 
173
        return False
 
174
        
 
175
 
 
176
    def tick(self):
 
177
        self.update(self.last_msg, self.last_cnt, self.last_total)
 
178
                 
 
179
 
 
180
 
 
181
    def update(self, msg, current_cnt=None, total_cnt=None):
 
182
        """Update and redraw progress bar."""
 
183
 
 
184
        if current_cnt < 0:
 
185
            current_cnt = 0
 
186
            
 
187
        if current_cnt > total_cnt:
 
188
            total_cnt = current_cnt
 
189
 
 
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
            
 
195
        if self.throttle():
 
196
            return 
 
197
        
 
198
        if self.show_eta and self.start_time and total_cnt:
 
199
            eta = get_eta(self.start_time, current_cnt, total_cnt,
 
200
                    last_updates = self.last_updates)
 
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
 
233
            cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
 
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) + '] '
 
240
            elif False:
 
241
                # don't know total, so can't show completion.
 
242
                # so just show an expanded spinning thingy
 
243
                m = self.spin_pos % cols
 
244
                ms = (' ' * m + '*').ljust(cols)
 
245
                
 
246
                bar_str = '[' + ms + '] '
 
247
            else:
 
248
                bar_str = ''
 
249
        else:
 
250
            bar_str = ''
 
251
 
 
252
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
 
253
 
 
254
        assert len(m) < self.width
 
255
        self.to_file.write('\r' + m.ljust(self.width - 1))
 
256
        #self.to_file.flush()
 
257
            
 
258
 
 
259
    def clear(self):        
 
260
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
261
        #self.to_file.flush()        
 
262
    
 
263
 
 
264
        
 
265
def str_tdelta(delt):
 
266
    if delt is None:
 
267
        return "-:--:--"
 
268
    delt = int(round(delt))
 
269
    return '%d:%02d:%02d' % (delt/3600,
 
270
                             (delt/60) % 60,
 
271
                             delt % 60)
 
272
 
 
273
 
 
274
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
 
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
 
 
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
 
 
307
    return total_duration - elapsed
 
308
 
 
309
 
 
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"
 
318
 
 
319
 
 
320
def demo():
 
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:'
 
333
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
 
334
    for i in range(100):
 
335
        pb.update('Elephanten', i, 99)
 
336
        sleep(0.1)
 
337
    sleep(2)
 
338
    pb.clear()
 
339
    sleep(1)
 
340
 
 
341
    print 'done!'
 
342
 
 
343
if __name__ == "__main__":
 
344
    demo()