~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/progress.py

  • Committer: Ian Clatworthy
  • Date: 2009-01-19 02:24:15 UTC
  • mto: This revision was merged to the branch mainline in revision 3944.
  • Revision ID: ian.clatworthy@canonical.com-20090119022415-mo0mcfeiexfktgwt
apply jam's log --short fix (Ian Clatworthy)

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.
 
1
# Copyright (C) 2005, 2006, 2008 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
 
 
18
"""Progress indicators.
 
19
 
 
20
The usual way to use this is via bzrlib.ui.ui_factory.nested_progress_bar which
 
21
will maintain a ProgressBarStack for you.
 
22
 
 
23
For direct use, the factory ProgressBar will return an auto-detected progress
 
24
bar that should match your terminal type. You can manually create a
 
25
ProgressBarStack too if you need multiple levels of cooperating progress bars.
 
26
Note that bzrlib's internal functions use the ui module, so if you are using
 
27
bzrlib it really is best to use bzrlib.ui.ui_factory.
27
28
"""
28
29
 
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
30
 
39
31
import sys
40
32
import time
41
33
import os
42
 
from collections import deque
43
 
 
44
 
 
45
 
import bzrlib.errors as errors
46
 
from bzrlib.trace import mutter 
 
34
import warnings
 
35
 
 
36
 
 
37
from bzrlib import (
 
38
    errors,
 
39
    osutils,
 
40
    trace,
 
41
    ui,
 
42
    )
 
43
from bzrlib.trace import mutter
47
44
 
48
45
 
49
46
def _supports_progress(f):
50
 
    if not hasattr(f, 'isatty'):
 
47
    """Detect if we can use pretty progress bars on the output stream f.
 
48
 
 
49
    If this returns true we expect that a human may be looking at that 
 
50
    output, and that we can repaint a line to update it.
 
51
    """
 
52
    isatty = getattr(f, 'isatty', None)
 
53
    if isatty is None:
51
54
        return False
52
 
    if not f.isatty():
 
55
    if not isatty():
53
56
        return False
54
57
    if os.environ.get('TERM') == 'dumb':
55
58
        # e.g. emacs compile window
57
60
    return True
58
61
 
59
62
 
 
63
class ProgressTask(object):
 
64
    """Model component of a progress indicator.
 
65
 
 
66
    Most code that needs to indicate progress should update one of these, 
 
67
    and it will in turn update the display, if one is present.
 
68
 
 
69
    Code updating the task may also set fields as hints about how to display
 
70
    it: show_pct, show_spinner, show_eta, show_count, show_bar.  UIs
 
71
    will not necessarily respect all these fields.
 
72
    """
 
73
 
 
74
    def __init__(self, parent_task=None, ui_factory=None):
 
75
        self._parent_task = parent_task
 
76
        self._last_update = 0
 
77
        self.total_cnt = None
 
78
        self.current_cnt = None
 
79
        self.msg = ''
 
80
        self.ui_factory = ui_factory
 
81
        self.show_pct = False
 
82
        self.show_spinner = True
 
83
        self.show_eta = False,
 
84
        self.show_count = True
 
85
        self.show_bar = True
 
86
 
 
87
    def update(self, msg, current_cnt=None, total_cnt=None):
 
88
        self.msg = msg
 
89
        self.current_cnt = current_cnt
 
90
        if total_cnt:
 
91
            self.total_cnt = total_cnt
 
92
        self.ui_factory.show_progress(self)
 
93
 
 
94
    def tick(self):
 
95
        self.update(self.msg)
 
96
 
 
97
    def finished(self):
 
98
        self.ui_factory.progress_finished(self)
 
99
 
 
100
    def make_sub_task(self):
 
101
        return ProgressTask(self, self.ui_factory)
 
102
 
 
103
    def _overall_completion_fraction(self, child_fraction=0.0):
 
104
        """Return fractional completion of this task and its parents
 
105
        
 
106
        Returns None if no completion can be computed."""
 
107
        if self.total_cnt:
 
108
            own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
 
109
        else:
 
110
            own_fraction = None
 
111
        if self._parent_task is None:
 
112
            return own_fraction
 
113
        else:
 
114
            if own_fraction is None:
 
115
                own_fraction = 0.0
 
116
            return self._parent_task._overall_completion_fraction(own_fraction)
 
117
 
 
118
    def note(self, fmt_string, *args):
 
119
        """Record a note without disrupting the progress bar."""
 
120
        # XXX: shouldn't be here; put it in mutter or the ui instead
 
121
        self.ui_factory.note(fmt_string % args)
 
122
 
 
123
    def clear(self):
 
124
        # XXX: shouldn't be here; put it in mutter or the ui instead
 
125
        self.ui_factory.clear_term()
 
126
 
60
127
 
61
128
def ProgressBar(to_file=None, **kwargs):
62
129
    """Abstract factory"""
63
130
    if to_file is None:
64
131
        to_file = sys.stderr
65
 
    if _supports_progress(to_file):
66
 
        return TTYProgressBar(to_file=to_file, **kwargs)
 
132
    requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
 
133
    # An value of '' or not set reverts to standard processing
 
134
    if requested_bar_type in (None, ''):
 
135
        if _supports_progress(to_file):
 
136
            return TTYProgressBar(to_file=to_file, **kwargs)
 
137
        else:
 
138
            return DummyProgress(to_file=to_file, **kwargs)
67
139
    else:
68
 
        return DotsProgressBar(to_file=to_file, **kwargs)
69
 
    
70
 
 
 
140
        # Minor sanitation to prevent spurious errors
 
141
        requested_bar_type = requested_bar_type.lower().strip()
 
142
        # TODO: jam 20060710 Arguably we shouldn't raise an exception
 
143
        #       but should instead just disable progress bars if we
 
144
        #       don't recognize the type
 
145
        if requested_bar_type not in _progress_bar_types:
 
146
            raise errors.InvalidProgressBarType(requested_bar_type,
 
147
                                                _progress_bar_types.keys())
 
148
        return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
 
149
 
 
150
 
71
151
class ProgressBarStack(object):
72
152
    """A stack of progress bars."""
73
153
 
93
173
        self._show_count = show_count
94
174
        self._to_messages_file = to_messages_file
95
175
        self._stack = []
96
 
        self._klass = klass or TTYProgressBar
 
176
        self._klass = klass or ProgressBar
97
177
 
98
178
    def top(self):
99
179
        if len(self._stack) != 0:
127
207
    def return_pb(self, bar):
128
208
        """Return bar after its been used."""
129
209
        if bar is not self._stack[-1]:
130
 
            raise errors.MissingProgressBarFinish()
131
 
        self._stack.pop()
 
210
            warnings.warn("%r is not currently active" % (bar,))
 
211
        else:
 
212
            self._stack.pop()
132
213
 
133
214
 
134
215
class _BaseProgressBar(object):
137
218
                 to_file=None,
138
219
                 show_pct=False,
139
220
                 show_spinner=False,
140
 
                 show_eta=True,
 
221
                 show_eta=False,
141
222
                 show_bar=True,
142
223
                 show_count=True,
143
224
                 to_messages_file=None,
160
241
        self._stack = _stack
161
242
        # seed throttler
162
243
        self.MIN_PAUSE = 0.1 # seconds
163
 
        now = time.clock()
 
244
        now = time.time()
164
245
        # starting now
165
246
        self.start_time = now
166
247
        # next update should not throttle
169
250
    def finished(self):
170
251
        """Return this bar to its progress stack."""
171
252
        self.clear()
172
 
        assert self._stack is not None
173
253
        self._stack.return_pb(self)
174
254
 
175
255
    def note(self, fmt_string, *args, **kwargs):
187
267
 
188
268
    This can be used as the default argument for methods that
189
269
    take an optional progress indicator."""
 
270
 
190
271
    def tick(self):
191
272
        pass
192
273
 
233
314
    def child_update(self, message, current, total):
234
315
        self.tick()
235
316
 
 
317
 
 
318
 
236
319
    
237
320
class TTYProgressBar(_BaseProgressBar):
238
321
    """Progress bar display object.
262
345
        _BaseProgressBar.__init__(self, **kwargs)
263
346
        self.spin_pos = 0
264
347
        self.width = terminal_width()
265
 
        self.start_time = None
266
 
        self.last_updates = deque()
 
348
        self.last_updates = []
 
349
        self._max_last_updates = 10
267
350
        self.child_fraction = 0
 
351
        self._have_output = False
268
352
    
269
 
 
270
 
    def throttle(self):
 
353
    def throttle(self, old_msg):
271
354
        """Return True if the bar was updated too recently"""
272
355
        # time.time consistently takes 40/4000 ms = 0.01 ms.
273
 
        # but every single update to the pb invokes it.
274
 
        # so we use time.clock which takes 20/4000 ms = 0.005ms
275
 
        # on the downside, time.clock() appears to have approximately
276
 
        # 10ms granularity, so we treat a zero-time change as 'throttled.'
277
 
        
278
 
        now = time.clock()
 
356
        # time.clock() is faster, but gives us CPU time, not wall-clock time
 
357
        now = time.time()
 
358
        if self.start_time is not None and (now - self.start_time) < 1:
 
359
            return True
 
360
        if old_msg != self.last_msg:
 
361
            return False
279
362
        interval = now - self.last_update
280
363
        # if interval > 0
281
364
        if interval < self.MIN_PAUSE:
282
365
            return True
283
366
 
284
367
        self.last_updates.append(now - self.last_update)
 
368
        # Don't let the queue grow without bound
 
369
        self.last_updates = self.last_updates[-self._max_last_updates:]
285
370
        self.last_update = now
286
371
        return False
287
372
        
288
373
    def tick(self):
289
 
        self.update(self.last_msg, self.last_cnt, self.last_total, 
 
374
        self.update(self.last_msg, self.last_cnt, self.last_total,
290
375
                    self.child_fraction)
291
376
 
292
377
    def child_update(self, message, current, total):
296
381
                pass
297
382
            elif self.last_cnt + child_fraction <= self.last_total:
298
383
                self.child_fraction = child_fraction
299
 
            else:
300
 
                mutter('not updating child fraction')
301
384
        if self.last_msg is None:
302
385
            self.last_msg = ''
303
386
        self.tick()
304
387
 
305
 
    def update(self, msg, current_cnt=None, total_cnt=None, 
306
 
               child_fraction=0):
307
 
        """Update and redraw progress bar."""
 
388
    def update(self, msg, current_cnt=None, total_cnt=None,
 
389
            child_fraction=0):
 
390
        """Update and redraw progress bar.
 
391
        """
308
392
        if msg is None:
309
393
            msg = self.last_msg
310
394
 
330
414
        ##     self.child_fraction == child_fraction):
331
415
        ##     return
332
416
 
 
417
        if msg is None:
 
418
            msg = ''
 
419
 
333
420
        old_msg = self.last_msg
334
421
        # save these for the tick() function
335
422
        self.last_msg = msg
341
428
        # but multiple that by 4000 calls -> starts to cost.
342
429
        # so anything to make this function call faster
343
430
        # will improve base 'diff' time by up to 0.1 seconds.
344
 
        if old_msg == self.last_msg and self.throttle():
 
431
        if self.throttle(old_msg):
345
432
            return
346
433
 
347
434
        if self.show_eta and self.start_time and self.last_total:
375
462
            # make both fields the same size
376
463
            t = '%i' % (self.last_total)
377
464
            c = '%*i' % (len(t), self.last_cnt)
378
 
            count_str = ' ' + c + '/' + t 
 
465
            count_str = ' ' + c + '/' + t
379
466
 
380
467
        if self.show_bar:
381
468
            # progress bar, if present, soaks up all remaining space
399
486
        else:
400
487
            bar_str = ''
401
488
 
402
 
        m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
403
 
 
404
 
        assert len(m) < self.width
405
 
        self.to_file.write('\r' + m.ljust(self.width - 1))
 
489
        m = spin_str + bar_str + self.last_msg + count_str \
 
490
            + pct_str + eta_str
 
491
        self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
 
492
        self._have_output = True
406
493
        #self.to_file.flush()
407
494
            
408
 
    def clear(self):        
409
 
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
495
    def clear(self):
 
496
        if self._have_output:
 
497
            self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
498
        self._have_output = False
410
499
        #self.to_file.flush()        
411
500
 
412
501
 
 
502
 
 
503
 
413
504
class ChildProgress(_BaseProgressBar):
414
505
    """A progress indicator that pushes its data to the parent"""
415
506
 
423
514
 
424
515
    def update(self, msg, current_cnt=None, total_cnt=None):
425
516
        self.current = current_cnt
426
 
        self.total = total_cnt
 
517
        if total_cnt is not None:
 
518
            self.total = total_cnt
427
519
        self.message = msg
428
520
        self.child_fraction = 0
429
521
        self.tick()
452
544
    def note(self, *args, **kwargs):
453
545
        self.parent.note(*args, **kwargs)
454
546
 
455
 
 
 
547
 
 
548
class InstrumentedProgress(TTYProgressBar):
 
549
    """TTYProgress variant that tracks outcomes"""
 
550
 
 
551
    def __init__(self, *args, **kwargs):
 
552
        self.always_throttled = True
 
553
        self.never_throttle = False
 
554
        TTYProgressBar.__init__(self, *args, **kwargs)
 
555
 
 
556
    def throttle(self, old_message):
 
557
        if self.never_throttle:
 
558
            result =  False
 
559
        else:
 
560
            result = TTYProgressBar.throttle(self, old_message)
 
561
        if result is False:
 
562
            self.always_throttled = False
 
563
 
 
564
 
456
565
def str_tdelta(delt):
457
566
    if delt is None:
458
567
        return "-:--:--"
475
584
    if current > total:
476
585
        return None                     # wtf?
477
586
 
478
 
    elapsed = time.clock() - start_time
 
587
    elapsed = time.time() - start_time
479
588
 
480
589
    if elapsed < 2.0:                   # not enough time to estimate
481
590
        return None
482
591
    
483
592
    total_duration = float(elapsed) * float(total) / float(current)
484
593
 
485
 
    assert total_duration >= elapsed
486
 
 
487
594
    if last_updates and len(last_updates) >= n_recent:
488
 
        while len(last_updates) > n_recent:
489
 
            last_updates.popleft()
490
595
        avg = sum(last_updates) / float(len(last_updates))
491
596
        time_left = avg * (total - current)
492
597
 
512
617
            self.cur_phase = 0
513
618
        else:
514
619
            self.cur_phase += 1
515
 
        assert self.cur_phase < self.total 
516
620
        self.pb.update(self.message, self.cur_phase, self.total)
517
621
 
518
622
 
519
 
def run_tests():
520
 
    import doctest
521
 
    result = doctest.testmod()
522
 
    if result[1] > 0:
523
 
        if result[0] == 0:
524
 
            print "All tests passed"
525
 
    else:
526
 
        print "No tests to run"
527
 
 
528
 
 
529
 
def demo():
530
 
    sleep = time.sleep
531
 
    
532
 
    print 'dumb-terminal test:'
533
 
    pb = DotsProgressBar()
534
 
    for i in range(100):
535
 
        pb.update('Leoparden', i, 99)
536
 
        sleep(0.1)
537
 
    sleep(1.5)
538
 
    pb.clear()
539
 
    sleep(1.5)
540
 
    
541
 
    print 'smart-terminal test:'
542
 
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
543
 
    for i in range(100):
544
 
        pb.update('Elephanten', i, 99)
545
 
        sleep(0.1)
546
 
    sleep(2)
547
 
    pb.clear()
548
 
    sleep(1)
549
 
 
550
 
    print 'done!'
551
 
 
552
 
if __name__ == "__main__":
553
 
    demo()
 
623
_progress_bar_types = {}
 
624
_progress_bar_types['dummy'] = DummyProgress
 
625
_progress_bar_types['none'] = DummyProgress
 
626
_progress_bar_types['tty'] = TTYProgressBar
 
627
_progress_bar_types['dots'] = DotsProgressBar