~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/progress.py

  • Committer: Jelmer Vernooij
  • Date: 2009-07-18 21:09:00 UTC
  • mfrom: (4416.8.1 bzr.dev)
  • mto: This revision was merged to the branch mainline in revision 4547.
  • Revision ID: jelmer@samba.org-20090718210900-oht5snx25j1jyeha
Merge create_prefix fix necessary for bzr-svn.

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 Ltd
 
1
# Copyright (C) 2005, 2006, 2008, 2009 Canonical Ltd
3
2
#
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
13
12
#
14
13
# You should have received a copy of the GNU General Public License
15
14
# 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.
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 manage a conceptual stack of nested activities.
27
22
"""
28
23
 
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
24
 
38
25
import sys
39
26
import time
40
27
import os
41
28
 
42
 
from bzrlib.lazy_import import lazy_import
43
 
lazy_import(globals(), """
 
29
 
44
30
from bzrlib import (
45
31
    errors,
46
32
    )
47
 
""")
48
 
 
49
33
from bzrlib.trace import mutter
50
 
 
51
 
 
 
34
from bzrlib.symbol_versioning import (
 
35
    deprecated_function,
 
36
    deprecated_in,
 
37
    )
 
38
 
 
39
 
 
40
# XXX: deprecated; can be removed when the ProgressBar factory is removed
52
41
def _supports_progress(f):
53
42
    """Detect if we can use pretty progress bars on the output stream f.
54
43
 
55
 
    If this returns true we expect that a human may be looking at that 
 
44
    If this returns true we expect that a human may be looking at that
56
45
    output, and that we can repaint a line to update it.
57
46
    """
58
47
    isatty = getattr(f, 'isatty', None)
66
55
    return True
67
56
 
68
57
 
69
 
_progress_bar_types = {}
70
 
 
71
 
 
 
58
class ProgressTask(object):
 
59
    """Model component of a progress indicator.
 
60
 
 
61
    Most code that needs to indicate progress should update one of these,
 
62
    and it will in turn update the display, if one is present.
 
63
 
 
64
    Code updating the task may also set fields as hints about how to display
 
65
    it: show_pct, show_spinner, show_eta, show_count, show_bar.  UIs
 
66
    will not necessarily respect all these fields.
 
67
    """
 
68
 
 
69
    def __init__(self, parent_task=None, ui_factory=None):
 
70
        """Construct a new progress task.
 
71
 
 
72
        Normally you should not call this directly but rather through
 
73
        `ui_factory.nested_progress_bar`.
 
74
        """
 
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 __repr__(self):
 
88
        return '%s(%r/%r, msg=%r)' % (
 
89
            self.__class__.__name__,
 
90
            self.current_cnt,
 
91
            self.total_cnt,
 
92
            self.msg)
 
93
 
 
94
    def update(self, msg, current_cnt=None, total_cnt=None):
 
95
        self.msg = msg
 
96
        self.current_cnt = current_cnt
 
97
        if total_cnt:
 
98
            self.total_cnt = total_cnt
 
99
        self.ui_factory._progress_updated(self)
 
100
 
 
101
    def tick(self):
 
102
        self.update(self.msg)
 
103
 
 
104
    def finished(self):
 
105
        self.ui_factory._progress_finished(self)
 
106
 
 
107
    def make_sub_task(self):
 
108
        return ProgressTask(self, self.ui_factory)
 
109
 
 
110
    def _overall_completion_fraction(self, child_fraction=0.0):
 
111
        """Return fractional completion of this task and its parents
 
112
 
 
113
        Returns None if no completion can be computed."""
 
114
        if self.current_cnt is not None and self.total_cnt:
 
115
            own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
 
116
        else:
 
117
            # if this task has no estimation, it just passes on directly
 
118
            # whatever the child has measured...
 
119
            own_fraction = child_fraction
 
120
        if self._parent_task is None:
 
121
            return own_fraction
 
122
        else:
 
123
            if own_fraction is None:
 
124
                own_fraction = 0.0
 
125
            return self._parent_task._overall_completion_fraction(own_fraction)
 
126
 
 
127
    def note(self, fmt_string, *args):
 
128
        """Record a note without disrupting the progress bar."""
 
129
        # XXX: shouldn't be here; put it in mutter or the ui instead
 
130
        if args:
 
131
            self.ui_factory.note(fmt_string % args)
 
132
        else:
 
133
            self.ui_factory.note(fmt_string)
 
134
 
 
135
    def clear(self):
 
136
        # XXX: shouldn't be here; put it in mutter or the ui instead
 
137
        self.ui_factory.clear_term()
 
138
 
 
139
 
 
140
@deprecated_function(deprecated_in((1, 16, 0)))
72
141
def ProgressBar(to_file=None, **kwargs):
73
142
    """Abstract factory"""
74
143
    if to_file is None:
91
160
                                                _progress_bar_types.keys())
92
161
        return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
93
162
 
94
 
 
95
 
class ProgressBarStack(object):
96
 
    """A stack of progress bars."""
97
 
 
98
 
    def __init__(self,
99
 
                 to_file=None,
100
 
                 show_pct=False,
101
 
                 show_spinner=True,
102
 
                 show_eta=False,
103
 
                 show_bar=True,
104
 
                 show_count=True,
105
 
                 to_messages_file=None,
106
 
                 klass=None):
107
 
        """Setup the stack with the parameters the progress bars should have."""
108
 
        if to_file is None:
109
 
            to_file = sys.stderr
110
 
        if to_messages_file is None:
111
 
            to_messages_file = sys.stdout
112
 
        self._to_file = to_file
113
 
        self._show_pct = show_pct
114
 
        self._show_spinner = show_spinner
115
 
        self._show_eta = show_eta
116
 
        self._show_bar = show_bar
117
 
        self._show_count = show_count
118
 
        self._to_messages_file = to_messages_file
119
 
        self._stack = []
120
 
        self._klass = klass or ProgressBar
121
 
 
122
 
    def top(self):
123
 
        if len(self._stack) != 0:
124
 
            return self._stack[-1]
125
 
        else:
126
 
            return None
127
 
 
128
 
    def bottom(self):
129
 
        if len(self._stack) != 0:
130
 
            return self._stack[0]
131
 
        else:
132
 
            return None
133
 
 
134
 
    def get_nested(self):
135
 
        """Return a nested progress bar."""
136
 
        if len(self._stack) == 0:
137
 
            func = self._klass
138
 
        else:
139
 
            func = self.top().child_progress
140
 
        new_bar = func(to_file=self._to_file,
141
 
                       show_pct=self._show_pct,
142
 
                       show_spinner=self._show_spinner,
143
 
                       show_eta=self._show_eta,
144
 
                       show_bar=self._show_bar,
145
 
                       show_count=self._show_count,
146
 
                       to_messages_file=self._to_messages_file,
147
 
                       _stack=self)
148
 
        self._stack.append(new_bar)
149
 
        return new_bar
150
 
 
151
 
    def return_pb(self, bar):
152
 
        """Return bar after its been used."""
153
 
        if bar is not self._stack[-1]:
154
 
            raise errors.MissingProgressBarFinish()
155
 
        self._stack.pop()
156
 
 
157
 
 
 
163
 
158
164
class _BaseProgressBar(object):
159
165
 
160
166
    def __init__(self,
193
199
    def finished(self):
194
200
        """Return this bar to its progress stack."""
195
201
        self.clear()
196
 
        assert self._stack is not None
197
202
        self._stack.return_pb(self)
198
203
 
199
204
    def note(self, fmt_string, *args, **kwargs):
202
207
        self.to_messages_file.write(fmt_string % args)
203
208
        self.to_messages_file.write('\n')
204
209
 
 
210
    @deprecated_function(deprecated_in((1, 16, 0)))
205
211
    def child_progress(self, **kwargs):
206
212
        return ChildProgress(**kwargs)
207
213
 
211
217
 
212
218
    This can be used as the default argument for methods that
213
219
    take an optional progress indicator."""
 
220
 
214
221
    def tick(self):
215
222
        pass
216
223
 
222
229
 
223
230
    def clear(self):
224
231
        pass
225
 
        
 
232
 
226
233
    def note(self, fmt_string, *args, **kwargs):
227
234
        """See _BaseProgressBar.note()."""
228
235
 
230
237
        return DummyProgress(**kwargs)
231
238
 
232
239
 
233
 
_progress_bar_types['dummy'] = DummyProgress
234
 
_progress_bar_types['none'] = DummyProgress
235
 
 
236
 
 
237
240
class DotsProgressBar(_BaseProgressBar):
238
241
 
 
242
    @deprecated_function(deprecated_in((1, 16, 0)))
239
243
    def __init__(self, **kwargs):
240
244
        _BaseProgressBar.__init__(self, **kwargs)
241
245
        self.last_msg = None
242
246
        self.need_nl = False
243
 
        
 
247
 
244
248
    def tick(self):
245
249
        self.update()
246
 
        
 
250
 
247
251
    def update(self, msg=None, current_cnt=None, total_cnt=None):
248
252
        if msg and msg != self.last_msg:
249
253
            if self.need_nl:
252
256
            self.last_msg = msg
253
257
        self.need_nl = True
254
258
        self.to_file.write('.')
255
 
        
 
259
 
256
260
    def clear(self):
257
261
        if self.need_nl:
258
262
            self.to_file.write('\n')
259
263
        self.need_nl = False
260
 
        
 
264
 
261
265
    def child_update(self, message, current, total):
262
266
        self.tick()
263
267
 
264
268
 
265
 
_progress_bar_types['dots'] = DotsProgressBar
266
 
 
267
 
    
268
269
class TTYProgressBar(_BaseProgressBar):
269
270
    """Progress bar display object.
270
271
 
287
288
    """
288
289
    SPIN_CHARS = r'/-\|'
289
290
 
290
 
 
 
291
    @deprecated_function(deprecated_in((1, 16, 0)))
291
292
    def __init__(self, **kwargs):
292
293
        from bzrlib.osutils import terminal_width
293
294
        _BaseProgressBar.__init__(self, **kwargs)
297
298
        self._max_last_updates = 10
298
299
        self.child_fraction = 0
299
300
        self._have_output = False
300
 
    
301
301
 
302
302
    def throttle(self, old_msg):
303
303
        """Return True if the bar was updated too recently"""
318
318
        self.last_updates = self.last_updates[-self._max_last_updates:]
319
319
        self.last_update = now
320
320
        return False
321
 
        
 
321
 
322
322
    def tick(self):
323
 
        self.update(self.last_msg, self.last_cnt, self.last_total, 
 
323
        self.update(self.last_msg, self.last_cnt, self.last_total,
324
324
                    self.child_fraction)
325
325
 
326
326
    def child_update(self, message, current, total):
330
330
                pass
331
331
            elif self.last_cnt + child_fraction <= self.last_total:
332
332
                self.child_fraction = child_fraction
333
 
            else:
334
 
                mutter('not updating child fraction')
335
333
        if self.last_msg is None:
336
334
            self.last_msg = ''
337
335
        self.tick()
338
336
 
339
 
    def update(self, msg, current_cnt=None, total_cnt=None, 
340
 
               child_fraction=0):
341
 
        """Update and redraw progress bar."""
 
337
    def update(self, msg, current_cnt=None, total_cnt=None,
 
338
            child_fraction=0):
 
339
        """Update and redraw progress bar.
 
340
        """
342
341
        if msg is None:
343
342
            msg = self.last_msg
344
343
 
347
346
 
348
347
        if current_cnt < 0:
349
348
            current_cnt = 0
350
 
            
 
349
 
351
350
        if current_cnt > total_cnt:
352
351
            total_cnt = current_cnt
353
 
        
354
 
        ## # optional corner case optimisation 
 
352
 
 
353
        ## # optional corner case optimisation
355
354
        ## # currently does not seem to fire so costs more than saved.
356
355
        ## # trivial optimal case:
357
356
        ## # NB if callers are doing a clear and restore with
364
363
        ##     self.child_fraction == child_fraction):
365
364
        ##     return
366
365
 
 
366
        if msg is None:
 
367
            msg = ''
 
368
 
367
369
        old_msg = self.last_msg
368
370
        # save these for the tick() function
369
371
        self.last_msg = msg
371
373
        self.last_total = total_cnt
372
374
        self.child_fraction = child_fraction
373
375
 
374
 
        # each function call takes 20ms/4000 = 0.005 ms, 
 
376
        # each function call takes 20ms/4000 = 0.005 ms,
375
377
        # but multiple that by 4000 calls -> starts to cost.
376
378
        # so anything to make this function call faster
377
379
        # will improve base 'diff' time by up to 0.1 seconds.
379
381
            return
380
382
 
381
383
        if self.show_eta and self.start_time and self.last_total:
382
 
            eta = get_eta(self.start_time, self.last_cnt + self.child_fraction, 
 
384
            eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
383
385
                    self.last_total, last_updates = self.last_updates)
384
386
            eta_str = " " + str_tdelta(eta)
385
387
        else:
386
388
            eta_str = ""
387
389
 
388
390
        if self.show_spinner:
389
 
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
 
391
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
390
392
        else:
391
393
            spin_str = ''
392
394
 
409
411
            # make both fields the same size
410
412
            t = '%i' % (self.last_total)
411
413
            c = '%*i' % (len(t), self.last_cnt)
412
 
            count_str = ' ' + c + '/' + t 
 
414
            count_str = ' ' + c + '/' + t
413
415
 
414
416
        if self.show_bar:
415
417
            # progress bar, if present, soaks up all remaining space
418
420
 
419
421
            if self.last_total:
420
422
                # number of markers highlighted in bar
421
 
                markers = int(round(float(cols) * 
 
423
                markers = int(round(float(cols) *
422
424
                              (self.last_cnt + self.child_fraction) / self.last_total))
423
425
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
424
426
            elif False:
426
428
                # so just show an expanded spinning thingy
427
429
                m = self.spin_pos % cols
428
430
                ms = (' ' * m + '*').ljust(cols)
429
 
                
 
431
 
430
432
                bar_str = '[' + ms + '] '
431
433
            else:
432
434
                bar_str = ''
433
435
        else:
434
436
            bar_str = ''
435
437
 
436
 
        m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
 
438
        m = spin_str + bar_str + self.last_msg + count_str \
 
439
            + pct_str + eta_str
437
440
        self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
438
441
        self._have_output = True
439
442
        #self.to_file.flush()
440
 
            
441
 
    def clear(self):        
 
443
 
 
444
    def clear(self):
442
445
        if self._have_output:
443
446
            self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
444
447
        self._have_output = False
445
 
        #self.to_file.flush()        
446
 
 
447
 
 
448
 
_progress_bar_types['tty'] = TTYProgressBar
 
448
        #self.to_file.flush()
449
449
 
450
450
 
451
451
class ChildProgress(_BaseProgressBar):
452
452
    """A progress indicator that pushes its data to the parent"""
453
453
 
 
454
    @deprecated_function(deprecated_in((1, 16, 0)))
454
455
    def __init__(self, _stack, **kwargs):
455
456
        _BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
456
457
        self.parent = _stack.top()
461
462
 
462
463
    def update(self, msg, current_cnt=None, total_cnt=None):
463
464
        self.current = current_cnt
464
 
        self.total = total_cnt
 
465
        if total_cnt is not None:
 
466
            self.total = total_cnt
465
467
        self.message = msg
466
468
        self.child_fraction = 0
467
469
        self.tick()
490
492
    def note(self, *args, **kwargs):
491
493
        self.parent.note(*args, **kwargs)
492
494
 
493
 
 
 
495
 
494
496
def str_tdelta(delt):
495
497
    if delt is None:
496
498
        return "-:--:--"
517
519
 
518
520
    if elapsed < 2.0:                   # not enough time to estimate
519
521
        return None
520
 
    
 
522
 
521
523
    total_duration = float(elapsed) * float(total) / float(current)
522
524
 
523
 
    assert total_duration >= elapsed
524
 
 
525
525
    if last_updates and len(last_updates) >= n_recent:
526
526
        avg = sum(last_updates) / float(len(last_updates))
527
527
        time_left = avg * (total - current)
548
548
            self.cur_phase = 0
549
549
        else:
550
550
            self.cur_phase += 1
551
 
        assert self.cur_phase < self.total 
552
551
        self.pb.update(self.message, self.cur_phase, self.total)
553
552
 
554
553
 
555
 
def run_tests():
556
 
    import doctest
557
 
    result = doctest.testmod()
558
 
    if result[1] > 0:
559
 
        if result[0] == 0:
560
 
            print "All tests passed"
561
 
    else:
562
 
        print "No tests to run"
563
 
 
564
 
 
565
 
def demo():
566
 
    sleep = time.sleep
567
 
    
568
 
    print 'dumb-terminal test:'
569
 
    pb = DotsProgressBar()
570
 
    for i in range(100):
571
 
        pb.update('Leoparden', i, 99)
572
 
        sleep(0.1)
573
 
    sleep(1.5)
574
 
    pb.clear()
575
 
    sleep(1.5)
576
 
    
577
 
    print 'smart-terminal test:'
578
 
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
579
 
    for i in range(100):
580
 
        pb.update('Elephanten', i, 99)
581
 
        sleep(0.1)
582
 
    sleep(2)
583
 
    pb.clear()
584
 
    sleep(1)
585
 
 
586
 
    print 'done!'
587
 
 
588
 
if __name__ == "__main__":
589
 
    demo()
 
554
_progress_bar_types = {}
 
555
_progress_bar_types['dummy'] = DummyProgress
 
556
_progress_bar_types['none'] = DummyProgress
 
557
_progress_bar_types['tty'] = TTYProgressBar
 
558
_progress_bar_types['dots'] = DotsProgressBar