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, 2009 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., 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 collections import deque
45
import bzrlib.errors as errors
46
from bzrlib.trace import mutter
33
from bzrlib.trace import mutter
34
from bzrlib.symbol_versioning import (
40
# XXX: deprecated; can be removed when the ProgressBar factory is removed
49
41
def _supports_progress(f):
50
if not hasattr(f, 'isatty'):
42
"""Detect if we can use pretty progress bars on the output stream f.
44
If this returns true we expect that a human may be looking at that
45
output, and that we can repaint a line to update it.
47
isatty = getattr(f, 'isatty', None)
54
52
if os.environ.get('TERM') == 'dumb':
55
53
# e.g. emacs compile window
61
def ProgressBar(to_file=sys.stderr, **kwargs):
58
class ProgressTask(object):
59
"""Model component of a progress indicator.
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.
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.
69
def __init__(self, parent_task=None, ui_factory=None):
70
"""Construct a new progress task.
72
Normally you should not call this directly but rather through
73
`ui_factory.nested_progress_bar`.
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
88
return '%s(%r/%r, msg=%r)' % (
89
self.__class__.__name__,
94
def update(self, msg, current_cnt=None, total_cnt=None):
96
self.current_cnt = current_cnt
98
self.total_cnt = total_cnt
99
self.ui_factory._progress_updated(self)
102
self.update(self.msg)
105
self.ui_factory._progress_finished(self)
107
def make_sub_task(self):
108
return ProgressTask(self, self.ui_factory)
110
def _overall_completion_fraction(self, child_fraction=0.0):
111
"""Return fractional completion of this task and its parents
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
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:
123
if own_fraction is None:
125
return self._parent_task._overall_completion_fraction(own_fraction)
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
131
self.ui_factory.note(fmt_string % args)
133
self.ui_factory.note(fmt_string)
136
# XXX: shouldn't be here; put it in mutter or the ui instead
137
self.ui_factory.clear_term()
140
@deprecated_function(deprecated_in((1, 16, 0)))
141
def ProgressBar(to_file=None, **kwargs):
62
142
"""Abstract factory"""
63
if _supports_progress(to_file):
64
return TTYProgressBar(to_file=to_file, **kwargs)
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)
151
return DummyProgress(to_file=to_file, **kwargs)
66
return DotsProgressBar(to_file=to_file, **kwargs)
69
class ProgressBarStack(object):
70
"""A stack of progress bars."""
79
to_messages_file=sys.stdout,
81
"""Setup the stack with the parameters the progress bars should have."""
82
self._to_file = to_file
83
self._show_pct = show_pct
84
self._show_spinner = show_spinner
85
self._show_eta = show_eta
86
self._show_bar = show_bar
87
self._show_count = show_count
88
self._to_messages_file = to_messages_file
90
self._klass = klass or TTYProgressBar
93
if len(self._stack) != 0:
94
return self._stack[-1]
99
if len(self._stack) != 0:
100
return self._stack[0]
104
def get_nested(self):
105
"""Return a nested progress bar."""
106
if len(self._stack) == 0:
109
func = self.top().child_progress
110
new_bar = func(to_file=self._to_file,
111
show_pct=self._show_pct,
112
show_spinner=self._show_spinner,
113
show_eta=self._show_eta,
114
show_bar=self._show_bar,
115
show_count=self._show_count,
116
to_messages_file=self._to_messages_file,
118
self._stack.append(new_bar)
121
def return_pb(self, bar):
122
"""Return bar after its been used."""
123
if bar is not self._stack[-1]:
124
raise errors.MissingProgressBarFinish()
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)
128
164
class _BaseProgressBar(object):
130
166
def __init__(self,
133
169
show_spinner=False,
137
to_messages_file=sys.stdout,
173
to_messages_file=None,
139
175
object.__init__(self)
178
if to_messages_file is None:
179
to_messages_file = sys.stdout
140
180
self.to_file = to_file
141
181
self.to_messages_file = to_messages_file
142
182
self.last_msg = None
192
233
def note(self, fmt_string, *args, **kwargs):
193
234
"""See _BaseProgressBar.note()."""
195
236
def child_progress(self, **kwargs):
196
237
return DummyProgress(**kwargs)
198
240
class DotsProgressBar(_BaseProgressBar):
242
@deprecated_function(deprecated_in((1, 16, 0)))
200
243
def __init__(self, **kwargs):
201
244
_BaseProgressBar.__init__(self, **kwargs)
202
245
self.last_msg = None
203
246
self.need_nl = False
208
251
def update(self, msg=None, current_cnt=None, total_cnt=None):
209
252
if msg and msg != self.last_msg:
211
254
self.to_file.write('\n')
213
255
self.to_file.write(msg + ': ')
214
256
self.last_msg = msg
215
257
self.need_nl = True
216
258
self.to_file.write('.')
220
262
self.to_file.write('\n')
222
265
def child_update(self, message, current, total):
225
269
class TTYProgressBar(_BaseProgressBar):
226
270
"""Progress bar display object.
245
289
SPIN_CHARS = r'/-\|'
291
@deprecated_function(deprecated_in((1, 16, 0)))
248
292
def __init__(self, **kwargs):
249
293
from bzrlib.osutils import terminal_width
250
294
_BaseProgressBar.__init__(self, **kwargs)
251
295
self.spin_pos = 0
252
296
self.width = terminal_width()
253
self.start_time = None
254
self.last_updates = deque()
297
self.last_updates = []
298
self._max_last_updates = 10
255
299
self.child_fraction = 0
300
self._have_output = False
302
def throttle(self, old_msg):
259
303
"""Return True if the bar was updated too recently"""
260
304
# 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.'
305
# time.clock() is faster, but gives us CPU time, not wall-clock time
307
if self.start_time is not None and (now - self.start_time) < 1:
309
if old_msg != self.last_msg:
267
311
interval = now - self.last_update
268
312
# if interval > 0
269
313
if interval < self.MIN_PAUSE:
272
316
self.last_updates.append(now - self.last_update)
317
# Don't let the queue grow without bound
318
self.last_updates = self.last_updates[-self._max_last_updates:]
273
319
self.last_update = now
278
self.update(self.last_msg, self.last_cnt, self.last_total,
323
self.update(self.last_msg, self.last_cnt, self.last_total,
279
324
self.child_fraction)
281
326
def child_update(self, message, current, total):
286
331
elif self.last_cnt + child_fraction <= self.last_total:
287
332
self.child_fraction = child_fraction
289
mutter('not updating child fraction')
290
333
if self.last_msg is None:
291
334
self.last_msg = ''
337
def update(self, msg, current_cnt=None, total_cnt=None,
339
"""Update and redraw progress bar.
295
def update(self, msg, current_cnt=None, total_cnt=None,
297
"""Update and redraw progress bar."""
344
if total_cnt is None:
345
total_cnt = self.last_total
299
347
if current_cnt < 0:
302
350
if current_cnt > total_cnt:
303
351
total_cnt = current_cnt
305
## # optional corner case optimisation
353
## # optional corner case optimisation
306
354
## # currently does not seem to fire so costs more than saved.
307
355
## # trivial optimal case:
308
356
## # NB if callers are doing a clear and restore with
322
373
self.last_total = total_cnt
323
374
self.child_fraction = child_fraction
325
# each function call takes 20ms/4000 = 0.005 ms,
376
# each function call takes 20ms/4000 = 0.005 ms,
326
377
# but multiple that by 4000 calls -> starts to cost.
327
378
# so anything to make this function call faster
328
379
# will improve base 'diff' time by up to 0.1 seconds.
329
if old_msg == self.last_msg and self.throttle():
380
if self.throttle(old_msg):
332
383
if self.show_eta and self.start_time and self.last_total:
333
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,
334
385
self.last_total, last_updates = self.last_updates)
335
386
eta_str = " " + str_tdelta(eta)
339
390
if self.show_spinner:
340
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
391
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
377
428
# so just show an expanded spinning thingy
378
429
m = self.spin_pos % cols
379
430
ms = (' ' * m + '*').ljust(cols)
381
432
bar_str = '[' + ms + '] '
387
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 \
440
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
441
self._have_output = True
442
#self.to_file.flush()
389
assert len(m) < self.width
390
self.to_file.write('\r' + m.ljust(self.width - 1))
445
if self._have_output:
446
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
447
self._have_output = False
391
448
#self.to_file.flush()
394
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
395
#self.to_file.flush()
398
451
class ChildProgress(_BaseProgressBar):
399
452
"""A progress indicator that pushes its data to the parent"""
454
@deprecated_function(deprecated_in((1, 16, 0)))
400
455
def __init__(self, _stack, **kwargs):
401
456
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
402
457
self.parent = _stack.top()