~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/progress.py

  • Committer: Aaron Bentley
  • Date: 2006-04-05 20:37:37 UTC
  • mto: (2027.1.2 revert-subpath-56549)
  • mto: This revision was merged to the branch mainline in revision 1647.
  • Revision ID: abentley@panoramicfeedback.com-20060405203737-d50848ef2df7344a
Eliminated conflicts_to_strings, made remove_files a ConflictList member

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 
 
47
import bzrlib.ui
46
48
 
47
49
 
48
50
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:
 
51
    if not hasattr(f, 'isatty'):
56
52
        return False
57
 
    if not isatty():
 
53
    if not f.isatty():
58
54
        return False
59
55
    if os.environ.get('TERM') == 'dumb':
60
56
        # e.g. emacs compile window
62
58
    return True
63
59
 
64
60
 
65
 
_progress_bar_types = {}
66
 
 
67
 
 
68
 
def ProgressBar(to_file=None, **kwargs):
 
61
 
 
62
def ProgressBar(to_file=sys.stderr, **kwargs):
69
63
    """Abstract factory"""
70
 
    if to_file is None:
71
 
        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)
 
64
    if _supports_progress(to_file):
 
65
        return TTYProgressBar(to_file=to_file, **kwargs)
79
66
    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
 
 
 
67
        return DotsProgressBar(to_file=to_file, **kwargs)
 
68
    
90
69
 
91
70
class ProgressBarStack(object):
92
71
    """A stack of progress bars."""
93
72
 
94
73
    def __init__(self,
95
 
                 to_file=None,
 
74
                 to_file=sys.stderr,
96
75
                 show_pct=False,
97
76
                 show_spinner=True,
98
77
                 show_eta=False,
99
78
                 show_bar=True,
100
79
                 show_count=True,
101
 
                 to_messages_file=None,
 
80
                 to_messages_file=sys.stdout,
102
81
                 klass=None):
103
82
        """Setup the stack with the parameters the progress bars should have."""
104
 
        if to_file is None:
105
 
            to_file = sys.stderr
106
 
        if to_messages_file is None:
107
 
            to_messages_file = sys.stdout
108
83
        self._to_file = to_file
109
84
        self._show_pct = show_pct
110
85
        self._show_spinner = show_spinner
113
88
        self._show_count = show_count
114
89
        self._to_messages_file = to_messages_file
115
90
        self._stack = []
116
 
        self._klass = klass or ProgressBar
 
91
        self._klass = klass or TTYProgressBar
117
92
 
118
93
    def top(self):
119
94
        if len(self._stack) != 0:
154
129
class _BaseProgressBar(object):
155
130
 
156
131
    def __init__(self,
157
 
                 to_file=None,
 
132
                 to_file=sys.stderr,
158
133
                 show_pct=False,
159
134
                 show_spinner=False,
160
 
                 show_eta=False,
 
135
                 show_eta=True,
161
136
                 show_bar=True,
162
137
                 show_count=True,
163
 
                 to_messages_file=None,
 
138
                 to_messages_file=sys.stdout,
164
139
                 _stack=None):
165
140
        object.__init__(self)
166
 
        if to_file is None:
167
 
            to_file = sys.stderr
168
 
        if to_messages_file is None:
169
 
            to_messages_file = sys.stdout
170
141
        self.to_file = to_file
171
142
        self.to_messages_file = to_messages_file
172
143
        self.last_msg = None
180
151
        self._stack = _stack
181
152
        # seed throttler
182
153
        self.MIN_PAUSE = 0.1 # seconds
183
 
        now = time.time()
 
154
        now = time.clock()
184
155
        # starting now
185
156
        self.start_time = now
186
157
        # next update should not throttle
189
160
    def finished(self):
190
161
        """Return this bar to its progress stack."""
191
162
        self.clear()
 
163
        assert self._stack is not None
192
164
        self._stack.return_pb(self)
193
165
 
194
166
    def note(self, fmt_string, *args, **kwargs):
195
167
        """Record a note without disrupting the progress bar."""
196
 
        self.clear()
 
168
        bzrlib.ui.ui_factory.clear_term()
197
169
        self.to_messages_file.write(fmt_string % args)
198
170
        self.to_messages_file.write('\n')
199
171
 
224
196
    def child_progress(self, **kwargs):
225
197
        return DummyProgress(**kwargs)
226
198
 
227
 
 
228
 
_progress_bar_types['dummy'] = DummyProgress
229
 
_progress_bar_types['none'] = DummyProgress
230
 
 
231
 
 
232
199
class DotsProgressBar(_BaseProgressBar):
233
200
 
234
201
    def __init__(self, **kwargs):
243
210
        if msg and msg != self.last_msg:
244
211
            if self.need_nl:
245
212
                self.to_file.write('\n')
 
213
            
246
214
            self.to_file.write(msg + ': ')
247
215
            self.last_msg = msg
248
216
        self.need_nl = True
251
219
    def clear(self):
252
220
        if self.need_nl:
253
221
            self.to_file.write('\n')
254
 
        self.need_nl = False
255
222
        
256
223
    def child_update(self, message, current, total):
257
224
        self.tick()
258
 
 
259
 
 
260
 
_progress_bar_types['dots'] = DotsProgressBar
261
 
 
262
225
    
263
226
class TTYProgressBar(_BaseProgressBar):
264
227
    """Progress bar display object.
288
251
        _BaseProgressBar.__init__(self, **kwargs)
289
252
        self.spin_pos = 0
290
253
        self.width = terminal_width()
291
 
        self.last_updates = []
292
 
        self._max_last_updates = 10
 
254
        self.start_time = None
 
255
        self.last_updates = deque()
293
256
        self.child_fraction = 0
294
 
        self._have_output = False
295
257
    
296
 
    def throttle(self, old_msg):
 
258
 
 
259
    def throttle(self):
297
260
        """Return True if the bar was updated too recently"""
298
261
        # 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()
301
 
        if self.start_time is not None and (now - self.start_time) < 1:
302
 
            return True
303
 
        if old_msg != self.last_msg:
304
 
            return False
 
262
        # but every single update to the pb invokes it.
 
263
        # so we use time.clock which takes 20/4000 ms = 0.005ms
 
264
        # on the downside, time.clock() appears to have approximately
 
265
        # 10ms granularity, so we treat a zero-time change as 'throttled.'
 
266
        
 
267
        now = time.clock()
305
268
        interval = now - self.last_update
306
269
        # if interval > 0
307
270
        if interval < self.MIN_PAUSE:
308
271
            return True
309
272
 
310
273
        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
274
        self.last_update = now
314
275
        return False
315
276
        
 
277
 
316
278
    def tick(self):
317
 
        self.update(self.last_msg, self.last_cnt, self.last_total,
 
279
        self.update(self.last_msg, self.last_cnt, self.last_total, 
318
280
                    self.child_fraction)
319
281
 
320
282
    def child_update(self, message, current, total):
324
286
                pass
325
287
            elif self.last_cnt + child_fraction <= self.last_total:
326
288
                self.child_fraction = child_fraction
 
289
            else:
 
290
                mutter('not updating child fraction')
327
291
        if self.last_msg is None:
328
292
            self.last_msg = ''
329
293
        self.tick()
330
294
 
331
 
    def update(self, msg, current_cnt=None, total_cnt=None,
 
295
 
 
296
    def update(self, msg, current_cnt=None, total_cnt=None, 
332
297
               child_fraction=0):
333
298
        """Update and redraw progress bar."""
334
 
        if msg is None:
335
 
            msg = self.last_msg
336
 
 
337
 
        if total_cnt is None:
338
 
            total_cnt = self.last_total
339
299
 
340
300
        if current_cnt < 0:
341
301
            current_cnt = 0
367
327
        # but multiple that by 4000 calls -> starts to cost.
368
328
        # so anything to make this function call faster
369
329
        # will improve base 'diff' time by up to 0.1 seconds.
370
 
        if self.throttle(old_msg):
 
330
        if old_msg == self.last_msg and self.throttle():
371
331
            return
372
332
 
373
333
        if self.show_eta and self.start_time and self.last_total:
426
386
            bar_str = ''
427
387
 
428
388
        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
 
389
 
 
390
        assert len(m) < self.width
 
391
        self.to_file.write('\r' + m.ljust(self.width - 1))
431
392
        #self.to_file.flush()
432
393
            
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
 
394
    def clear(self):        
 
395
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
437
396
        #self.to_file.flush()        
438
397
 
439
398
 
440
 
_progress_bar_types['tty'] = TTYProgressBar
441
 
 
442
 
 
443
399
class ChildProgress(_BaseProgressBar):
444
400
    """A progress indicator that pushes its data to the parent"""
445
 
 
446
401
    def __init__(self, _stack, **kwargs):
447
402
        _BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
448
403
        self.parent = _stack.top()
453
408
 
454
409
    def update(self, msg, current_cnt=None, total_cnt=None):
455
410
        self.current = current_cnt
456
 
        if total_cnt is not None:
457
 
            self.total = total_cnt
 
411
        self.total = total_cnt
458
412
        self.message = msg
459
413
        self.child_fraction = 0
460
414
        self.tick()
480
434
    def clear(self):
481
435
        pass
482
436
 
483
 
    def note(self, *args, **kwargs):
484
 
        self.parent.note(*args, **kwargs)
485
 
 
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
 
 
 
437
 
504
438
def str_tdelta(delt):
505
439
    if delt is None:
506
440
        return "-:--:--"
523
457
    if current > total:
524
458
        return None                     # wtf?
525
459
 
526
 
    elapsed = time.time() - start_time
 
460
    elapsed = time.clock() - start_time
527
461
 
528
462
    if elapsed < 2.0:                   # not enough time to estimate
529
463
        return None
530
464
    
531
465
    total_duration = float(elapsed) * float(total) / float(current)
532
466
 
 
467
    assert total_duration >= elapsed
 
468
 
533
469
    if last_updates and len(last_updates) >= n_recent:
 
470
        while len(last_updates) > n_recent:
 
471
            last_updates.popleft()
534
472
        avg = sum(last_updates) / float(len(last_updates))
535
473
        time_left = avg * (total - current)
536
474
 
556
494
            self.cur_phase = 0
557
495
        else:
558
496
            self.cur_phase += 1
 
497
        assert self.cur_phase < self.total 
559
498
        self.pb.update(self.message, self.cur_phase, self.total)
 
499
 
 
500
 
 
501
def run_tests():
 
502
    import doctest
 
503
    result = doctest.testmod()
 
504
    if result[1] > 0:
 
505
        if result[0] == 0:
 
506
            print "All tests passed"
 
507
    else:
 
508
        print "No tests to run"
 
509
 
 
510
 
 
511
def demo():
 
512
    sleep = time.sleep
 
513
    
 
514
    print 'dumb-terminal test:'
 
515
    pb = DotsProgressBar()
 
516
    for i in range(100):
 
517
        pb.update('Leoparden', i, 99)
 
518
        sleep(0.1)
 
519
    sleep(1.5)
 
520
    pb.clear()
 
521
    sleep(1.5)
 
522
    
 
523
    print 'smart-terminal test:'
 
524
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
 
525
    for i in range(100):
 
526
        pb.update('Elephanten', i, 99)
 
527
        sleep(0.1)
 
528
    sleep(2)
 
529
    pb.clear()
 
530
    sleep(1)
 
531
 
 
532
    print 'done!'
 
533
 
 
534
if __name__ == "__main__":
 
535
    demo()