/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

  • Committer: Aaron Bentley
  • Date: 2006-03-18 21:02:22 UTC
  • mto: (1558.7.5 Aaron's integration)
  • mto: This revision was merged to the branch mainline in revision 1629.
  • Revision ID: aaron.bentley@utoronto.ca-20060318210222-8a49d67ada6f8638
Fix overall progress bar's interaction with 'note' and 'warning'

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