/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

Update news and deprecated the old progress bar api.

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, 2006 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
import bzrlib.errors as errors
 
46
 
 
47
 
 
48
def _supports_progress(f):
 
49
    if not hasattr(f, 'isatty'):
 
50
        return False
 
51
    if not f.isatty():
 
52
        return False
 
53
    if os.environ.get('TERM') == 'dumb':
 
54
        # e.g. emacs compile window
 
55
        return False
 
56
    return True
 
57
 
 
58
 
 
59
 
 
60
def ProgressBar(to_file=sys.stderr, **kwargs):
 
61
    """Abstract factory"""
 
62
    if _supports_progress(to_file):
 
63
        return TTYProgressBar(to_file=to_file, **kwargs)
 
64
    else:
 
65
        return DotsProgressBar(to_file=to_file, **kwargs)
 
66
    
 
67
 
 
68
class ProgressBarStack(object):
 
69
    """A stack of progress bars."""
 
70
 
 
71
    def __init__(self,
 
72
                 to_file=sys.stderr,
 
73
                 show_pct=False,
 
74
                 show_spinner=False,
 
75
                 show_eta=True,
 
76
                 show_bar=True,
 
77
                 show_count=True,
 
78
                 to_messages_file=sys.stdout):
 
79
        """Setup the stack with the parameters the progress bars should have."""
 
80
        self._to_file = to_file
 
81
        self._show_pct = show_pct
 
82
        self._show_spinner = show_spinner
 
83
        self._show_eta = show_eta
 
84
        self._show_bar = show_bar
 
85
        self._show_count = show_count
 
86
        self._to_messages_file = to_messages_file
 
87
        self._stack = []
 
88
 
 
89
    def get_nested(self):
 
90
        """Return a nested progress bar."""
 
91
        # initial implementation - return a new bar each time.
 
92
        new_bar = TTYProgressBar(to_file=self._to_file,
 
93
                                 show_pct=self._show_pct,
 
94
                                 show_spinner=self._show_spinner,
 
95
                                 show_eta=self._show_eta,
 
96
                                 show_bar=self._show_bar,
 
97
                                 show_count=self._show_count,
 
98
                                 to_messages_file=self._to_messages_file,
 
99
                                 _stack=self)
 
100
        self._stack.append(new_bar)
 
101
        return new_bar
 
102
 
 
103
    def return_pb(self, bar):
 
104
        """Return bar after its been used."""
 
105
        if bar != self._stack[-1]:
 
106
            raise errors.MissingProgressBarFinish()
 
107
        self._stack.pop()
 
108
 
 
109
 
 
110
class _BaseProgressBar(object):
 
111
 
 
112
    def __init__(self,
 
113
                 to_file=sys.stderr,
 
114
                 show_pct=False,
 
115
                 show_spinner=False,
 
116
                 show_eta=True,
 
117
                 show_bar=True,
 
118
                 show_count=True,
 
119
                 to_messages_file=sys.stdout,
 
120
                 _stack=None):
 
121
        object.__init__(self)
 
122
        self.to_file = to_file
 
123
        self.to_messages_file = to_messages_file
 
124
        self.last_msg = None
 
125
        self.last_cnt = None
 
126
        self.last_total = None
 
127
        self.show_pct = show_pct
 
128
        self.show_spinner = show_spinner
 
129
        self.show_eta = show_eta
 
130
        self.show_bar = show_bar
 
131
        self.show_count = show_count
 
132
        self._stack = _stack
 
133
 
 
134
    def finished(self):
 
135
        """Return this bar to its progress stack."""
 
136
        self.clear()
 
137
        assert self._stack
 
138
        self._stack.return_pb(self)
 
139
 
 
140
    def note(self, fmt_string, *args, **kwargs):
 
141
        """Record a note without disrupting the progress bar."""
 
142
        self.clear()
 
143
        self.to_messages_file.write(fmt_string % args)
 
144
        self.to_messages_file.write('\n')
 
145
 
 
146
 
 
147
class DummyProgress(_BaseProgressBar):
 
148
    """Progress-bar standin that does nothing.
 
149
 
 
150
    This can be used as the default argument for methods that
 
151
    take an optional progress indicator."""
 
152
    def tick(self):
 
153
        pass
 
154
 
 
155
    def update(self, msg=None, current=None, total=None):
 
156
        pass
 
157
 
 
158
    def clear(self):
 
159
        pass
 
160
        
 
161
    def note(self, fmt_string, *args, **kwargs):
 
162
        """See _BaseProgressBar.note()."""
 
163
 
 
164
 
 
165
class DotsProgressBar(_BaseProgressBar):
 
166
    def __init__(self, **kwargs):
 
167
        _BaseProgressBar.__init__(self, **kwargs)
 
168
        self.last_msg = None
 
169
        self.need_nl = False
 
170
        
 
171
    def tick(self):
 
172
        self.update()
 
173
        
 
174
    def update(self, msg=None, current_cnt=None, total_cnt=None):
 
175
        if msg and msg != self.last_msg:
 
176
            if self.need_nl:
 
177
                self.to_file.write('\n')
 
178
            
 
179
            self.to_file.write(msg + ': ')
 
180
            self.last_msg = msg
 
181
        self.need_nl = True
 
182
        self.to_file.write('.')
 
183
        
 
184
    def clear(self):
 
185
        if self.need_nl:
 
186
            self.to_file.write('\n')
 
187
        
 
188
    
 
189
class TTYProgressBar(_BaseProgressBar):
 
190
    """Progress bar display object.
 
191
 
 
192
    Several options are available to control the display.  These can
 
193
    be passed as parameters to the constructor or assigned at any time:
 
194
 
 
195
    show_pct
 
196
        Show percentage complete.
 
197
    show_spinner
 
198
        Show rotating baton.  This ticks over on every update even
 
199
        if the values don't change.
 
200
    show_eta
 
201
        Show predicted time-to-completion.
 
202
    show_bar
 
203
        Show bar graph.
 
204
    show_count
 
205
        Show numerical counts.
 
206
 
 
207
    The output file should be in line-buffered or unbuffered mode.
 
208
    """
 
209
    SPIN_CHARS = r'/-\|'
 
210
    MIN_PAUSE = 0.1 # seconds
 
211
 
 
212
 
 
213
    def __init__(self, **kwargs):
 
214
        from bzrlib.osutils import terminal_width
 
215
        _BaseProgressBar.__init__(self, **kwargs)
 
216
        self.spin_pos = 0
 
217
        self.width = terminal_width()
 
218
        self.start_time = None
 
219
        self.last_update = None
 
220
        self.last_updates = deque()
 
221
    
 
222
 
 
223
    def throttle(self):
 
224
        """Return True if the bar was updated too recently"""
 
225
        now = time.time()
 
226
        if self.start_time is None:
 
227
            self.start_time = self.last_update = now
 
228
            return False
 
229
        else:
 
230
            interval = now - self.last_update
 
231
            if interval > 0 and interval < self.MIN_PAUSE:
 
232
                return True
 
233
 
 
234
        self.last_updates.append(now - self.last_update)
 
235
        self.last_update = now
 
236
        return False
 
237
        
 
238
 
 
239
    def tick(self):
 
240
        self.update(self.last_msg, self.last_cnt, self.last_total)
 
241
                 
 
242
 
 
243
 
 
244
    def update(self, msg, current_cnt=None, total_cnt=None):
 
245
        """Update and redraw progress bar."""
 
246
 
 
247
        if current_cnt < 0:
 
248
            current_cnt = 0
 
249
            
 
250
        if current_cnt > total_cnt:
 
251
            total_cnt = current_cnt
 
252
        
 
253
        old_msg = self.last_msg
 
254
        # save these for the tick() function
 
255
        self.last_msg = msg
 
256
        self.last_cnt = current_cnt
 
257
        self.last_total = total_cnt
 
258
            
 
259
        if old_msg == self.last_msg and self.throttle():
 
260
            return 
 
261
        
 
262
        if self.show_eta and self.start_time and total_cnt:
 
263
            eta = get_eta(self.start_time, current_cnt, total_cnt,
 
264
                    last_updates = self.last_updates)
 
265
            eta_str = " " + str_tdelta(eta)
 
266
        else:
 
267
            eta_str = ""
 
268
 
 
269
        if self.show_spinner:
 
270
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
 
271
        else:
 
272
            spin_str = ''
 
273
 
 
274
        # always update this; it's also used for the bar
 
275
        self.spin_pos += 1
 
276
 
 
277
        if self.show_pct and total_cnt and current_cnt:
 
278
            pct = 100.0 * current_cnt / total_cnt
 
279
            pct_str = ' (%5.1f%%)' % pct
 
280
        else:
 
281
            pct_str = ''
 
282
 
 
283
        if not self.show_count:
 
284
            count_str = ''
 
285
        elif current_cnt is None:
 
286
            count_str = ''
 
287
        elif total_cnt is None:
 
288
            count_str = ' %i' % (current_cnt)
 
289
        else:
 
290
            # make both fields the same size
 
291
            t = '%i' % (total_cnt)
 
292
            c = '%*i' % (len(t), current_cnt)
 
293
            count_str = ' ' + c + '/' + t 
 
294
 
 
295
        if self.show_bar:
 
296
            # progress bar, if present, soaks up all remaining space
 
297
            cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
 
298
                   - len(eta_str) - len(count_str) - 3
 
299
 
 
300
            if total_cnt:
 
301
                # number of markers highlighted in bar
 
302
                markers = int(round(float(cols) * current_cnt / total_cnt))
 
303
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
 
304
            elif False:
 
305
                # don't know total, so can't show completion.
 
306
                # so just show an expanded spinning thingy
 
307
                m = self.spin_pos % cols
 
308
                ms = (' ' * m + '*').ljust(cols)
 
309
                
 
310
                bar_str = '[' + ms + '] '
 
311
            else:
 
312
                bar_str = ''
 
313
        else:
 
314
            bar_str = ''
 
315
 
 
316
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
 
317
 
 
318
        assert len(m) < self.width
 
319
        self.to_file.write('\r' + m.ljust(self.width - 1))
 
320
        #self.to_file.flush()
 
321
            
 
322
    def clear(self):        
 
323
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
324
        #self.to_file.flush()        
 
325
 
 
326
        
 
327
def str_tdelta(delt):
 
328
    if delt is None:
 
329
        return "-:--:--"
 
330
    delt = int(round(delt))
 
331
    return '%d:%02d:%02d' % (delt/3600,
 
332
                             (delt/60) % 60,
 
333
                             delt % 60)
 
334
 
 
335
 
 
336
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
 
337
    if start_time is None:
 
338
        return None
 
339
 
 
340
    if not total:
 
341
        return None
 
342
 
 
343
    if current < enough_samples:
 
344
        return None
 
345
 
 
346
    if current > total:
 
347
        return None                     # wtf?
 
348
 
 
349
    elapsed = time.time() - start_time
 
350
 
 
351
    if elapsed < 2.0:                   # not enough time to estimate
 
352
        return None
 
353
    
 
354
    total_duration = float(elapsed) * float(total) / float(current)
 
355
 
 
356
    assert total_duration >= elapsed
 
357
 
 
358
    if last_updates and len(last_updates) >= n_recent:
 
359
        while len(last_updates) > n_recent:
 
360
            last_updates.popleft()
 
361
        avg = sum(last_updates) / float(len(last_updates))
 
362
        time_left = avg * (total - current)
 
363
 
 
364
        old_time_left = total_duration - elapsed
 
365
 
 
366
        # We could return the average, or some other value here
 
367
        return (time_left + old_time_left) / 2
 
368
 
 
369
    return total_duration - elapsed
 
370
 
 
371
 
 
372
def run_tests():
 
373
    import doctest
 
374
    result = doctest.testmod()
 
375
    if result[1] > 0:
 
376
        if result[0] == 0:
 
377
            print "All tests passed"
 
378
    else:
 
379
        print "No tests to run"
 
380
 
 
381
 
 
382
def demo():
 
383
    sleep = time.sleep
 
384
    
 
385
    print 'dumb-terminal test:'
 
386
    pb = DotsProgressBar()
 
387
    for i in range(100):
 
388
        pb.update('Leoparden', i, 99)
 
389
        sleep(0.1)
 
390
    sleep(1.5)
 
391
    pb.clear()
 
392
    sleep(1.5)
 
393
    
 
394
    print 'smart-terminal test:'
 
395
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
 
396
    for i in range(100):
 
397
        pb.update('Elephanten', i, 99)
 
398
        sleep(0.1)
 
399
    sleep(2)
 
400
    pb.clear()
 
401
    sleep(1)
 
402
 
 
403
    print 'done!'
 
404
 
 
405
if __name__ == "__main__":
 
406
    demo()