1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
2
# Copyright (C) 2005, 2006 Canonical <canonical.com>
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.
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.
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
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.
1
# Copyright (C) 2005, 2006, 2008 Canonical Ltd
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.
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.
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
18
"""Progress indicators.
20
The usual way to use this is via bzrlib.ui.ui_factory.nested_progress_bar which
21
will maintain a ProgressBarStack for you.
23
For direct use, the factory ProgressBar will return an auto-detected progress
24
bar that should match your terminal type. You can manually create a
25
ProgressBarStack too if you need multiple levels of cooperating progress bars.
26
Note that bzrlib's internal functions use the ui module, so if you are using
27
bzrlib it really is best to use bzrlib.ui.ui_factory.
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 collections import deque
45
import bzrlib.errors as errors
46
from bzrlib.trace import mutter
43
from bzrlib.trace import mutter
49
46
def _supports_progress(f):
50
if not hasattr(f, 'isatty'):
47
"""Detect if we can use pretty progress bars on the output stream f.
49
If this returns true we expect that a human may be looking at that
50
output, and that we can repaint a line to update it.
52
isatty = getattr(f, 'isatty', None)
54
57
if os.environ.get('TERM') == 'dumb':
55
58
# e.g. emacs compile window
63
class ProgressTask(object):
64
"""Model component of a progress indicator.
66
Most code that needs to indicate progress should update one of these,
67
and it will in turn update the display, if one is present.
69
Code updating the task may also set fields as hints about how to display
70
it: show_pct, show_spinner, show_eta, show_count, show_bar. UIs
71
will not necessarily respect all these fields.
74
def __init__(self, parent_task=None, ui_factory=None):
75
self._parent_task = parent_task
78
self.current_cnt = None
80
self.ui_factory = ui_factory
82
self.show_spinner = True
83
self.show_eta = False,
84
self.show_count = True
87
def update(self, msg, current_cnt=None, total_cnt=None):
89
self.current_cnt = current_cnt
91
self.total_cnt = total_cnt
92
self.ui_factory.show_progress(self)
98
self.ui_factory.progress_finished(self)
100
def make_sub_task(self):
101
return ProgressTask(self, self.ui_factory)
103
def _overall_completion_fraction(self, child_fraction=0.0):
104
"""Return fractional completion of this task and its parents
106
Returns None if no completion can be computed."""
108
own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
111
if self._parent_task is None:
114
if own_fraction is None:
116
return self._parent_task._overall_completion_fraction(own_fraction)
118
def note(self, fmt_string, *args):
119
"""Record a note without disrupting the progress bar."""
120
# XXX: shouldn't be here; put it in mutter or the ui instead
121
self.ui_factory.note(fmt_string % args)
124
# XXX: shouldn't be here; put it in mutter or the ui instead
125
self.ui_factory.clear_term()
61
128
def ProgressBar(to_file=None, **kwargs):
62
129
"""Abstract factory"""
63
130
if to_file is None:
64
131
to_file = sys.stderr
65
if _supports_progress(to_file):
66
return TTYProgressBar(to_file=to_file, **kwargs)
132
requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
133
# An value of '' or not set reverts to standard processing
134
if requested_bar_type in (None, ''):
135
if _supports_progress(to_file):
136
return TTYProgressBar(to_file=to_file, **kwargs)
138
return DummyProgress(to_file=to_file, **kwargs)
68
return DotsProgressBar(to_file=to_file, **kwargs)
140
# Minor sanitation to prevent spurious errors
141
requested_bar_type = requested_bar_type.lower().strip()
142
# TODO: jam 20060710 Arguably we shouldn't raise an exception
143
# but should instead just disable progress bars if we
144
# don't recognize the type
145
if requested_bar_type not in _progress_bar_types:
146
raise errors.InvalidProgressBarType(requested_bar_type,
147
_progress_bar_types.keys())
148
return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
71
151
class ProgressBarStack(object):
72
152
"""A stack of progress bars."""
262
345
_BaseProgressBar.__init__(self, **kwargs)
263
346
self.spin_pos = 0
264
347
self.width = terminal_width()
265
self.start_time = None
266
self.last_updates = deque()
348
self.last_updates = []
349
self._max_last_updates = 10
267
350
self.child_fraction = 0
351
self._have_output = False
353
def throttle(self, old_msg):
271
354
"""Return True if the bar was updated too recently"""
272
355
# time.time consistently takes 40/4000 ms = 0.01 ms.
273
# but every single update to the pb invokes it.
274
# so we use time.clock which takes 20/4000 ms = 0.005ms
275
# on the downside, time.clock() appears to have approximately
276
# 10ms granularity, so we treat a zero-time change as 'throttled.'
356
# time.clock() is faster, but gives us CPU time, not wall-clock time
358
if self.start_time is not None and (now - self.start_time) < 1:
360
if old_msg != self.last_msg:
279
362
interval = now - self.last_update
280
363
# if interval > 0
281
364
if interval < self.MIN_PAUSE:
284
367
self.last_updates.append(now - self.last_update)
368
# Don't let the queue grow without bound
369
self.last_updates = self.last_updates[-self._max_last_updates:]
285
370
self.last_update = now
289
self.update(self.last_msg, self.last_cnt, self.last_total,
374
self.update(self.last_msg, self.last_cnt, self.last_total,
290
375
self.child_fraction)
292
377
def child_update(self, message, current, total):
297
382
elif self.last_cnt + child_fraction <= self.last_total:
298
383
self.child_fraction = child_fraction
300
mutter('not updating child fraction')
301
384
if self.last_msg is None:
302
385
self.last_msg = ''
305
def update(self, msg, current_cnt=None, total_cnt=None,
307
"""Update and redraw progress bar."""
388
def update(self, msg, current_cnt=None, total_cnt=None,
390
"""Update and redraw progress bar.
309
393
msg = self.last_msg
402
m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
404
assert len(m) < self.width
405
self.to_file.write('\r' + m.ljust(self.width - 1))
489
m = spin_str + bar_str + self.last_msg + count_str \
491
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
492
self._have_output = True
406
493
#self.to_file.flush()
409
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
496
if self._have_output:
497
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
498
self._have_output = False
410
499
#self.to_file.flush()
413
504
class ChildProgress(_BaseProgressBar):
414
505
"""A progress indicator that pushes its data to the parent"""
452
544
def note(self, *args, **kwargs):
453
545
self.parent.note(*args, **kwargs)
548
class InstrumentedProgress(TTYProgressBar):
549
"""TTYProgress variant that tracks outcomes"""
551
def __init__(self, *args, **kwargs):
552
self.always_throttled = True
553
self.never_throttle = False
554
TTYProgressBar.__init__(self, *args, **kwargs)
556
def throttle(self, old_message):
557
if self.never_throttle:
560
result = TTYProgressBar.throttle(self, old_message)
562
self.always_throttled = False
456
565
def str_tdelta(delt):
512
617
self.cur_phase = 0
514
619
self.cur_phase += 1
515
assert self.cur_phase < self.total
516
620
self.pb.update(self.message, self.cur_phase, self.total)
521
result = doctest.testmod()
524
print "All tests passed"
526
print "No tests to run"
532
print 'dumb-terminal test:'
533
pb = DotsProgressBar()
535
pb.update('Leoparden', i, 99)
541
print 'smart-terminal test:'
542
pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
544
pb.update('Elephanten', i, 99)
552
if __name__ == "__main__":
623
_progress_bar_types = {}
624
_progress_bar_types['dummy'] = DummyProgress
625
_progress_bar_types['none'] = DummyProgress
626
_progress_bar_types['tty'] = TTYProgressBar
627
_progress_bar_types['dots'] = DotsProgressBar