~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/progress.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-10-02 00:43:10 UTC
  • mfrom: (2057.1.1 bzr.dev)
  • Revision ID: pqm@pqm.ubuntu.com-20061002004310-6e09ddd7fd28f71c
Merge in 0.11 NEWS entry.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
2
 
# Copyright (C) 2005 Canonical <canonical.com>
 
2
# Copyright (C) 2005, 2006 Canonical <canonical.com>
3
3
#
4
4
#    This program is free software; you can redistribute it and/or modify
5
5
#    it under the terms of the GNU General Public License as published by
39
39
import sys
40
40
import time
41
41
import os
42
 
from collections import deque
43
 
 
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
 
42
 
 
43
import bzrlib.errors as errors
 
44
from bzrlib.trace import mutter
57
45
 
58
46
 
59
47
def _supports_progress(f):
60
 
    if not hasattr(f, 'isatty'):
 
48
    isatty = getattr(f, 'isatty', None)
 
49
    if isatty is None:
61
50
        return False
62
 
    if not f.isatty():
 
51
    if not isatty():
63
52
        return False
64
53
    if os.environ.get('TERM') == 'dumb':
65
54
        # e.g. emacs compile window
67
56
    return True
68
57
 
69
58
 
70
 
 
71
 
def ProgressBar(to_file=sys.stderr, **kwargs):
 
59
_progress_bar_types = {}
 
60
 
 
61
 
 
62
def ProgressBar(to_file=None, **kwargs):
72
63
    """Abstract factory"""
73
 
    if _supports_progress(to_file):
74
 
        return TTYProgressBar(to_file=to_file, **kwargs)
 
64
    if to_file is None:
 
65
        to_file = sys.stderr
 
66
    requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
 
67
    # An value of '' or not set reverts to standard processing
 
68
    if requested_bar_type in (None, ''):
 
69
        if _supports_progress(to_file):
 
70
            return TTYProgressBar(to_file=to_file, **kwargs)
 
71
        else:
 
72
            return DotsProgressBar(to_file=to_file, **kwargs)
75
73
    else:
76
 
        return DotsProgressBar(to_file=to_file, **kwargs)
77
 
    
78
 
    
 
74
        # Minor sanitation to prevent spurious errors
 
75
        requested_bar_type = requested_bar_type.lower().strip()
 
76
        # TODO: jam 20060710 Arguably we shouldn't raise an exception
 
77
        #       but should instead just disable progress bars if we
 
78
        #       don't recognize the type
 
79
        if requested_bar_type not in _progress_bar_types:
 
80
            raise errors.InvalidProgressBarType(requested_bar_type,
 
81
                                                _progress_bar_types.keys())
 
82
        return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
 
83
 
 
84
 
 
85
class ProgressBarStack(object):
 
86
    """A stack of progress bars."""
 
87
 
 
88
    def __init__(self,
 
89
                 to_file=None,
 
90
                 show_pct=False,
 
91
                 show_spinner=True,
 
92
                 show_eta=False,
 
93
                 show_bar=True,
 
94
                 show_count=True,
 
95
                 to_messages_file=None,
 
96
                 klass=None):
 
97
        """Setup the stack with the parameters the progress bars should have."""
 
98
        if to_file is None:
 
99
            to_file = sys.stderr
 
100
        if to_messages_file is None:
 
101
            to_messages_file = sys.stdout
 
102
        self._to_file = to_file
 
103
        self._show_pct = show_pct
 
104
        self._show_spinner = show_spinner
 
105
        self._show_eta = show_eta
 
106
        self._show_bar = show_bar
 
107
        self._show_count = show_count
 
108
        self._to_messages_file = to_messages_file
 
109
        self._stack = []
 
110
        self._klass = klass or ProgressBar
 
111
 
 
112
    def top(self):
 
113
        if len(self._stack) != 0:
 
114
            return self._stack[-1]
 
115
        else:
 
116
            return None
 
117
 
 
118
    def bottom(self):
 
119
        if len(self._stack) != 0:
 
120
            return self._stack[0]
 
121
        else:
 
122
            return None
 
123
 
 
124
    def get_nested(self):
 
125
        """Return a nested progress bar."""
 
126
        if len(self._stack) == 0:
 
127
            func = self._klass
 
128
        else:
 
129
            func = self.top().child_progress
 
130
        new_bar = func(to_file=self._to_file,
 
131
                       show_pct=self._show_pct,
 
132
                       show_spinner=self._show_spinner,
 
133
                       show_eta=self._show_eta,
 
134
                       show_bar=self._show_bar,
 
135
                       show_count=self._show_count,
 
136
                       to_messages_file=self._to_messages_file,
 
137
                       _stack=self)
 
138
        self._stack.append(new_bar)
 
139
        return new_bar
 
140
 
 
141
    def return_pb(self, bar):
 
142
        """Return bar after its been used."""
 
143
        if bar is not self._stack[-1]:
 
144
            raise errors.MissingProgressBarFinish()
 
145
        self._stack.pop()
 
146
 
 
147
 
79
148
class _BaseProgressBar(object):
 
149
 
80
150
    def __init__(self,
81
 
                 to_file=sys.stderr,
 
151
                 to_file=None,
82
152
                 show_pct=False,
83
153
                 show_spinner=False,
84
 
                 show_eta=True,
 
154
                 show_eta=False,
85
155
                 show_bar=True,
86
 
                 show_count=True):
 
156
                 show_count=True,
 
157
                 to_messages_file=None,
 
158
                 _stack=None):
87
159
        object.__init__(self)
 
160
        if to_file is None:
 
161
            to_file = sys.stderr
 
162
        if to_messages_file is None:
 
163
            to_messages_file = sys.stdout
88
164
        self.to_file = to_file
89
 
 
 
165
        self.to_messages_file = to_messages_file
90
166
        self.last_msg = None
91
167
        self.last_cnt = None
92
168
        self.last_total = None
95
171
        self.show_eta = show_eta
96
172
        self.show_bar = show_bar
97
173
        self.show_count = show_count
98
 
 
 
174
        self._stack = _stack
 
175
        # seed throttler
 
176
        self.MIN_PAUSE = 0.1 # seconds
 
177
        now = time.clock()
 
178
        # starting now
 
179
        self.start_time = now
 
180
        # next update should not throttle
 
181
        self.last_update = now - self.MIN_PAUSE - 1
 
182
 
 
183
    def finished(self):
 
184
        """Return this bar to its progress stack."""
 
185
        self.clear()
 
186
        assert self._stack is not None
 
187
        self._stack.return_pb(self)
 
188
 
 
189
    def note(self, fmt_string, *args, **kwargs):
 
190
        """Record a note without disrupting the progress bar."""
 
191
        self.clear()
 
192
        self.to_messages_file.write(fmt_string % args)
 
193
        self.to_messages_file.write('\n')
 
194
 
 
195
    def child_progress(self, **kwargs):
 
196
        return ChildProgress(**kwargs)
99
197
 
100
198
 
101
199
class DummyProgress(_BaseProgressBar):
109
207
    def update(self, msg=None, current=None, total=None):
110
208
        pass
111
209
 
 
210
    def child_update(self, message, current, total):
 
211
        pass
 
212
 
112
213
    def clear(self):
113
214
        pass
114
215
        
115
 
    
 
216
    def note(self, fmt_string, *args, **kwargs):
 
217
        """See _BaseProgressBar.note()."""
 
218
 
 
219
    def child_progress(self, **kwargs):
 
220
        return DummyProgress(**kwargs)
 
221
 
 
222
 
 
223
_progress_bar_types['dummy'] = DummyProgress
 
224
_progress_bar_types['none'] = DummyProgress
 
225
 
 
226
 
116
227
class DotsProgressBar(_BaseProgressBar):
 
228
 
117
229
    def __init__(self, **kwargs):
118
230
        _BaseProgressBar.__init__(self, **kwargs)
119
231
        self.last_msg = None
126
238
        if msg and msg != self.last_msg:
127
239
            if self.need_nl:
128
240
                self.to_file.write('\n')
129
 
            
130
241
            self.to_file.write(msg + ': ')
131
242
            self.last_msg = msg
132
243
        self.need_nl = True
135
246
    def clear(self):
136
247
        if self.need_nl:
137
248
            self.to_file.write('\n')
 
249
        self.need_nl = False
138
250
        
 
251
    def child_update(self, message, current, total):
 
252
        self.tick()
 
253
 
 
254
 
 
255
_progress_bar_types['dots'] = DotsProgressBar
 
256
 
139
257
    
140
258
class TTYProgressBar(_BaseProgressBar):
141
259
    """Progress bar display object.
158
276
    The output file should be in line-buffered or unbuffered mode.
159
277
    """
160
278
    SPIN_CHARS = r'/-\|'
161
 
    MIN_PAUSE = 0.1 # seconds
162
279
 
163
280
 
164
281
    def __init__(self, **kwargs):
 
282
        from bzrlib.osutils import terminal_width
165
283
        _BaseProgressBar.__init__(self, **kwargs)
166
284
        self.spin_pos = 0
167
 
        self.width = _width()
168
 
        self.start_time = None
169
 
        self.last_update = None
170
 
        self.last_updates = deque()
 
285
        self.width = terminal_width()
 
286
        self.last_updates = []
 
287
        self._max_last_updates = 10
 
288
        self.child_fraction = 0
 
289
        self._have_output = False
171
290
    
172
291
 
173
 
    def throttle(self):
 
292
    def throttle(self, old_msg):
174
293
        """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
 
294
        # time.time consistently takes 40/4000 ms = 0.01 ms.
 
295
        # but every single update to the pb invokes it.
 
296
        # so we use time.clock which takes 20/4000 ms = 0.005ms
 
297
        # on the downside, time.clock() appears to have approximately
 
298
        # 10ms granularity, so we treat a zero-time change as 'throttled.'
 
299
        now = time.clock()
 
300
        if self.start_time is not None and (now - self.start_time) < 1:
 
301
            return True
 
302
        if old_msg != self.last_msg:
178
303
            return False
179
 
        else:
180
 
            interval = now - self.last_update
181
 
            if interval > 0 and interval < self.MIN_PAUSE:
182
 
                return True
 
304
        interval = now - self.last_update
 
305
        # if interval > 0
 
306
        if interval < self.MIN_PAUSE:
 
307
            return True
183
308
 
184
309
        self.last_updates.append(now - self.last_update)
 
310
        # Don't let the queue grow without bound
 
311
        self.last_updates = self.last_updates[-self._max_last_updates:]
185
312
        self.last_update = now
186
313
        return False
187
314
        
188
 
 
189
315
    def tick(self):
190
 
        self.update(self.last_msg, self.last_cnt, self.last_total)
191
 
                 
192
 
 
193
 
 
194
 
    def update(self, msg, current_cnt=None, total_cnt=None):
 
316
        self.update(self.last_msg, self.last_cnt, self.last_total, 
 
317
                    self.child_fraction)
 
318
 
 
319
    def child_update(self, message, current, total):
 
320
        if current is not None and total != 0:
 
321
            child_fraction = float(current) / total
 
322
            if self.last_cnt is None:
 
323
                pass
 
324
            elif self.last_cnt + child_fraction <= self.last_total:
 
325
                self.child_fraction = child_fraction
 
326
            else:
 
327
                mutter('not updating child fraction')
 
328
        if self.last_msg is None:
 
329
            self.last_msg = ''
 
330
        self.tick()
 
331
 
 
332
    def update(self, msg, current_cnt=None, total_cnt=None, 
 
333
               child_fraction=0):
195
334
        """Update and redraw progress bar."""
 
335
        if msg is None:
 
336
            msg = self.last_msg
 
337
 
 
338
        if total_cnt is None:
 
339
            total_cnt = self.last_total
196
340
 
197
341
        if current_cnt < 0:
198
342
            current_cnt = 0
199
343
            
200
344
        if current_cnt > total_cnt:
201
345
            total_cnt = current_cnt
 
346
        
 
347
        ## # optional corner case optimisation 
 
348
        ## # currently does not seem to fire so costs more than saved.
 
349
        ## # trivial optimal case:
 
350
        ## # NB if callers are doing a clear and restore with
 
351
        ## # the saved values, this will prevent that:
 
352
        ## # in that case add a restore method that calls
 
353
        ## # _do_update or some such
 
354
        ## if (self.last_msg == msg and
 
355
        ##     self.last_cnt == current_cnt and
 
356
        ##     self.last_total == total_cnt and
 
357
        ##     self.child_fraction == child_fraction):
 
358
        ##     return
202
359
 
 
360
        old_msg = self.last_msg
203
361
        # save these for the tick() function
204
362
        self.last_msg = msg
205
363
        self.last_cnt = current_cnt
206
364
        self.last_total = total_cnt
207
 
            
208
 
        if self.throttle():
209
 
            return 
210
 
        
211
 
        if self.show_eta and self.start_time and total_cnt:
212
 
            eta = get_eta(self.start_time, current_cnt, total_cnt,
213
 
                    last_updates = self.last_updates)
 
365
        self.child_fraction = child_fraction
 
366
 
 
367
        # each function call takes 20ms/4000 = 0.005 ms, 
 
368
        # but multiple that by 4000 calls -> starts to cost.
 
369
        # so anything to make this function call faster
 
370
        # will improve base 'diff' time by up to 0.1 seconds.
 
371
        if self.throttle(old_msg):
 
372
            return
 
373
 
 
374
        if self.show_eta and self.start_time and self.last_total:
 
375
            eta = get_eta(self.start_time, self.last_cnt + self.child_fraction, 
 
376
                    self.last_total, last_updates = self.last_updates)
214
377
            eta_str = " " + str_tdelta(eta)
215
378
        else:
216
379
            eta_str = ""
223
386
        # always update this; it's also used for the bar
224
387
        self.spin_pos += 1
225
388
 
226
 
        if self.show_pct and total_cnt and current_cnt:
227
 
            pct = 100.0 * current_cnt / total_cnt
 
389
        if self.show_pct and self.last_total and self.last_cnt:
 
390
            pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
228
391
            pct_str = ' (%5.1f%%)' % pct
229
392
        else:
230
393
            pct_str = ''
231
394
 
232
395
        if not self.show_count:
233
396
            count_str = ''
234
 
        elif current_cnt is None:
 
397
        elif self.last_cnt is None:
235
398
            count_str = ''
236
 
        elif total_cnt is None:
237
 
            count_str = ' %i' % (current_cnt)
 
399
        elif self.last_total is None:
 
400
            count_str = ' %i' % (self.last_cnt)
238
401
        else:
239
402
            # make both fields the same size
240
 
            t = '%i' % (total_cnt)
241
 
            c = '%*i' % (len(t), current_cnt)
 
403
            t = '%i' % (self.last_total)
 
404
            c = '%*i' % (len(t), self.last_cnt)
242
405
            count_str = ' ' + c + '/' + t 
243
406
 
244
407
        if self.show_bar:
245
408
            # progress bar, if present, soaks up all remaining space
246
 
            cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
 
409
            cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
247
410
                   - len(eta_str) - len(count_str) - 3
248
411
 
249
 
            if total_cnt:
 
412
            if self.last_total:
250
413
                # number of markers highlighted in bar
251
 
                markers = int(round(float(cols) * current_cnt / total_cnt))
 
414
                markers = int(round(float(cols) * 
 
415
                              (self.last_cnt + self.child_fraction) / self.last_total))
252
416
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
253
417
            elif False:
254
418
                # don't know total, so can't show completion.
262
426
        else:
263
427
            bar_str = ''
264
428
 
265
 
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
 
429
        m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
266
430
 
267
431
        assert len(m) < self.width
268
432
        self.to_file.write('\r' + m.ljust(self.width - 1))
 
433
        self._have_output = True
269
434
        #self.to_file.flush()
270
435
            
271
 
 
272
436
    def clear(self):        
273
 
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
437
        if self._have_output:
 
438
            self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
439
        self._have_output = False
274
440
        #self.to_file.flush()        
275
 
    
276
 
 
277
 
        
 
441
 
 
442
 
 
443
_progress_bar_types['tty'] = TTYProgressBar
 
444
 
 
445
 
 
446
class ChildProgress(_BaseProgressBar):
 
447
    """A progress indicator that pushes its data to the parent"""
 
448
 
 
449
    def __init__(self, _stack, **kwargs):
 
450
        _BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
 
451
        self.parent = _stack.top()
 
452
        self.current = None
 
453
        self.total = None
 
454
        self.child_fraction = 0
 
455
        self.message = None
 
456
 
 
457
    def update(self, msg, current_cnt=None, total_cnt=None):
 
458
        self.current = current_cnt
 
459
        self.total = total_cnt
 
460
        self.message = msg
 
461
        self.child_fraction = 0
 
462
        self.tick()
 
463
 
 
464
    def child_update(self, message, current, total):
 
465
        if current is None or total == 0:
 
466
            self.child_fraction = 0
 
467
        else:
 
468
            self.child_fraction = float(current) / total
 
469
        self.tick()
 
470
 
 
471
    def tick(self):
 
472
        if self.current is None:
 
473
            count = None
 
474
        else:
 
475
            count = self.current+self.child_fraction
 
476
            if count > self.total:
 
477
                if __debug__:
 
478
                    mutter('clamping count of %d to %d' % (count, self.total))
 
479
                count = self.total
 
480
        self.parent.child_update(self.message, count, self.total)
 
481
 
 
482
    def clear(self):
 
483
        pass
 
484
 
 
485
    def note(self, *args, **kwargs):
 
486
        self.parent.note(*args, **kwargs)
 
487
 
 
488
 
278
489
def str_tdelta(delt):
279
490
    if delt is None:
280
491
        return "-:--:--"
297
508
    if current > total:
298
509
        return None                     # wtf?
299
510
 
300
 
    elapsed = time.time() - start_time
 
511
    elapsed = time.clock() - start_time
301
512
 
302
513
    if elapsed < 2.0:                   # not enough time to estimate
303
514
        return None
307
518
    assert total_duration >= elapsed
308
519
 
309
520
    if last_updates and len(last_updates) >= n_recent:
310
 
        while len(last_updates) > n_recent:
311
 
            last_updates.popleft()
312
521
        avg = sum(last_updates) / float(len(last_updates))
313
522
        time_left = avg * (total - current)
314
523
 
320
529
    return total_duration - elapsed
321
530
 
322
531
 
 
532
class ProgressPhase(object):
 
533
    """Update progress object with the current phase"""
 
534
    def __init__(self, message, total, pb):
 
535
        object.__init__(self)
 
536
        self.pb = pb
 
537
        self.message = message
 
538
        self.total = total
 
539
        self.cur_phase = None
 
540
 
 
541
    def next_phase(self):
 
542
        if self.cur_phase is None:
 
543
            self.cur_phase = 0
 
544
        else:
 
545
            self.cur_phase += 1
 
546
        assert self.cur_phase < self.total 
 
547
        self.pb.update(self.message, self.cur_phase, self.total)
 
548
 
 
549
 
323
550
def run_tests():
324
551
    import doctest
325
552
    result = doctest.testmod()