1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
2
# Copyright (C) 2005 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
45
"""Return estimated terminal width.
47
TODO: Do something smart on Windows?
49
TODO: Is there anything that gets a better update when the window
50
is resized while the program is running?
53
return int(os.environ['COLUMNS'])
54
except (IndexError, KeyError, ValueError):
37
from bzrlib.trace import mutter
38
from bzrlib.symbol_versioning import (
58
45
def _supports_progress(f):
59
if not hasattr(f, 'isatty'):
46
"""Detect if we can use pretty progress bars on file F.
48
If this returns true we expect that a human may be looking at that
49
output, and that we can repaint a line to update it.
51
This doesn't check the policy for whether we *should* use them.
53
isatty = getattr(f, 'isatty', None)
63
58
if os.environ.get('TERM') == 'dumb':
64
59
# e.g. emacs compile window
70
def ProgressBar(to_file=sys.stderr, **kwargs):
64
class ProgressTask(object):
65
"""Model component of a progress indicator.
67
Most code that needs to indicate progress should update one of these,
68
and it will in turn update the display, if one is present.
70
Code updating the task may also set fields as hints about how to display
71
it: show_pct, show_spinner, show_eta, show_count, show_bar. UIs
72
will not necessarily respect all these fields.
75
def __init__(self, parent_task=None, ui_factory=None):
76
"""Construct a new progress task.
78
Normally you should not call this directly but rather through
79
`ui_factory.nested_progress_bar`.
81
self._parent_task = parent_task
84
self.current_cnt = None
86
self.ui_factory = ui_factory
88
self.show_spinner = True
89
self.show_eta = False,
90
self.show_count = True
94
return '%s(%r/%r, msg=%r)' % (
95
self.__class__.__name__,
100
def update(self, msg, current_cnt=None, total_cnt=None):
102
self.current_cnt = current_cnt
104
self.total_cnt = total_cnt
105
self.ui_factory._progress_updated(self)
108
self.update(self.msg)
111
self.ui_factory._progress_finished(self)
113
def make_sub_task(self):
114
return ProgressTask(self, self.ui_factory)
116
def _overall_completion_fraction(self, child_fraction=0.0):
117
"""Return fractional completion of this task and its parents
119
Returns None if no completion can be computed."""
120
if self.current_cnt is not None and self.total_cnt:
121
own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
123
# if this task has no estimation, it just passes on directly
124
# whatever the child has measured...
125
own_fraction = child_fraction
126
if self._parent_task is None:
129
if own_fraction is None:
131
return self._parent_task._overall_completion_fraction(own_fraction)
133
def note(self, fmt_string, *args):
134
"""Record a note without disrupting the progress bar."""
135
# XXX: shouldn't be here; put it in mutter or the ui instead
137
self.ui_factory.note(fmt_string % args)
139
self.ui_factory.note(fmt_string)
142
# XXX: shouldn't be here; put it in mutter or the ui instead
143
self.ui_factory.clear_term()
146
@deprecated_function(deprecated_in((1, 16, 0)))
147
def ProgressBar(to_file=None, **kwargs):
71
148
"""Abstract factory"""
72
if _supports_progress(to_file):
73
return TTYProgressBar(to_file=to_file, **kwargs)
151
requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
152
# An value of '' or not set reverts to standard processing
153
if requested_bar_type in (None, ''):
154
if _supports_progress(to_file):
155
return TTYProgressBar(to_file=to_file, **kwargs)
157
return DummyProgress(to_file=to_file, **kwargs)
75
return DotsProgressBar(to_file=to_file, **kwargs)
159
# Minor sanitation to prevent spurious errors
160
requested_bar_type = requested_bar_type.lower().strip()
161
# TODO: jam 20060710 Arguably we shouldn't raise an exception
162
# but should instead just disable progress bars if we
163
# don't recognize the type
164
if requested_bar_type not in _progress_bar_types:
165
raise errors.InvalidProgressBarType(requested_bar_type,
166
_progress_bar_types.keys())
167
return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
78
170
class _BaseProgressBar(object):
79
172
def __init__(self,
82
175
show_spinner=False,
179
to_messages_file=None,
86
181
object.__init__(self)
184
if to_messages_file is None:
185
to_messages_file = sys.stdout
87
186
self.to_file = to_file
187
self.to_messages_file = to_messages_file
89
188
self.last_msg = None
90
189
self.last_cnt = None
91
190
self.last_total = None
94
193
self.show_eta = show_eta
95
194
self.show_bar = show_bar
96
195
self.show_count = show_count
198
self.MIN_PAUSE = 0.1 # seconds
201
self.start_time = now
202
# next update should not throttle
203
self.last_update = now - self.MIN_PAUSE - 1
206
"""Return this bar to its progress stack."""
208
self._stack.return_pb(self)
210
def note(self, fmt_string, *args, **kwargs):
211
"""Record a note without disrupting the progress bar."""
213
self.to_messages_file.write(fmt_string % args)
214
self.to_messages_file.write('\n')
216
@deprecated_function(deprecated_in((1, 16, 0)))
217
def child_progress(self, **kwargs):
218
return ChildProgress(**kwargs)
221
class DummyProgress(_BaseProgressBar):
222
"""Progress-bar standin that does nothing.
224
This can be used as the default argument for methods that
225
take an optional progress indicator."""
230
def update(self, msg=None, current=None, total=None):
233
def child_update(self, message, current, total):
239
def note(self, fmt_string, *args, **kwargs):
240
"""See _BaseProgressBar.note()."""
242
def child_progress(self, **kwargs):
243
return DummyProgress(**kwargs)
100
246
class DotsProgressBar(_BaseProgressBar):
248
@deprecated_function(deprecated_in((1, 16, 0)))
101
249
def __init__(self, **kwargs):
102
250
_BaseProgressBar.__init__(self, **kwargs)
103
251
self.last_msg = None
104
252
self.need_nl = False
109
257
def update(self, msg=None, current_cnt=None, total_cnt=None):
110
258
if msg and msg != self.last_msg:
112
260
self.to_file.write('\n')
114
261
self.to_file.write(msg + ': ')
115
262
self.last_msg = msg
116
263
self.need_nl = True
117
264
self.to_file.write('.')
121
268
self.to_file.write('\n')
271
def child_update(self, message, current, total):
124
275
class TTYProgressBar(_BaseProgressBar):
125
276
"""Progress bar display object.
142
293
The output file should be in line-buffered or unbuffered mode.
144
295
SPIN_CHARS = r'/-\|'
145
MIN_PAUSE = 0.1 # seconds
297
@deprecated_function(deprecated_in((1, 16, 0)))
148
298
def __init__(self, **kwargs):
299
from bzrlib.osutils import terminal_width
149
300
_BaseProgressBar.__init__(self, **kwargs)
150
301
self.spin_pos = 0
151
self.width = _width()
152
self.start_time = None
153
self.last_update = None
302
self.width = terminal_width()
303
self.last_updates = []
304
self._max_last_updates = 10
305
self.child_fraction = 0
306
self._have_output = False
308
def throttle(self, old_msg):
157
309
"""Return True if the bar was updated too recently"""
310
# time.time consistently takes 40/4000 ms = 0.01 ms.
311
# time.clock() is faster, but gives us CPU time, not wall-clock time
158
312
now = time.time()
159
if self.start_time is None:
160
self.start_time = self.last_update = now
313
if self.start_time is not None and (now - self.start_time) < 1:
315
if old_msg != self.last_msg:
163
interval = now - self.last_update
164
if interval > 0 and interval < self.MIN_PAUSE:
317
interval = now - self.last_update
319
if interval < self.MIN_PAUSE:
322
self.last_updates.append(now - self.last_update)
323
# Don't let the queue grow without bound
324
self.last_updates = self.last_updates[-self._max_last_updates:]
167
325
self.last_update = now
172
self.update(self.last_msg, self.last_cnt, self.last_total)
176
def update(self, msg, current_cnt=None, total_cnt=None):
177
"""Update and redraw progress bar."""
329
self.update(self.last_msg, self.last_cnt, self.last_total,
332
def child_update(self, message, current, total):
333
if current is not None and total != 0:
334
child_fraction = float(current) / total
335
if self.last_cnt is None:
337
elif self.last_cnt + child_fraction <= self.last_total:
338
self.child_fraction = child_fraction
339
if self.last_msg is None:
343
def update(self, msg, current_cnt=None, total_cnt=None,
345
"""Update and redraw progress bar.
350
if total_cnt is None:
351
total_cnt = self.last_total
356
if current_cnt > total_cnt:
357
total_cnt = current_cnt
359
## # optional corner case optimisation
360
## # currently does not seem to fire so costs more than saved.
361
## # trivial optimal case:
362
## # NB if callers are doing a clear and restore with
363
## # the saved values, this will prevent that:
364
## # in that case add a restore method that calls
365
## # _do_update or some such
366
## if (self.last_msg == msg and
367
## self.last_cnt == current_cnt and
368
## self.last_total == total_cnt and
369
## self.child_fraction == child_fraction):
375
old_msg = self.last_msg
179
376
# save these for the tick() function
180
377
self.last_msg = msg
181
378
self.last_cnt = current_cnt
182
379
self.last_total = total_cnt
188
assert current_cnt <= total_cnt
190
assert current_cnt >= 0
192
if self.show_eta and self.start_time and total_cnt:
193
eta = get_eta(self.start_time, current_cnt, total_cnt)
380
self.child_fraction = child_fraction
382
# each function call takes 20ms/4000 = 0.005 ms,
383
# but multiple that by 4000 calls -> starts to cost.
384
# so anything to make this function call faster
385
# will improve base 'diff' time by up to 0.1 seconds.
386
if self.throttle(old_msg):
389
if self.show_eta and self.start_time and self.last_total:
390
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
391
self.last_total, last_updates = self.last_updates)
194
392
eta_str = " " + str_tdelta(eta)
198
396
if self.show_spinner:
199
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
397
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
203
401
# always update this; it's also used for the bar
204
402
self.spin_pos += 1
206
if self.show_pct and total_cnt and current_cnt:
207
pct = 100.0 * current_cnt / total_cnt
404
if self.show_pct and self.last_total and self.last_cnt:
405
pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
208
406
pct_str = ' (%5.1f%%)' % pct
212
410
if not self.show_count:
214
elif current_cnt is None:
412
elif self.last_cnt is None:
216
elif total_cnt is None:
217
count_str = ' %i' % (current_cnt)
414
elif self.last_total is None:
415
count_str = ' %i' % (self.last_cnt)
219
417
# make both fields the same size
220
t = '%i' % (total_cnt)
221
c = '%*i' % (len(t), current_cnt)
222
count_str = ' ' + c + '/' + t
418
t = '%i' % (self.last_total)
419
c = '%*i' % (len(t), self.last_cnt)
420
count_str = ' ' + c + '/' + t
224
422
if self.show_bar:
225
423
# progress bar, if present, soaks up all remaining space
226
cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
424
cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
227
425
- len(eta_str) - len(count_str) - 3
230
428
# number of markers highlighted in bar
231
markers = int(round(float(cols) * current_cnt / total_cnt))
429
markers = int(round(float(cols) *
430
(self.last_cnt + self.child_fraction) / self.last_total))
232
431
bar_str = '[' + ('=' * markers).ljust(cols) + '] '
234
433
# don't know total, so can't show completion.
235
434
# so just show an expanded spinning thingy
236
435
m = self.spin_pos % cols
237
436
ms = (' ' * m + '*').ljust(cols)
239
438
bar_str = '[' + ms + '] '
245
m = spin_str + bar_str + msg + count_str + pct_str + eta_str
247
assert len(m) < self.width
248
self.to_file.write('\r' + m.ljust(self.width - 1))
249
#self.to_file.flush()
253
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
254
#self.to_file.flush()
444
m = spin_str + bar_str + self.last_msg + count_str \
446
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
447
self._have_output = True
448
#self.to_file.flush()
451
if self._have_output:
452
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
453
self._have_output = False
454
#self.to_file.flush()
457
class ChildProgress(_BaseProgressBar):
458
"""A progress indicator that pushes its data to the parent"""
460
@deprecated_function(deprecated_in((1, 16, 0)))
461
def __init__(self, _stack, **kwargs):
462
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
463
self.parent = _stack.top()
466
self.child_fraction = 0
469
def update(self, msg, current_cnt=None, total_cnt=None):
470
self.current = current_cnt
471
if total_cnt is not None:
472
self.total = total_cnt
474
self.child_fraction = 0
477
def child_update(self, message, current, total):
478
if current is None or total == 0:
479
self.child_fraction = 0
481
self.child_fraction = float(current) / total
485
if self.current is None:
488
count = self.current+self.child_fraction
489
if count > self.total:
491
mutter('clamping count of %d to %d' % (count, self.total))
493
self.parent.child_update(self.message, count, self.total)
498
def note(self, *args, **kwargs):
499
self.parent.note(*args, **kwargs)
258
502
def str_tdelta(delt):
282
526
if elapsed < 2.0: # not enough time to estimate
285
529
total_duration = float(elapsed) * float(total) / float(current)
287
assert total_duration >= elapsed
531
if last_updates and len(last_updates) >= n_recent:
532
avg = sum(last_updates) / float(len(last_updates))
533
time_left = avg * (total - current)
535
old_time_left = total_duration - elapsed
537
# We could return the average, or some other value here
538
return (time_left + old_time_left) / 2
289
540
return total_duration - elapsed
294
result = doctest.testmod()
297
print "All tests passed"
299
print "No tests to run"
305
print 'dumb-terminal test:'
306
pb = DotsProgressBar()
308
pb.update('Leoparden', i, 99)
314
print 'smart-terminal test:'
315
pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
317
pb.update('Elephanten', i, 99)
325
if __name__ == "__main__":
543
class ProgressPhase(object):
544
"""Update progress object with the current phase"""
545
def __init__(self, message, total, pb):
546
object.__init__(self)
548
self.message = message
550
self.cur_phase = None
552
def next_phase(self):
553
if self.cur_phase is None:
557
self.pb.update(self.message, self.cur_phase, self.total)
560
_progress_bar_types = {}
561
_progress_bar_types['dummy'] = DummyProgress
562
_progress_bar_types['none'] = DummyProgress
563
_progress_bar_types['tty'] = TTYProgressBar
564
_progress_bar_types['dots'] = DotsProgressBar