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
61
def ProgressBar(to_file=sys.stderr, **kwargs):
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
122
self.ui_factory.note(fmt_string % args)
124
self.ui_factory.note(fmt_string)
127
# XXX: shouldn't be here; put it in mutter or the ui instead
128
self.ui_factory.clear_term()
131
def ProgressBar(to_file=None, **kwargs):
62
132
"""Abstract factory"""
63
if _supports_progress(to_file):
64
return TTYProgressBar(to_file=to_file, **kwargs)
135
requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
136
# An value of '' or not set reverts to standard processing
137
if requested_bar_type in (None, ''):
138
if _supports_progress(to_file):
139
return TTYProgressBar(to_file=to_file, **kwargs)
141
return DummyProgress(to_file=to_file, **kwargs)
66
return DotsProgressBar(to_file=to_file, **kwargs)
143
# Minor sanitation to prevent spurious errors
144
requested_bar_type = requested_bar_type.lower().strip()
145
# TODO: jam 20060710 Arguably we shouldn't raise an exception
146
# but should instead just disable progress bars if we
147
# don't recognize the type
148
if requested_bar_type not in _progress_bar_types:
149
raise errors.InvalidProgressBarType(requested_bar_type,
150
_progress_bar_types.keys())
151
return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
69
154
class ProgressBarStack(object):
70
155
"""A stack of progress bars."""
72
157
def __init__(self,
75
160
show_spinner=True,
79
to_messages_file=sys.stdout,
164
to_messages_file=None,
81
166
"""Setup the stack with the parameters the progress bars should have."""
169
if to_messages_file is None:
170
to_messages_file = sys.stdout
82
171
self._to_file = to_file
83
172
self._show_pct = show_pct
84
173
self._show_spinner = show_spinner
121
210
def return_pb(self, bar):
122
211
"""Return bar after its been used."""
123
212
if bar is not self._stack[-1]:
124
raise errors.MissingProgressBarFinish()
213
warnings.warn("%r is not currently active" % (bar,))
128
218
class _BaseProgressBar(object):
130
220
def __init__(self,
133
223
show_spinner=False,
137
to_messages_file=sys.stdout,
227
to_messages_file=None,
139
229
object.__init__(self)
232
if to_messages_file is None:
233
to_messages_file = sys.stdout
140
234
self.to_file = to_file
141
235
self.to_messages_file = to_messages_file
142
236
self.last_msg = None
250
348
_BaseProgressBar.__init__(self, **kwargs)
251
349
self.spin_pos = 0
252
350
self.width = terminal_width()
253
self.start_time = None
254
self.last_updates = deque()
351
self.last_updates = []
352
self._max_last_updates = 10
255
353
self.child_fraction = 0
354
self._have_output = False
356
def throttle(self, old_msg):
259
357
"""Return True if the bar was updated too recently"""
260
358
# time.time consistently takes 40/4000 ms = 0.01 ms.
261
# but every single update to the pb invokes it.
262
# so we use time.clock which takes 20/4000 ms = 0.005ms
263
# on the downside, time.clock() appears to have approximately
264
# 10ms granularity, so we treat a zero-time change as 'throttled.'
359
# time.clock() is faster, but gives us CPU time, not wall-clock time
361
if self.start_time is not None and (now - self.start_time) < 1:
363
if old_msg != self.last_msg:
267
365
interval = now - self.last_update
268
366
# if interval > 0
269
367
if interval < self.MIN_PAUSE:
272
370
self.last_updates.append(now - self.last_update)
371
# Don't let the queue grow without bound
372
self.last_updates = self.last_updates[-self._max_last_updates:]
273
373
self.last_update = now
278
self.update(self.last_msg, self.last_cnt, self.last_total,
377
self.update(self.last_msg, self.last_cnt, self.last_total,
279
378
self.child_fraction)
281
380
def child_update(self, message, current, total):
286
385
elif self.last_cnt + child_fraction <= self.last_total:
287
386
self.child_fraction = child_fraction
289
mutter('not updating child fraction')
290
387
if self.last_msg is None:
291
388
self.last_msg = ''
391
def update(self, msg, current_cnt=None, total_cnt=None,
393
"""Update and redraw progress bar.
295
def update(self, msg, current_cnt=None, total_cnt=None,
297
"""Update and redraw progress bar."""
398
if total_cnt is None:
399
total_cnt = self.last_total
299
401
if current_cnt < 0:
387
m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
389
assert len(m) < self.width
390
self.to_file.write('\r' + m.ljust(self.width - 1))
492
m = spin_str + bar_str + self.last_msg + count_str \
494
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
495
self._have_output = True
391
496
#self.to_file.flush()
394
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
499
if self._have_output:
500
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
501
self._have_output = False
395
502
#self.to_file.flush()
398
507
class ChildProgress(_BaseProgressBar):
399
508
"""A progress indicator that pushes its data to the parent"""
400
510
def __init__(self, _stack, **kwargs):
401
511
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
402
512
self.parent = _stack.top()
436
547
def note(self, *args, **kwargs):
437
548
self.parent.note(*args, **kwargs)
551
class InstrumentedProgress(TTYProgressBar):
552
"""TTYProgress variant that tracks outcomes"""
554
def __init__(self, *args, **kwargs):
555
self.always_throttled = True
556
self.never_throttle = False
557
TTYProgressBar.__init__(self, *args, **kwargs)
559
def throttle(self, old_message):
560
if self.never_throttle:
563
result = TTYProgressBar.throttle(self, old_message)
565
self.always_throttled = False
440
568
def str_tdelta(delt):
496
620
self.cur_phase = 0
498
622
self.cur_phase += 1
499
assert self.cur_phase < self.total
500
623
self.pb.update(self.message, self.cur_phase, self.total)
505
result = doctest.testmod()
508
print "All tests passed"
510
print "No tests to run"
516
print 'dumb-terminal test:'
517
pb = DotsProgressBar()
519
pb.update('Leoparden', i, 99)
525
print 'smart-terminal test:'
526
pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
528
pb.update('Elephanten', i, 99)
536
if __name__ == "__main__":
626
_progress_bar_types = {}
627
_progress_bar_types['dummy'] = DummyProgress
628
_progress_bar_types['none'] = DummyProgress
629
_progress_bar_types['tty'] = TTYProgressBar
630
_progress_bar_types['dots'] = DotsProgressBar