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., 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 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
44
def _supports_progress(f):
59
if not hasattr(f, 'isatty'):
45
"""Detect if we can use pretty progress bars on the output stream f.
47
If this returns true we expect that a human may be looking at that
48
output, and that we can repaint a line to update it.
50
isatty = getattr(f, 'isatty', None)
63
55
if os.environ.get('TERM') == 'dumb':
64
56
# e.g. emacs compile window
70
def ProgressBar(to_file=sys.stderr, **kwargs):
61
class ProgressTask(object):
62
"""Model component of a progress indicator.
64
Most code that needs to indicate progress should update one of these,
65
and it will in turn update the display, if one is present.
67
Code updating the task may also set fields as hints about how to display
68
it: show_pct, show_spinner, show_eta, show_count, show_bar. UIs
69
will not necessarily respect all these fields.
72
def __init__(self, parent_task=None, ui_factory=None):
73
self._parent_task = parent_task
76
self.current_cnt = None
78
self.ui_factory = ui_factory
80
self.show_spinner = True
81
self.show_eta = False,
82
self.show_count = True
86
return '%s(%r/%r, msg=%r)' % (
87
self.__class__.__name__,
92
def update(self, msg, current_cnt=None, total_cnt=None):
94
self.current_cnt = current_cnt
96
self.total_cnt = total_cnt
97
self.ui_factory._progress_updated(self)
100
self.update(self.msg)
103
self.ui_factory._progress_finished(self)
105
def make_sub_task(self):
106
return ProgressTask(self, self.ui_factory)
108
def _overall_completion_fraction(self, child_fraction=0.0):
109
"""Return fractional completion of this task and its parents
111
Returns None if no completion can be computed."""
112
if self.current_cnt is not None and self.total_cnt:
113
own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
116
if self._parent_task is None:
119
if own_fraction is None:
121
return self._parent_task._overall_completion_fraction(own_fraction)
123
def note(self, fmt_string, *args):
124
"""Record a note without disrupting the progress bar."""
125
# XXX: shouldn't be here; put it in mutter or the ui instead
127
self.ui_factory.note(fmt_string % args)
129
self.ui_factory.note(fmt_string)
132
# XXX: shouldn't be here; put it in mutter or the ui instead
133
self.ui_factory.clear_term()
136
def ProgressBar(to_file=None, **kwargs):
71
137
"""Abstract factory"""
72
if _supports_progress(to_file):
73
return TTYProgressBar(to_file=to_file, **kwargs)
140
requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
141
# An value of '' or not set reverts to standard processing
142
if requested_bar_type in (None, ''):
143
if _supports_progress(to_file):
144
return TTYProgressBar(to_file=to_file, **kwargs)
146
return DummyProgress(to_file=to_file, **kwargs)
75
return DotsProgressBar(to_file=to_file, **kwargs)
148
# Minor sanitation to prevent spurious errors
149
requested_bar_type = requested_bar_type.lower().strip()
150
# TODO: jam 20060710 Arguably we shouldn't raise an exception
151
# but should instead just disable progress bars if we
152
# don't recognize the type
153
if requested_bar_type not in _progress_bar_types:
154
raise errors.InvalidProgressBarType(requested_bar_type,
155
_progress_bar_types.keys())
156
return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
159
class ProgressBarStack(object):
160
"""A stack of progress bars.
162
This class is deprecated: instead, ask the ui factory for a new progress
163
task and finish it when it's done.
166
@deprecated_method(deprecated_in((1, 12, 0)))
174
to_messages_file=None,
176
"""Setup the stack with the parameters the progress bars should have."""
179
if to_messages_file is None:
180
to_messages_file = sys.stdout
181
self._to_file = to_file
182
self._show_pct = show_pct
183
self._show_spinner = show_spinner
184
self._show_eta = show_eta
185
self._show_bar = show_bar
186
self._show_count = show_count
187
self._to_messages_file = to_messages_file
189
self._klass = klass or ProgressBar
192
if len(self._stack) != 0:
193
return self._stack[-1]
198
if len(self._stack) != 0:
199
return self._stack[0]
203
def get_nested(self):
204
"""Return a nested progress bar."""
205
if len(self._stack) == 0:
208
func = self.top().child_progress
209
new_bar = func(to_file=self._to_file,
210
show_pct=self._show_pct,
211
show_spinner=self._show_spinner,
212
show_eta=self._show_eta,
213
show_bar=self._show_bar,
214
show_count=self._show_count,
215
to_messages_file=self._to_messages_file,
217
self._stack.append(new_bar)
220
def return_pb(self, bar):
221
"""Return bar after its been used."""
222
if bar is not self._stack[-1]:
223
warnings.warn("%r is not currently active" % (bar,))
78
228
class _BaseProgressBar(object):
79
230
def __init__(self,
82
233
show_spinner=False,
237
to_messages_file=None,
86
239
object.__init__(self)
242
if to_messages_file is None:
243
to_messages_file = sys.stdout
87
244
self.to_file = to_file
245
self.to_messages_file = to_messages_file
89
246
self.last_msg = None
90
247
self.last_cnt = None
91
248
self.last_total = None
157
351
The output file should be in line-buffered or unbuffered mode.
159
353
SPIN_CHARS = r'/-\|'
160
MIN_PAUSE = 0.1 # seconds
163
356
def __init__(self, **kwargs):
357
from bzrlib.osutils import terminal_width
164
358
_BaseProgressBar.__init__(self, **kwargs)
165
359
self.spin_pos = 0
166
self.width = _width()
167
self.start_time = None
168
self.last_update = None
360
self.width = terminal_width()
361
self.last_updates = []
362
self._max_last_updates = 10
363
self.child_fraction = 0
364
self._have_output = False
366
def throttle(self, old_msg):
172
367
"""Return True if the bar was updated too recently"""
368
# time.time consistently takes 40/4000 ms = 0.01 ms.
369
# time.clock() is faster, but gives us CPU time, not wall-clock time
173
370
now = time.time()
174
if self.start_time is None:
175
self.start_time = self.last_update = now
371
if self.start_time is not None and (now - self.start_time) < 1:
373
if old_msg != self.last_msg:
178
interval = now - self.last_update
179
if interval > 0 and interval < self.MIN_PAUSE:
375
interval = now - self.last_update
377
if interval < self.MIN_PAUSE:
380
self.last_updates.append(now - self.last_update)
381
# Don't let the queue grow without bound
382
self.last_updates = self.last_updates[-self._max_last_updates:]
182
383
self.last_update = now
187
self.update(self.last_msg, self.last_cnt, self.last_total)
191
def update(self, msg, current_cnt=None, total_cnt=None):
192
"""Update and redraw progress bar."""
387
self.update(self.last_msg, self.last_cnt, self.last_total,
390
def child_update(self, message, current, total):
391
if current is not None and total != 0:
392
child_fraction = float(current) / total
393
if self.last_cnt is None:
395
elif self.last_cnt + child_fraction <= self.last_total:
396
self.child_fraction = child_fraction
397
if self.last_msg is None:
401
def update(self, msg, current_cnt=None, total_cnt=None,
403
"""Update and redraw progress bar.
408
if total_cnt is None:
409
total_cnt = self.last_total
414
if current_cnt > total_cnt:
415
total_cnt = current_cnt
417
## # optional corner case optimisation
418
## # currently does not seem to fire so costs more than saved.
419
## # trivial optimal case:
420
## # NB if callers are doing a clear and restore with
421
## # the saved values, this will prevent that:
422
## # in that case add a restore method that calls
423
## # _do_update or some such
424
## if (self.last_msg == msg and
425
## self.last_cnt == current_cnt and
426
## self.last_total == total_cnt and
427
## self.child_fraction == child_fraction):
433
old_msg = self.last_msg
194
434
# save these for the tick() function
195
435
self.last_msg = msg
196
436
self.last_cnt = current_cnt
197
437
self.last_total = total_cnt
203
assert current_cnt <= total_cnt
205
assert current_cnt >= 0
207
if self.show_eta and self.start_time and total_cnt:
208
eta = get_eta(self.start_time, current_cnt, total_cnt)
438
self.child_fraction = child_fraction
440
# each function call takes 20ms/4000 = 0.005 ms,
441
# but multiple that by 4000 calls -> starts to cost.
442
# so anything to make this function call faster
443
# will improve base 'diff' time by up to 0.1 seconds.
444
if self.throttle(old_msg):
447
if self.show_eta and self.start_time and self.last_total:
448
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
449
self.last_total, last_updates = self.last_updates)
209
450
eta_str = " " + str_tdelta(eta)
213
454
if self.show_spinner:
214
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
455
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
218
459
# always update this; it's also used for the bar
219
460
self.spin_pos += 1
221
if self.show_pct and total_cnt and current_cnt:
222
pct = 100.0 * current_cnt / total_cnt
462
if self.show_pct and self.last_total and self.last_cnt:
463
pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
223
464
pct_str = ' (%5.1f%%)' % pct
227
468
if not self.show_count:
229
elif current_cnt is None:
470
elif self.last_cnt is None:
231
elif total_cnt is None:
232
count_str = ' %i' % (current_cnt)
472
elif self.last_total is None:
473
count_str = ' %i' % (self.last_cnt)
234
475
# make both fields the same size
235
t = '%i' % (total_cnt)
236
c = '%*i' % (len(t), current_cnt)
237
count_str = ' ' + c + '/' + t
476
t = '%i' % (self.last_total)
477
c = '%*i' % (len(t), self.last_cnt)
478
count_str = ' ' + c + '/' + t
239
480
if self.show_bar:
240
481
# progress bar, if present, soaks up all remaining space
241
cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
482
cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
242
483
- len(eta_str) - len(count_str) - 3
245
486
# number of markers highlighted in bar
246
markers = int(round(float(cols) * current_cnt / total_cnt))
487
markers = int(round(float(cols) *
488
(self.last_cnt + self.child_fraction) / self.last_total))
247
489
bar_str = '[' + ('=' * markers).ljust(cols) + '] '
249
491
# don't know total, so can't show completion.
250
492
# so just show an expanded spinning thingy
251
493
m = self.spin_pos % cols
252
494
ms = (' ' * m + '*').ljust(cols)
254
496
bar_str = '[' + ms + '] '
260
m = spin_str + bar_str + msg + count_str + pct_str + eta_str
262
assert len(m) < self.width
263
self.to_file.write('\r' + m.ljust(self.width - 1))
264
#self.to_file.flush()
268
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
269
#self.to_file.flush()
502
m = spin_str + bar_str + self.last_msg + count_str \
504
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
505
self._have_output = True
506
#self.to_file.flush()
509
if self._have_output:
510
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
511
self._have_output = False
512
#self.to_file.flush()
517
class ChildProgress(_BaseProgressBar):
518
"""A progress indicator that pushes its data to the parent"""
520
def __init__(self, _stack, **kwargs):
521
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
522
self.parent = _stack.top()
525
self.child_fraction = 0
528
def update(self, msg, current_cnt=None, total_cnt=None):
529
self.current = current_cnt
530
if total_cnt is not None:
531
self.total = total_cnt
533
self.child_fraction = 0
536
def child_update(self, message, current, total):
537
if current is None or total == 0:
538
self.child_fraction = 0
540
self.child_fraction = float(current) / total
544
if self.current is None:
547
count = self.current+self.child_fraction
548
if count > self.total:
550
mutter('clamping count of %d to %d' % (count, self.total))
552
self.parent.child_update(self.message, count, self.total)
557
def note(self, *args, **kwargs):
558
self.parent.note(*args, **kwargs)
561
class InstrumentedProgress(TTYProgressBar):
562
"""TTYProgress variant that tracks outcomes"""
564
def __init__(self, *args, **kwargs):
565
self.always_throttled = True
566
self.never_throttle = False
567
TTYProgressBar.__init__(self, *args, **kwargs)
569
def throttle(self, old_message):
570
if self.never_throttle:
573
result = TTYProgressBar.throttle(self, old_message)
575
self.always_throttled = False
273
578
def str_tdelta(delt):
297
602
if elapsed < 2.0: # not enough time to estimate
300
605
total_duration = float(elapsed) * float(total) / float(current)
302
assert total_duration >= elapsed
607
if last_updates and len(last_updates) >= n_recent:
608
avg = sum(last_updates) / float(len(last_updates))
609
time_left = avg * (total - current)
611
old_time_left = total_duration - elapsed
613
# We could return the average, or some other value here
614
return (time_left + old_time_left) / 2
304
616
return total_duration - elapsed
309
result = doctest.testmod()
312
print "All tests passed"
314
print "No tests to run"
320
print 'dumb-terminal test:'
321
pb = DotsProgressBar()
323
pb.update('Leoparden', i, 99)
329
print 'smart-terminal test:'
330
pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
332
pb.update('Elephanten', i, 99)
340
if __name__ == "__main__":
619
class ProgressPhase(object):
620
"""Update progress object with the current phase"""
621
def __init__(self, message, total, pb):
622
object.__init__(self)
624
self.message = message
626
self.cur_phase = None
628
def next_phase(self):
629
if self.cur_phase is None:
633
self.pb.update(self.message, self.cur_phase, self.total)
636
_progress_bar_types = {}
637
_progress_bar_types['dummy'] = DummyProgress
638
_progress_bar_types['none'] = DummyProgress
639
_progress_bar_types['tty'] = TTYProgressBar
640
_progress_bar_types['dots'] = DotsProgressBar