~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/progress.py

  • Committer: Aaron Bentley
  • Date: 2006-06-24 05:58:21 UTC
  • mto: This revision was merged to the branch mainline in revision 1811.
  • Revision ID: aaron.bentley@utoronto.ca-20060624055821-123b47ddc04748a5
Avoid re-parsing texts version components

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, 2006 Canonical Ltd
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
 
"""Progress indicators.
20
 
 
21
 
The usual way to use this is via bzrlib.ui.ui_factory.nested_progress_bar which
22
 
will maintain a ProgressBarStack for you.
23
 
 
24
 
For direct use, the factory ProgressBar will return an auto-detected progress
25
 
bar that should match your terminal type. You can manually create a
26
 
ProgressBarStack too if you need multiple levels of cooperating progress bars.
27
 
Note that bzrlib's internal functions use the ui module, so if you are using
28
 
bzrlib it really is best to use bzrlib.ui.ui_factory.
 
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.
29
27
"""
30
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
 
31
35
# TODO: Optionally show elapsed time instead/as well as ETA; nicer
32
36
# when the rate is unpredictable
33
37
 
 
38
 
34
39
import sys
35
40
import time
36
41
import os
37
 
 
38
 
from bzrlib.lazy_import import lazy_import
39
 
lazy_import(globals(), """
40
 
from bzrlib import (
41
 
    errors,
42
 
    )
43
 
""")
44
 
 
45
 
from bzrlib.trace import mutter
 
42
from collections import deque
 
43
 
 
44
 
 
45
import bzrlib.errors as errors
 
46
from bzrlib.trace import mutter 
46
47
 
47
48
 
48
49
def _supports_progress(f):
49
 
    """Detect if we can use pretty progress bars on the output stream f.
50
 
 
51
 
    If this returns true we expect that a human may be looking at that 
52
 
    output, and that we can repaint a line to update it.
53
 
    """
54
 
    isatty = getattr(f, 'isatty', None)
55
 
    if isatty is None:
 
50
    if not hasattr(f, 'isatty'):
56
51
        return False
57
 
    if not isatty():
 
52
    if not f.isatty():
58
53
        return False
59
54
    if os.environ.get('TERM') == 'dumb':
60
55
        # e.g. emacs compile window
62
57
    return True
63
58
 
64
59
 
65
 
_progress_bar_types = {}
66
 
 
67
60
 
68
61
def ProgressBar(to_file=None, **kwargs):
69
62
    """Abstract factory"""
70
63
    if to_file is None:
71
64
        to_file = sys.stderr
72
 
    requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
73
 
    # An value of '' or not set reverts to standard processing
74
 
    if requested_bar_type in (None, ''):
75
 
        if _supports_progress(to_file):
76
 
            return TTYProgressBar(to_file=to_file, **kwargs)
77
 
        else:
78
 
            return DummyProgress(to_file=to_file, **kwargs)
 
65
    if _supports_progress(to_file):
 
66
        return TTYProgressBar(to_file=to_file, **kwargs)
79
67
    else:
80
 
        # Minor sanitation to prevent spurious errors
81
 
        requested_bar_type = requested_bar_type.lower().strip()
82
 
        # TODO: jam 20060710 Arguably we shouldn't raise an exception
83
 
        #       but should instead just disable progress bars if we
84
 
        #       don't recognize the type
85
 
        if requested_bar_type not in _progress_bar_types:
86
 
            raise errors.InvalidProgressBarType(requested_bar_type,
87
 
                                                _progress_bar_types.keys())
88
 
        return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
89
 
 
 
68
        return DotsProgressBar(to_file=to_file, **kwargs)
 
69
    
90
70
 
91
71
class ProgressBarStack(object):
92
72
    """A stack of progress bars."""
113
93
        self._show_count = show_count
114
94
        self._to_messages_file = to_messages_file
115
95
        self._stack = []
116
 
        self._klass = klass or ProgressBar
 
96
        self._klass = klass or TTYProgressBar
117
97
 
118
98
    def top(self):
119
99
        if len(self._stack) != 0:
180
160
        self._stack = _stack
181
161
        # seed throttler
182
162
        self.MIN_PAUSE = 0.1 # seconds
183
 
        now = time.time()
 
163
        now = time.clock()
184
164
        # starting now
185
165
        self.start_time = now
186
166
        # next update should not throttle
189
169
    def finished(self):
190
170
        """Return this bar to its progress stack."""
191
171
        self.clear()
 
172
        assert self._stack is not None
192
173
        self._stack.return_pb(self)
193
174
 
194
175
    def note(self, fmt_string, *args, **kwargs):
225
206
        return DummyProgress(**kwargs)
226
207
 
227
208
 
228
 
_progress_bar_types['dummy'] = DummyProgress
229
 
_progress_bar_types['none'] = DummyProgress
230
 
 
231
 
 
232
209
class DotsProgressBar(_BaseProgressBar):
233
210
 
234
211
    def __init__(self, **kwargs):
256
233
    def child_update(self, message, current, total):
257
234
        self.tick()
258
235
 
259
 
 
260
 
_progress_bar_types['dots'] = DotsProgressBar
261
 
 
262
236
    
263
237
class TTYProgressBar(_BaseProgressBar):
264
238
    """Progress bar display object.
288
262
        _BaseProgressBar.__init__(self, **kwargs)
289
263
        self.spin_pos = 0
290
264
        self.width = terminal_width()
291
 
        self.last_updates = []
292
 
        self._max_last_updates = 10
 
265
        self.last_updates = deque()
293
266
        self.child_fraction = 0
294
 
        self._have_output = False
295
267
    
 
268
 
296
269
    def throttle(self, old_msg):
297
270
        """Return True if the bar was updated too recently"""
298
271
        # time.time consistently takes 40/4000 ms = 0.01 ms.
299
 
        # time.clock() is faster, but gives us CPU time, not wall-clock time
300
 
        now = time.time()
 
272
        # but every single update to the pb invokes it.
 
273
        # so we use time.clock which takes 20/4000 ms = 0.005ms
 
274
        # on the downside, time.clock() appears to have approximately
 
275
        # 10ms granularity, so we treat a zero-time change as 'throttled.'
 
276
        now = time.clock()
301
277
        if self.start_time is not None and (now - self.start_time) < 1:
302
278
            return True
303
279
        if old_msg != self.last_msg:
308
284
            return True
309
285
 
310
286
        self.last_updates.append(now - self.last_update)
311
 
        # Don't let the queue grow without bound
312
 
        self.last_updates = self.last_updates[-self._max_last_updates:]
313
287
        self.last_update = now
314
288
        return False
315
289
        
316
290
    def tick(self):
317
 
        self.update(self.last_msg, self.last_cnt, self.last_total,
 
291
        self.update(self.last_msg, self.last_cnt, self.last_total, 
318
292
                    self.child_fraction)
319
293
 
320
294
    def child_update(self, message, current, total):
324
298
                pass
325
299
            elif self.last_cnt + child_fraction <= self.last_total:
326
300
                self.child_fraction = child_fraction
 
301
            else:
 
302
                mutter('not updating child fraction')
327
303
        if self.last_msg is None:
328
304
            self.last_msg = ''
329
305
        self.tick()
330
306
 
331
 
    def update(self, msg, current_cnt=None, total_cnt=None,
 
307
    def update(self, msg, current_cnt=None, total_cnt=None, 
332
308
               child_fraction=0):
333
309
        """Update and redraw progress bar."""
334
310
        if msg is None:
426
402
            bar_str = ''
427
403
 
428
404
        m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
429
 
        self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
430
 
        self._have_output = True
 
405
 
 
406
        assert len(m) < self.width
 
407
        self.to_file.write('\r' + m.ljust(self.width - 1))
431
408
        #self.to_file.flush()
432
409
            
433
 
    def clear(self):
434
 
        if self._have_output:
435
 
            self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
436
 
        self._have_output = False
 
410
    def clear(self):        
 
411
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
437
412
        #self.to_file.flush()        
438
413
 
439
414
 
440
 
_progress_bar_types['tty'] = TTYProgressBar
441
 
 
442
 
 
443
415
class ChildProgress(_BaseProgressBar):
444
416
    """A progress indicator that pushes its data to the parent"""
445
417
 
453
425
 
454
426
    def update(self, msg, current_cnt=None, total_cnt=None):
455
427
        self.current = current_cnt
456
 
        if total_cnt is not None:
457
 
            self.total = total_cnt
 
428
        self.total = total_cnt
458
429
        self.message = msg
459
430
        self.child_fraction = 0
460
431
        self.tick()
483
454
    def note(self, *args, **kwargs):
484
455
        self.parent.note(*args, **kwargs)
485
456
 
486
 
 
487
 
class InstrumentedProgress(TTYProgressBar):
488
 
    """TTYProgress variant that tracks outcomes"""
489
 
 
490
 
    def __init__(self, *args, **kwargs):
491
 
        self.always_throttled = True
492
 
        self.never_throttle = False
493
 
        TTYProgressBar.__init__(self, *args, **kwargs)
494
 
 
495
 
    def throttle(self, old_message):
496
 
        if self.never_throttle:
497
 
            result =  False
498
 
        else:
499
 
            result = TTYProgressBar.throttle(self, old_message)
500
 
        if result is False:
501
 
            self.always_throttled = False
502
 
 
503
 
 
 
457
 
504
458
def str_tdelta(delt):
505
459
    if delt is None:
506
460
        return "-:--:--"
523
477
    if current > total:
524
478
        return None                     # wtf?
525
479
 
526
 
    elapsed = time.time() - start_time
 
480
    elapsed = time.clock() - start_time
527
481
 
528
482
    if elapsed < 2.0:                   # not enough time to estimate
529
483
        return None
530
484
    
531
485
    total_duration = float(elapsed) * float(total) / float(current)
532
486
 
 
487
    assert total_duration >= elapsed
 
488
 
533
489
    if last_updates and len(last_updates) >= n_recent:
 
490
        while len(last_updates) > n_recent:
 
491
            last_updates.popleft()
534
492
        avg = sum(last_updates) / float(len(last_updates))
535
493
        time_left = avg * (total - current)
536
494
 
556
514
            self.cur_phase = 0
557
515
        else:
558
516
            self.cur_phase += 1
 
517
        assert self.cur_phase < self.total 
559
518
        self.pb.update(self.message, self.cur_phase, self.total)
 
519
 
 
520
 
 
521
def run_tests():
 
522
    import doctest
 
523
    result = doctest.testmod()
 
524
    if result[1] > 0:
 
525
        if result[0] == 0:
 
526
            print "All tests passed"
 
527
    else:
 
528
        print "No tests to run"
 
529
 
 
530
 
 
531
def demo():
 
532
    sleep = time.sleep
 
533
    
 
534
    print 'dumb-terminal test:'
 
535
    pb = DotsProgressBar()
 
536
    for i in range(100):
 
537
        pb.update('Leoparden', i, 99)
 
538
        sleep(0.1)
 
539
    sleep(1.5)
 
540
    pb.clear()
 
541
    sleep(1.5)
 
542
    
 
543
    print 'smart-terminal test:'
 
544
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
 
545
    for i in range(100):
 
546
        pb.update('Elephanten', i, 99)
 
547
        sleep(0.1)
 
548
    sleep(2)
 
549
    pb.clear()
 
550
    sleep(1)
 
551
 
 
552
    print 'done!'
 
553
 
 
554
if __name__ == "__main__":
 
555
    demo()