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
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
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
19
"""Simple text-mode progress indicator.
21
To display an indicator, create a ProgressBar object. Call it,
22
passing Progress objects indicating the current state. When done,
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
18
"""Progress indicators.
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.
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.
33
# TODO: If not on a tty perhaps just print '......' for the benefit of IDEs, etc
35
# TODO: Optionally show elapsed time instead/as well as ETA; nicer
36
# when the rate is unpredictable
42
from bzrlib.lazy_import import lazy_import
43
lazy_import(globals(), """
44
31
from bzrlib import (
49
37
from bzrlib.trace import mutter
38
from bzrlib.symbol_versioning import (
52
44
def _supports_progress(f):
45
"""Detect if we can use pretty progress bars on the output stream f.
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.
53
50
isatty = getattr(f, 'isatty', None)
64
_progress_bar_types = {}
61
class ProgressTask(object):
62
"""Model component of a progress indicator.
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.
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.
72
def __init__(self, parent_task=None, ui_factory=None):
73
"""Construct a new progress task.
75
Normally you should not call this directly but rather through
76
`ui_factory.nested_progress_bar`.
78
self._parent_task = parent_task
81
self.current_cnt = None
83
self.ui_factory = ui_factory
85
self.show_spinner = True
86
self.show_eta = False,
87
self.show_count = True
91
return '%s(%r/%r, msg=%r)' % (
92
self.__class__.__name__,
97
def update(self, msg, current_cnt=None, total_cnt=None):
99
self.current_cnt = current_cnt
101
self.total_cnt = total_cnt
102
self.ui_factory._progress_updated(self)
105
self.update(self.msg)
108
self.ui_factory._progress_finished(self)
110
def make_sub_task(self):
111
return ProgressTask(self, self.ui_factory)
113
def _overall_completion_fraction(self, child_fraction=0.0):
114
"""Return fractional completion of this task and its parents
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
120
# if this task has no estimation, it just passes on directly
121
# whatever the child has measured...
122
own_fraction = child_fraction
123
if self._parent_task is None:
126
if own_fraction is None:
128
return self._parent_task._overall_completion_fraction(own_fraction)
130
def note(self, fmt_string, *args):
131
"""Record a note without disrupting the progress bar."""
132
# XXX: shouldn't be here; put it in mutter or the ui instead
134
self.ui_factory.note(fmt_string % args)
136
self.ui_factory.note(fmt_string)
139
# XXX: shouldn't be here; put it in mutter or the ui instead
140
self.ui_factory.clear_term()
67
143
def ProgressBar(to_file=None, **kwargs):
74
150
if _supports_progress(to_file):
75
151
return TTYProgressBar(to_file=to_file, **kwargs)
77
return DotsProgressBar(to_file=to_file, **kwargs)
153
return DummyProgress(to_file=to_file, **kwargs)
79
155
# Minor sanitation to prevent spurious errors
80
156
requested_bar_type = requested_bar_type.lower().strip()
86
162
_progress_bar_types.keys())
87
163
return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
90
166
class ProgressBarStack(object):
91
"""A stack of progress bars."""
167
"""A stack of progress bars.
169
This class is deprecated: instead, ask the ui factory for a new progress
170
task and finish it when it's done.
173
@deprecated_method(deprecated_in((1, 12, 0)))
93
174
def __init__(self,
146
227
def return_pb(self, bar):
147
228
"""Return bar after its been used."""
148
229
if bar is not self._stack[-1]:
149
raise errors.MissingProgressBarFinish()
230
warnings.warn("%r is not currently active" % (bar,))
153
235
class _BaseProgressBar(object):
155
237
def __init__(self,
225
307
return DummyProgress(**kwargs)
228
_progress_bar_types['dummy'] = DummyProgress
229
_progress_bar_types['none'] = DummyProgress
232
310
class DotsProgressBar(_BaseProgressBar):
234
312
def __init__(self, **kwargs):
235
313
_BaseProgressBar.__init__(self, **kwargs)
236
314
self.last_msg = None
237
315
self.need_nl = False
242
320
def update(self, msg=None, current_cnt=None, total_cnt=None):
243
321
if msg and msg != self.last_msg:
313
389
self.last_updates = self.last_updates[-self._max_last_updates:]
314
390
self.last_update = now
318
self.update(self.last_msg, self.last_cnt, self.last_total,
394
self.update(self.last_msg, self.last_cnt, self.last_total,
319
395
self.child_fraction)
321
397
def child_update(self, message, current, total):
326
402
elif self.last_cnt + child_fraction <= self.last_total:
327
403
self.child_fraction = child_fraction
329
mutter('not updating child fraction')
330
404
if self.last_msg is None:
331
405
self.last_msg = ''
334
def update(self, msg, current_cnt=None, total_cnt=None,
336
"""Update and redraw progress bar."""
408
def update(self, msg, current_cnt=None, total_cnt=None,
410
"""Update and redraw progress bar.
338
413
msg = self.last_msg
343
418
if current_cnt < 0:
346
421
if current_cnt > total_cnt:
347
422
total_cnt = current_cnt
349
## # optional corner case optimisation
424
## # optional corner case optimisation
350
425
## # currently does not seem to fire so costs more than saved.
351
426
## # trivial optimal case:
352
427
## # NB if callers are doing a clear and restore with
366
444
self.last_total = total_cnt
367
445
self.child_fraction = child_fraction
369
# each function call takes 20ms/4000 = 0.005 ms,
447
# each function call takes 20ms/4000 = 0.005 ms,
370
448
# but multiple that by 4000 calls -> starts to cost.
371
449
# so anything to make this function call faster
372
450
# will improve base 'diff' time by up to 0.1 seconds.
376
454
if self.show_eta and self.start_time and self.last_total:
377
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
455
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
378
456
self.last_total, last_updates = self.last_updates)
379
457
eta_str = " " + str_tdelta(eta)
383
461
if self.show_spinner:
384
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
462
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
404
482
# make both fields the same size
405
483
t = '%i' % (self.last_total)
406
484
c = '%*i' % (len(t), self.last_cnt)
407
count_str = ' ' + c + '/' + t
485
count_str = ' ' + c + '/' + t
409
487
if self.show_bar:
410
488
# progress bar, if present, soaks up all remaining space
421
499
# so just show an expanded spinning thingy
422
500
m = self.spin_pos % cols
423
501
ms = (' ' * m + '*').ljust(cols)
425
503
bar_str = '[' + ms + '] '
431
m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
509
m = spin_str + bar_str + self.last_msg + count_str \
432
511
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
433
512
self._have_output = True
434
513
#self.to_file.flush()
437
516
if self._have_output:
438
517
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
439
518
self._have_output = False
440
#self.to_file.flush()
443
_progress_bar_types['tty'] = TTYProgressBar
519
#self.to_file.flush()
446
524
class ChildProgress(_BaseProgressBar):
485
564
def note(self, *args, **kwargs):
486
565
self.parent.note(*args, **kwargs)
568
class InstrumentedProgress(TTYProgressBar):
569
"""TTYProgress variant that tracks outcomes"""
571
def __init__(self, *args, **kwargs):
572
self.always_throttled = True
573
self.never_throttle = False
574
TTYProgressBar.__init__(self, *args, **kwargs)
576
def throttle(self, old_message):
577
if self.never_throttle:
580
result = TTYProgressBar.throttle(self, old_message)
582
self.always_throttled = False
489
585
def str_tdelta(delt):
513
609
if elapsed < 2.0: # not enough time to estimate
516
612
total_duration = float(elapsed) * float(total) / float(current)
518
assert total_duration >= elapsed
520
614
if last_updates and len(last_updates) >= n_recent:
521
615
avg = sum(last_updates) / float(len(last_updates))
522
616
time_left = avg * (total - current)
543
637
self.cur_phase = 0
545
639
self.cur_phase += 1
546
assert self.cur_phase < self.total
547
640
self.pb.update(self.message, self.cur_phase, self.total)
552
result = doctest.testmod()
555
print "All tests passed"
557
print "No tests to run"
563
print 'dumb-terminal test:'
564
pb = DotsProgressBar()
566
pb.update('Leoparden', i, 99)
572
print 'smart-terminal test:'
573
pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
575
pb.update('Elephanten', i, 99)
583
if __name__ == "__main__":
643
_progress_bar_types = {}
644
_progress_bar_types['dummy'] = DummyProgress
645
_progress_bar_types['none'] = DummyProgress
646
_progress_bar_types['tty'] = TTYProgressBar
647
_progress_bar_types['dots'] = DotsProgressBar