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):
33
from bzrlib.trace import mutter
34
from bzrlib.symbol_versioning import (
40
# XXX: deprecated; can be removed when the ProgressBar factory is removed
58
41
def _supports_progress(f):
59
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)
63
52
if os.environ.get('TERM') == 'dumb':
64
53
# e.g. emacs compile window
70
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):
71
142
"""Abstract factory"""
72
if _supports_progress(to_file):
73
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)
75
return DotsProgressBar(to_file=to_file, **kwargs)
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)
78
164
class _BaseProgressBar(object):
79
166
def __init__(self,
82
169
show_spinner=False,
173
to_messages_file=None,
86
175
object.__init__(self)
178
if to_messages_file is None:
179
to_messages_file = sys.stdout
87
180
self.to_file = to_file
181
self.to_messages_file = to_messages_file
89
182
self.last_msg = None
90
183
self.last_cnt = None
91
184
self.last_total = None
94
187
self.show_eta = show_eta
95
188
self.show_bar = show_bar
96
189
self.show_count = show_count
192
self.MIN_PAUSE = 0.1 # seconds
195
self.start_time = now
196
# next update should not throttle
197
self.last_update = now - self.MIN_PAUSE - 1
200
"""Return this bar to its progress stack."""
202
self._stack.return_pb(self)
204
def note(self, fmt_string, *args, **kwargs):
205
"""Record a note without disrupting the progress bar."""
207
self.to_messages_file.write(fmt_string % args)
208
self.to_messages_file.write('\n')
210
@deprecated_function(deprecated_in((1, 16, 0)))
211
def child_progress(self, **kwargs):
212
return ChildProgress(**kwargs)
215
class DummyProgress(_BaseProgressBar):
216
"""Progress-bar standin that does nothing.
218
This can be used as the default argument for methods that
219
take an optional progress indicator."""
224
def update(self, msg=None, current=None, total=None):
227
def child_update(self, message, current, total):
233
def note(self, fmt_string, *args, **kwargs):
234
"""See _BaseProgressBar.note()."""
236
def child_progress(self, **kwargs):
237
return DummyProgress(**kwargs)
100
240
class DotsProgressBar(_BaseProgressBar):
242
@deprecated_function(deprecated_in((1, 16, 0)))
101
243
def __init__(self, **kwargs):
102
244
_BaseProgressBar.__init__(self, **kwargs)
103
245
self.last_msg = None
104
246
self.need_nl = False
109
251
def update(self, msg=None, current_cnt=None, total_cnt=None):
110
252
if msg and msg != self.last_msg:
112
254
self.to_file.write('\n')
114
255
self.to_file.write(msg + ': ')
115
256
self.last_msg = msg
116
257
self.need_nl = True
117
258
self.to_file.write('.')
121
262
self.to_file.write('\n')
265
def child_update(self, message, current, total):
124
269
class TTYProgressBar(_BaseProgressBar):
125
270
"""Progress bar display object.
142
287
The output file should be in line-buffered or unbuffered mode.
144
289
SPIN_CHARS = r'/-\|'
145
MIN_PAUSE = 0.1 # seconds
291
@deprecated_function(deprecated_in((1, 16, 0)))
148
292
def __init__(self, **kwargs):
293
from bzrlib.osutils import terminal_width
149
294
_BaseProgressBar.__init__(self, **kwargs)
150
295
self.spin_pos = 0
151
self.width = _width()
152
self.start_time = None
153
self.last_update = None
296
self.width = terminal_width()
297
self.last_updates = []
298
self._max_last_updates = 10
299
self.child_fraction = 0
300
self._have_output = False
302
def throttle(self, old_msg):
157
303
"""Return True if the bar was updated too recently"""
304
# time.time consistently takes 40/4000 ms = 0.01 ms.
305
# time.clock() is faster, but gives us CPU time, not wall-clock time
158
306
now = time.time()
159
if self.start_time is None:
160
self.start_time = self.last_update = now
307
if self.start_time is not None and (now - self.start_time) < 1:
309
if old_msg != self.last_msg:
163
interval = now - self.last_update
164
if interval > 0 and interval < self.MIN_PAUSE:
311
interval = now - self.last_update
313
if interval < self.MIN_PAUSE:
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:]
167
319
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."""
323
self.update(self.last_msg, self.last_cnt, self.last_total,
326
def child_update(self, message, current, total):
327
if current is not None and total != 0:
328
child_fraction = float(current) / total
329
if self.last_cnt is None:
331
elif self.last_cnt + child_fraction <= self.last_total:
332
self.child_fraction = child_fraction
333
if self.last_msg is None:
337
def update(self, msg, current_cnt=None, total_cnt=None,
339
"""Update and redraw progress bar.
344
if total_cnt is None:
345
total_cnt = self.last_total
350
if current_cnt > total_cnt:
351
total_cnt = current_cnt
353
## # optional corner case optimisation
354
## # currently does not seem to fire so costs more than saved.
355
## # trivial optimal case:
356
## # NB if callers are doing a clear and restore with
357
## # the saved values, this will prevent that:
358
## # in that case add a restore method that calls
359
## # _do_update or some such
360
## if (self.last_msg == msg and
361
## self.last_cnt == current_cnt and
362
## self.last_total == total_cnt and
363
## self.child_fraction == child_fraction):
369
old_msg = self.last_msg
179
370
# save these for the tick() function
180
371
self.last_msg = msg
181
372
self.last_cnt = current_cnt
182
373
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)
374
self.child_fraction = child_fraction
376
# each function call takes 20ms/4000 = 0.005 ms,
377
# but multiple that by 4000 calls -> starts to cost.
378
# so anything to make this function call faster
379
# will improve base 'diff' time by up to 0.1 seconds.
380
if self.throttle(old_msg):
383
if self.show_eta and self.start_time and self.last_total:
384
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
385
self.last_total, last_updates = self.last_updates)
194
386
eta_str = " " + str_tdelta(eta)
198
390
if self.show_spinner:
199
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
391
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
203
395
# always update this; it's also used for the bar
204
396
self.spin_pos += 1
206
if self.show_pct and total_cnt and current_cnt:
207
pct = 100.0 * current_cnt / total_cnt
398
if self.show_pct and self.last_total and self.last_cnt:
399
pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
208
400
pct_str = ' (%5.1f%%)' % pct
212
404
if not self.show_count:
214
elif current_cnt is None:
406
elif self.last_cnt is None:
216
elif total_cnt is None:
217
count_str = ' %i' % (current_cnt)
408
elif self.last_total is None:
409
count_str = ' %i' % (self.last_cnt)
219
411
# make both fields the same size
220
t = '%i' % (total_cnt)
221
c = '%*i' % (len(t), current_cnt)
222
count_str = ' ' + c + '/' + t
412
t = '%i' % (self.last_total)
413
c = '%*i' % (len(t), self.last_cnt)
414
count_str = ' ' + c + '/' + t
224
416
if self.show_bar:
225
417
# progress bar, if present, soaks up all remaining space
226
cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
418
cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
227
419
- len(eta_str) - len(count_str) - 3
230
422
# number of markers highlighted in bar
231
markers = int(round(float(cols) * current_cnt / total_cnt))
423
markers = int(round(float(cols) *
424
(self.last_cnt + self.child_fraction) / self.last_total))
232
425
bar_str = '[' + ('=' * markers).ljust(cols) + '] '
234
427
# don't know total, so can't show completion.
235
428
# so just show an expanded spinning thingy
236
429
m = self.spin_pos % cols
237
430
ms = (' ' * m + '*').ljust(cols)
239
432
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()
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()
445
if self._have_output:
446
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
447
self._have_output = False
448
#self.to_file.flush()
451
class ChildProgress(_BaseProgressBar):
452
"""A progress indicator that pushes its data to the parent"""
454
@deprecated_function(deprecated_in((1, 16, 0)))
455
def __init__(self, _stack, **kwargs):
456
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
457
self.parent = _stack.top()
460
self.child_fraction = 0
463
def update(self, msg, current_cnt=None, total_cnt=None):
464
self.current = current_cnt
465
if total_cnt is not None:
466
self.total = total_cnt
468
self.child_fraction = 0
471
def child_update(self, message, current, total):
472
if current is None or total == 0:
473
self.child_fraction = 0
475
self.child_fraction = float(current) / total
479
if self.current is None:
482
count = self.current+self.child_fraction
483
if count > self.total:
485
mutter('clamping count of %d to %d' % (count, self.total))
487
self.parent.child_update(self.message, count, self.total)
492
def note(self, *args, **kwargs):
493
self.parent.note(*args, **kwargs)
258
496
def str_tdelta(delt):
282
520
if elapsed < 2.0: # not enough time to estimate
285
523
total_duration = float(elapsed) * float(total) / float(current)
287
assert total_duration >= elapsed
525
if last_updates and len(last_updates) >= n_recent:
526
avg = sum(last_updates) / float(len(last_updates))
527
time_left = avg * (total - current)
529
old_time_left = total_duration - elapsed
531
# We could return the average, or some other value here
532
return (time_left + old_time_left) / 2
289
534
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__":
537
class ProgressPhase(object):
538
"""Update progress object with the current phase"""
539
def __init__(self, message, total, pb):
540
object.__init__(self)
542
self.message = message
544
self.cur_phase = None
546
def next_phase(self):
547
if self.cur_phase is None:
551
self.pb.update(self.message, self.cur_phase, self.total)
554
_progress_bar_types = {}
555
_progress_bar_types['dummy'] = DummyProgress
556
_progress_bar_types['none'] = DummyProgress
557
_progress_bar_types['tty'] = TTYProgressBar
558
_progress_bar_types['dots'] = DotsProgressBar