/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/progress.py

  • Committer: Martin Pool
  • Date: 2009-03-12 06:36:47 UTC
  • mto: This revision was merged to the branch mainline in revision 4144.
  • Revision ID: mbp@sourcefrog.net-20090312063647-06u94f77mk52dg7k
Small fetch progress tweaks

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2008, 2009 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 manage a conceptual stack of nested activities.
 
22
"""
 
23
 
 
24
 
 
25
import sys
 
26
import time
 
27
import os
 
28
import warnings
 
29
 
 
30
 
 
31
from bzrlib import (
 
32
    errors,
 
33
    osutils,
 
34
    trace,
 
35
    ui,
 
36
    )
 
37
from bzrlib.trace import mutter
 
38
from bzrlib.symbol_versioning import (
 
39
    deprecated_in,
 
40
    deprecated_method,
 
41
    )
 
42
 
 
43
 
 
44
def _supports_progress(f):
 
45
    """Detect if we can use pretty progress bars on the output stream f.
 
46
 
 
47
    If this returns true we expect that a human may be looking at that
 
48
    output, and that we can repaint a line to update it.
 
49
    """
 
50
    isatty = getattr(f, 'isatty', None)
 
51
    if isatty is None:
 
52
        return False
 
53
    if not isatty():
 
54
        return False
 
55
    if os.environ.get('TERM') == 'dumb':
 
56
        # e.g. emacs compile window
 
57
        return False
 
58
    return True
 
59
 
 
60
 
 
61
class ProgressTask(object):
 
62
    """Model component of a progress indicator.
 
63
 
 
64
    Most code that needs to indicate progress should update one of these,
 
65
    and it will in turn update the display, if one is present.
 
66
 
 
67
    Code updating the task may also set fields as hints about how to display
 
68
    it: show_pct, show_spinner, show_eta, show_count, show_bar.  UIs
 
69
    will not necessarily respect all these fields.
 
70
    """
 
71
 
 
72
    def __init__(self, parent_task=None, ui_factory=None):
 
73
        """Construct a new progress task.
 
74
 
 
75
        Normally you should not call this directly but rather through
 
76
        `ui_factory.nested_progress_bar`.
 
77
        """
 
78
        self._parent_task = parent_task
 
79
        self._last_update = 0
 
80
        self.total_cnt = None
 
81
        self.current_cnt = None
 
82
        self.msg = ''
 
83
        self.ui_factory = ui_factory
 
84
        self.show_pct = False
 
85
        self.show_spinner = True
 
86
        self.show_eta = False,
 
87
        self.show_count = True
 
88
        self.show_bar = True
 
89
 
 
90
    def __repr__(self):
 
91
        return '%s(%r/%r, msg=%r)' % (
 
92
            self.__class__.__name__,
 
93
            self.current_cnt,
 
94
            self.total_cnt,
 
95
            self.msg)
 
96
 
 
97
    def update(self, msg, current_cnt=None, total_cnt=None):
 
98
        self.msg = msg
 
99
        self.current_cnt = current_cnt
 
100
        if total_cnt:
 
101
            self.total_cnt = total_cnt
 
102
        self.ui_factory._progress_updated(self)
 
103
 
 
104
    def tick(self):
 
105
        self.update(self.msg)
 
106
 
 
107
    def finished(self):
 
108
        self.ui_factory._progress_finished(self)
 
109
 
 
110
    def make_sub_task(self):
 
111
        return ProgressTask(self, self.ui_factory)
 
112
 
 
113
    def _overall_completion_fraction(self, child_fraction=0.0):
 
114
        """Return fractional completion of this task and its parents
 
115
 
 
116
        Returns None if no completion can be computed."""
 
117
        if self.current_cnt is not None and self.total_cnt:
 
118
            own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
 
119
        else:
 
120
            own_fraction = None
 
121
        if self._parent_task is None:
 
122
            return own_fraction
 
123
        else:
 
124
            if own_fraction is None:
 
125
                own_fraction = 0.0
 
126
            return self._parent_task._overall_completion_fraction(own_fraction)
 
127
 
 
128
    def note(self, fmt_string, *args):
 
129
        """Record a note without disrupting the progress bar."""
 
130
        # XXX: shouldn't be here; put it in mutter or the ui instead
 
131
        if args:
 
132
            self.ui_factory.note(fmt_string % args)
 
133
        else:
 
134
            self.ui_factory.note(fmt_string)
 
135
 
 
136
    def clear(self):
 
137
        # XXX: shouldn't be here; put it in mutter or the ui instead
 
138
        self.ui_factory.clear_term()
 
139
 
 
140
 
 
141
def ProgressBar(to_file=None, **kwargs):
 
142
    """Abstract factory"""
 
143
    if to_file is None:
 
144
        to_file = sys.stderr
 
145
    requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
 
146
    # An value of '' or not set reverts to standard processing
 
147
    if requested_bar_type in (None, ''):
 
148
        if _supports_progress(to_file):
 
149
            return TTYProgressBar(to_file=to_file, **kwargs)
 
150
        else:
 
151
            return DummyProgress(to_file=to_file, **kwargs)
 
152
    else:
 
153
        # Minor sanitation to prevent spurious errors
 
154
        requested_bar_type = requested_bar_type.lower().strip()
 
155
        # TODO: jam 20060710 Arguably we shouldn't raise an exception
 
156
        #       but should instead just disable progress bars if we
 
157
        #       don't recognize the type
 
158
        if requested_bar_type not in _progress_bar_types:
 
159
            raise errors.InvalidProgressBarType(requested_bar_type,
 
160
                                                _progress_bar_types.keys())
 
161
        return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
 
162
 
 
163
 
 
164
class ProgressBarStack(object):
 
165
    """A stack of progress bars.
 
166
 
 
167
    This class is deprecated: instead, ask the ui factory for a new progress
 
168
    task and finish it when it's done.
 
169
    """
 
170
 
 
171
    @deprecated_method(deprecated_in((1, 12, 0)))
 
172
    def __init__(self,
 
173
                 to_file=None,
 
174
                 show_pct=False,
 
175
                 show_spinner=True,
 
176
                 show_eta=False,
 
177
                 show_bar=True,
 
178
                 show_count=True,
 
179
                 to_messages_file=None,
 
180
                 klass=None):
 
181
        """Setup the stack with the parameters the progress bars should have."""
 
182
        if to_file is None:
 
183
            to_file = sys.stderr
 
184
        if to_messages_file is None:
 
185
            to_messages_file = sys.stdout
 
186
        self._to_file = to_file
 
187
        self._show_pct = show_pct
 
188
        self._show_spinner = show_spinner
 
189
        self._show_eta = show_eta
 
190
        self._show_bar = show_bar
 
191
        self._show_count = show_count
 
192
        self._to_messages_file = to_messages_file
 
193
        self._stack = []
 
194
        self._klass = klass or ProgressBar
 
195
 
 
196
    def top(self):
 
197
        if len(self._stack) != 0:
 
198
            return self._stack[-1]
 
199
        else:
 
200
            return None
 
201
 
 
202
    def bottom(self):
 
203
        if len(self._stack) != 0:
 
204
            return self._stack[0]
 
205
        else:
 
206
            return None
 
207
 
 
208
    def get_nested(self):
 
209
        """Return a nested progress bar."""
 
210
        if len(self._stack) == 0:
 
211
            func = self._klass
 
212
        else:
 
213
            func = self.top().child_progress
 
214
        new_bar = func(to_file=self._to_file,
 
215
                       show_pct=self._show_pct,
 
216
                       show_spinner=self._show_spinner,
 
217
                       show_eta=self._show_eta,
 
218
                       show_bar=self._show_bar,
 
219
                       show_count=self._show_count,
 
220
                       to_messages_file=self._to_messages_file,
 
221
                       _stack=self)
 
222
        self._stack.append(new_bar)
 
223
        return new_bar
 
224
 
 
225
    def return_pb(self, bar):
 
226
        """Return bar after its been used."""
 
227
        if bar is not self._stack[-1]:
 
228
            warnings.warn("%r is not currently active" % (bar,))
 
229
        else:
 
230
            self._stack.pop()
 
231
 
 
232
 
 
233
class _BaseProgressBar(object):
 
234
 
 
235
    def __init__(self,
 
236
                 to_file=None,
 
237
                 show_pct=False,
 
238
                 show_spinner=False,
 
239
                 show_eta=False,
 
240
                 show_bar=True,
 
241
                 show_count=True,
 
242
                 to_messages_file=None,
 
243
                 _stack=None):
 
244
        object.__init__(self)
 
245
        if to_file is None:
 
246
            to_file = sys.stderr
 
247
        if to_messages_file is None:
 
248
            to_messages_file = sys.stdout
 
249
        self.to_file = to_file
 
250
        self.to_messages_file = to_messages_file
 
251
        self.last_msg = None
 
252
        self.last_cnt = None
 
253
        self.last_total = None
 
254
        self.show_pct = show_pct
 
255
        self.show_spinner = show_spinner
 
256
        self.show_eta = show_eta
 
257
        self.show_bar = show_bar
 
258
        self.show_count = show_count
 
259
        self._stack = _stack
 
260
        # seed throttler
 
261
        self.MIN_PAUSE = 0.1 # seconds
 
262
        now = time.time()
 
263
        # starting now
 
264
        self.start_time = now
 
265
        # next update should not throttle
 
266
        self.last_update = now - self.MIN_PAUSE - 1
 
267
 
 
268
    def finished(self):
 
269
        """Return this bar to its progress stack."""
 
270
        self.clear()
 
271
        self._stack.return_pb(self)
 
272
 
 
273
    def note(self, fmt_string, *args, **kwargs):
 
274
        """Record a note without disrupting the progress bar."""
 
275
        self.clear()
 
276
        self.to_messages_file.write(fmt_string % args)
 
277
        self.to_messages_file.write('\n')
 
278
 
 
279
    def child_progress(self, **kwargs):
 
280
        return ChildProgress(**kwargs)
 
281
 
 
282
 
 
283
class DummyProgress(_BaseProgressBar):
 
284
    """Progress-bar standin that does nothing.
 
285
 
 
286
    This can be used as the default argument for methods that
 
287
    take an optional progress indicator."""
 
288
 
 
289
    def tick(self):
 
290
        pass
 
291
 
 
292
    def update(self, msg=None, current=None, total=None):
 
293
        pass
 
294
 
 
295
    def child_update(self, message, current, total):
 
296
        pass
 
297
 
 
298
    def clear(self):
 
299
        pass
 
300
 
 
301
    def note(self, fmt_string, *args, **kwargs):
 
302
        """See _BaseProgressBar.note()."""
 
303
 
 
304
    def child_progress(self, **kwargs):
 
305
        return DummyProgress(**kwargs)
 
306
 
 
307
 
 
308
class DotsProgressBar(_BaseProgressBar):
 
309
 
 
310
    def __init__(self, **kwargs):
 
311
        _BaseProgressBar.__init__(self, **kwargs)
 
312
        self.last_msg = None
 
313
        self.need_nl = False
 
314
 
 
315
    def tick(self):
 
316
        self.update()
 
317
 
 
318
    def update(self, msg=None, current_cnt=None, total_cnt=None):
 
319
        if msg and msg != self.last_msg:
 
320
            if self.need_nl:
 
321
                self.to_file.write('\n')
 
322
            self.to_file.write(msg + ': ')
 
323
            self.last_msg = msg
 
324
        self.need_nl = True
 
325
        self.to_file.write('.')
 
326
 
 
327
    def clear(self):
 
328
        if self.need_nl:
 
329
            self.to_file.write('\n')
 
330
        self.need_nl = False
 
331
 
 
332
    def child_update(self, message, current, total):
 
333
        self.tick()
 
334
 
 
335
 
 
336
 
 
337
 
 
338
class TTYProgressBar(_BaseProgressBar):
 
339
    """Progress bar display object.
 
340
 
 
341
    Several options are available to control the display.  These can
 
342
    be passed as parameters to the constructor or assigned at any time:
 
343
 
 
344
    show_pct
 
345
        Show percentage complete.
 
346
    show_spinner
 
347
        Show rotating baton.  This ticks over on every update even
 
348
        if the values don't change.
 
349
    show_eta
 
350
        Show predicted time-to-completion.
 
351
    show_bar
 
352
        Show bar graph.
 
353
    show_count
 
354
        Show numerical counts.
 
355
 
 
356
    The output file should be in line-buffered or unbuffered mode.
 
357
    """
 
358
    SPIN_CHARS = r'/-\|'
 
359
 
 
360
 
 
361
    def __init__(self, **kwargs):
 
362
        from bzrlib.osutils import terminal_width
 
363
        _BaseProgressBar.__init__(self, **kwargs)
 
364
        self.spin_pos = 0
 
365
        self.width = terminal_width()
 
366
        self.last_updates = []
 
367
        self._max_last_updates = 10
 
368
        self.child_fraction = 0
 
369
        self._have_output = False
 
370
 
 
371
    def throttle(self, old_msg):
 
372
        """Return True if the bar was updated too recently"""
 
373
        # time.time consistently takes 40/4000 ms = 0.01 ms.
 
374
        # time.clock() is faster, but gives us CPU time, not wall-clock time
 
375
        now = time.time()
 
376
        if self.start_time is not None and (now - self.start_time) < 1:
 
377
            return True
 
378
        if old_msg != self.last_msg:
 
379
            return False
 
380
        interval = now - self.last_update
 
381
        # if interval > 0
 
382
        if interval < self.MIN_PAUSE:
 
383
            return True
 
384
 
 
385
        self.last_updates.append(now - self.last_update)
 
386
        # Don't let the queue grow without bound
 
387
        self.last_updates = self.last_updates[-self._max_last_updates:]
 
388
        self.last_update = now
 
389
        return False
 
390
 
 
391
    def tick(self):
 
392
        self.update(self.last_msg, self.last_cnt, self.last_total,
 
393
                    self.child_fraction)
 
394
 
 
395
    def child_update(self, message, current, total):
 
396
        if current is not None and total != 0:
 
397
            child_fraction = float(current) / total
 
398
            if self.last_cnt is None:
 
399
                pass
 
400
            elif self.last_cnt + child_fraction <= self.last_total:
 
401
                self.child_fraction = child_fraction
 
402
        if self.last_msg is None:
 
403
            self.last_msg = ''
 
404
        self.tick()
 
405
 
 
406
    def update(self, msg, current_cnt=None, total_cnt=None,
 
407
            child_fraction=0):
 
408
        """Update and redraw progress bar.
 
409
        """
 
410
        if msg is None:
 
411
            msg = self.last_msg
 
412
 
 
413
        if total_cnt is None:
 
414
            total_cnt = self.last_total
 
415
 
 
416
        if current_cnt < 0:
 
417
            current_cnt = 0
 
418
 
 
419
        if current_cnt > total_cnt:
 
420
            total_cnt = current_cnt
 
421
 
 
422
        ## # optional corner case optimisation
 
423
        ## # currently does not seem to fire so costs more than saved.
 
424
        ## # trivial optimal case:
 
425
        ## # NB if callers are doing a clear and restore with
 
426
        ## # the saved values, this will prevent that:
 
427
        ## # in that case add a restore method that calls
 
428
        ## # _do_update or some such
 
429
        ## if (self.last_msg == msg and
 
430
        ##     self.last_cnt == current_cnt and
 
431
        ##     self.last_total == total_cnt and
 
432
        ##     self.child_fraction == child_fraction):
 
433
        ##     return
 
434
 
 
435
        if msg is None:
 
436
            msg = ''
 
437
 
 
438
        old_msg = self.last_msg
 
439
        # save these for the tick() function
 
440
        self.last_msg = msg
 
441
        self.last_cnt = current_cnt
 
442
        self.last_total = total_cnt
 
443
        self.child_fraction = child_fraction
 
444
 
 
445
        # each function call takes 20ms/4000 = 0.005 ms,
 
446
        # but multiple that by 4000 calls -> starts to cost.
 
447
        # so anything to make this function call faster
 
448
        # will improve base 'diff' time by up to 0.1 seconds.
 
449
        if self.throttle(old_msg):
 
450
            return
 
451
 
 
452
        if self.show_eta and self.start_time and self.last_total:
 
453
            eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
 
454
                    self.last_total, last_updates = self.last_updates)
 
455
            eta_str = " " + str_tdelta(eta)
 
456
        else:
 
457
            eta_str = ""
 
458
 
 
459
        if self.show_spinner:
 
460
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
 
461
        else:
 
462
            spin_str = ''
 
463
 
 
464
        # always update this; it's also used for the bar
 
465
        self.spin_pos += 1
 
466
 
 
467
        if self.show_pct and self.last_total and self.last_cnt:
 
468
            pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
 
469
            pct_str = ' (%5.1f%%)' % pct
 
470
        else:
 
471
            pct_str = ''
 
472
 
 
473
        if not self.show_count:
 
474
            count_str = ''
 
475
        elif self.last_cnt is None:
 
476
            count_str = ''
 
477
        elif self.last_total is None:
 
478
            count_str = ' %i' % (self.last_cnt)
 
479
        else:
 
480
            # make both fields the same size
 
481
            t = '%i' % (self.last_total)
 
482
            c = '%*i' % (len(t), self.last_cnt)
 
483
            count_str = ' ' + c + '/' + t
 
484
 
 
485
        if self.show_bar:
 
486
            # progress bar, if present, soaks up all remaining space
 
487
            cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
 
488
                   - len(eta_str) - len(count_str) - 3
 
489
 
 
490
            if self.last_total:
 
491
                # number of markers highlighted in bar
 
492
                markers = int(round(float(cols) *
 
493
                              (self.last_cnt + self.child_fraction) / self.last_total))
 
494
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
 
495
            elif False:
 
496
                # don't know total, so can't show completion.
 
497
                # so just show an expanded spinning thingy
 
498
                m = self.spin_pos % cols
 
499
                ms = (' ' * m + '*').ljust(cols)
 
500
 
 
501
                bar_str = '[' + ms + '] '
 
502
            else:
 
503
                bar_str = ''
 
504
        else:
 
505
            bar_str = ''
 
506
 
 
507
        m = spin_str + bar_str + self.last_msg + count_str \
 
508
            + pct_str + eta_str
 
509
        self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
 
510
        self._have_output = True
 
511
        #self.to_file.flush()
 
512
 
 
513
    def clear(self):
 
514
        if self._have_output:
 
515
            self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
516
        self._have_output = False
 
517
        #self.to_file.flush()
 
518
 
 
519
 
 
520
 
 
521
 
 
522
class ChildProgress(_BaseProgressBar):
 
523
    """A progress indicator that pushes its data to the parent"""
 
524
 
 
525
    def __init__(self, _stack, **kwargs):
 
526
        _BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
 
527
        self.parent = _stack.top()
 
528
        self.current = None
 
529
        self.total = None
 
530
        self.child_fraction = 0
 
531
        self.message = None
 
532
 
 
533
    def update(self, msg, current_cnt=None, total_cnt=None):
 
534
        self.current = current_cnt
 
535
        if total_cnt is not None:
 
536
            self.total = total_cnt
 
537
        self.message = msg
 
538
        self.child_fraction = 0
 
539
        self.tick()
 
540
 
 
541
    def child_update(self, message, current, total):
 
542
        if current is None or total == 0:
 
543
            self.child_fraction = 0
 
544
        else:
 
545
            self.child_fraction = float(current) / total
 
546
        self.tick()
 
547
 
 
548
    def tick(self):
 
549
        if self.current is None:
 
550
            count = None
 
551
        else:
 
552
            count = self.current+self.child_fraction
 
553
            if count > self.total:
 
554
                if __debug__:
 
555
                    mutter('clamping count of %d to %d' % (count, self.total))
 
556
                count = self.total
 
557
        self.parent.child_update(self.message, count, self.total)
 
558
 
 
559
    def clear(self):
 
560
        pass
 
561
 
 
562
    def note(self, *args, **kwargs):
 
563
        self.parent.note(*args, **kwargs)
 
564
 
 
565
 
 
566
class InstrumentedProgress(TTYProgressBar):
 
567
    """TTYProgress variant that tracks outcomes"""
 
568
 
 
569
    def __init__(self, *args, **kwargs):
 
570
        self.always_throttled = True
 
571
        self.never_throttle = False
 
572
        TTYProgressBar.__init__(self, *args, **kwargs)
 
573
 
 
574
    def throttle(self, old_message):
 
575
        if self.never_throttle:
 
576
            result =  False
 
577
        else:
 
578
            result = TTYProgressBar.throttle(self, old_message)
 
579
        if result is False:
 
580
            self.always_throttled = False
 
581
 
 
582
 
 
583
def str_tdelta(delt):
 
584
    if delt is None:
 
585
        return "-:--:--"
 
586
    delt = int(round(delt))
 
587
    return '%d:%02d:%02d' % (delt/3600,
 
588
                             (delt/60) % 60,
 
589
                             delt % 60)
 
590
 
 
591
 
 
592
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
 
593
    if start_time is None:
 
594
        return None
 
595
 
 
596
    if not total:
 
597
        return None
 
598
 
 
599
    if current < enough_samples:
 
600
        return None
 
601
 
 
602
    if current > total:
 
603
        return None                     # wtf?
 
604
 
 
605
    elapsed = time.time() - start_time
 
606
 
 
607
    if elapsed < 2.0:                   # not enough time to estimate
 
608
        return None
 
609
 
 
610
    total_duration = float(elapsed) * float(total) / float(current)
 
611
 
 
612
    if last_updates and len(last_updates) >= n_recent:
 
613
        avg = sum(last_updates) / float(len(last_updates))
 
614
        time_left = avg * (total - current)
 
615
 
 
616
        old_time_left = total_duration - elapsed
 
617
 
 
618
        # We could return the average, or some other value here
 
619
        return (time_left + old_time_left) / 2
 
620
 
 
621
    return total_duration - elapsed
 
622
 
 
623
 
 
624
class ProgressPhase(object):
 
625
    """Update progress object with the current phase"""
 
626
    def __init__(self, message, total, pb):
 
627
        object.__init__(self)
 
628
        self.pb = pb
 
629
        self.message = message
 
630
        self.total = total
 
631
        self.cur_phase = None
 
632
 
 
633
    def next_phase(self):
 
634
        if self.cur_phase is None:
 
635
            self.cur_phase = 0
 
636
        else:
 
637
            self.cur_phase += 1
 
638
        self.pb.update(self.message, self.cur_phase, self.total)
 
639
 
 
640
 
 
641
_progress_bar_types = {}
 
642
_progress_bar_types['dummy'] = DummyProgress
 
643
_progress_bar_types['none'] = DummyProgress
 
644
_progress_bar_types['tty'] = TTYProgressBar
 
645
_progress_bar_types['dots'] = DotsProgressBar