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
44
"""Return estimated terminal width.
46
TODO: Do something smart on Windows?
48
TODO: Is there anything that gets a better update when the window
49
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)
64
55
if os.environ.get('TERM') == 'dumb':
65
56
# e.g. emacs compile window
71
class ProgressBar(object):
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
"""Construct a new progress task.
75
Normally you should not call this directly but rather through
76
`ui_factory.nested_progress_bar`.
78
self._parent_task = parent_task
81
self.current_cnt = None
83
self.ui_factory = ui_factory
85
self.show_spinner = True
86
self.show_eta = False,
87
self.show_count = True
91
return '%s(%r/%r, msg=%r)' % (
92
self.__class__.__name__,
97
def update(self, msg, current_cnt=None, total_cnt=None):
99
self.current_cnt = current_cnt
101
self.total_cnt = total_cnt
102
self.ui_factory._progress_updated(self)
105
self.update(self.msg)
108
self.ui_factory._progress_finished(self)
110
def make_sub_task(self):
111
return ProgressTask(self, self.ui_factory)
113
def _overall_completion_fraction(self, child_fraction=0.0):
114
"""Return fractional completion of this task and its parents
116
Returns None if no completion can be computed."""
117
if self.current_cnt is not None and self.total_cnt:
118
own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
120
# if this task has no estimation, it just passes on directly
121
# whatever the child has measured...
122
own_fraction = child_fraction
123
if self._parent_task is None:
126
if own_fraction is None:
128
return self._parent_task._overall_completion_fraction(own_fraction)
130
def note(self, fmt_string, *args):
131
"""Record a note without disrupting the progress bar."""
132
# XXX: shouldn't be here; put it in mutter or the ui instead
134
self.ui_factory.note(fmt_string % args)
136
self.ui_factory.note(fmt_string)
139
# XXX: shouldn't be here; put it in mutter or the ui instead
140
self.ui_factory.clear_term()
143
def ProgressBar(to_file=None, **kwargs):
144
"""Abstract factory"""
147
requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
148
# An value of '' or not set reverts to standard processing
149
if requested_bar_type in (None, ''):
150
if _supports_progress(to_file):
151
return TTYProgressBar(to_file=to_file, **kwargs)
153
return DummyProgress(to_file=to_file, **kwargs)
155
# Minor sanitation to prevent spurious errors
156
requested_bar_type = requested_bar_type.lower().strip()
157
# TODO: jam 20060710 Arguably we shouldn't raise an exception
158
# but should instead just disable progress bars if we
159
# don't recognize the type
160
if requested_bar_type not in _progress_bar_types:
161
raise errors.InvalidProgressBarType(requested_bar_type,
162
_progress_bar_types.keys())
163
return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
166
class ProgressBarStack(object):
167
"""A stack of progress bars.
169
This class is deprecated: instead, ask the ui factory for a new progress
170
task and finish it when it's done.
173
@deprecated_method(deprecated_in((1, 12, 0)))
181
to_messages_file=None,
183
"""Setup the stack with the parameters the progress bars should have."""
186
if to_messages_file is None:
187
to_messages_file = sys.stdout
188
self._to_file = to_file
189
self._show_pct = show_pct
190
self._show_spinner = show_spinner
191
self._show_eta = show_eta
192
self._show_bar = show_bar
193
self._show_count = show_count
194
self._to_messages_file = to_messages_file
196
self._klass = klass or ProgressBar
199
if len(self._stack) != 0:
200
return self._stack[-1]
205
if len(self._stack) != 0:
206
return self._stack[0]
210
def get_nested(self):
211
"""Return a nested progress bar."""
212
if len(self._stack) == 0:
215
func = self.top().child_progress
216
new_bar = func(to_file=self._to_file,
217
show_pct=self._show_pct,
218
show_spinner=self._show_spinner,
219
show_eta=self._show_eta,
220
show_bar=self._show_bar,
221
show_count=self._show_count,
222
to_messages_file=self._to_messages_file,
224
self._stack.append(new_bar)
227
def return_pb(self, bar):
228
"""Return bar after its been used."""
229
if bar is not self._stack[-1]:
230
warnings.warn("%r is not currently active" % (bar,))
235
class _BaseProgressBar(object):
244
to_messages_file=None,
246
object.__init__(self)
249
if to_messages_file is None:
250
to_messages_file = sys.stdout
251
self.to_file = to_file
252
self.to_messages_file = to_messages_file
255
self.last_total = None
256
self.show_pct = show_pct
257
self.show_spinner = show_spinner
258
self.show_eta = show_eta
259
self.show_bar = show_bar
260
self.show_count = show_count
263
self.MIN_PAUSE = 0.1 # seconds
266
self.start_time = now
267
# next update should not throttle
268
self.last_update = now - self.MIN_PAUSE - 1
271
"""Return this bar to its progress stack."""
273
self._stack.return_pb(self)
275
def note(self, fmt_string, *args, **kwargs):
276
"""Record a note without disrupting the progress bar."""
278
self.to_messages_file.write(fmt_string % args)
279
self.to_messages_file.write('\n')
281
def child_progress(self, **kwargs):
282
return ChildProgress(**kwargs)
285
class DummyProgress(_BaseProgressBar):
286
"""Progress-bar standin that does nothing.
288
This can be used as the default argument for methods that
289
take an optional progress indicator."""
294
def update(self, msg=None, current=None, total=None):
297
def child_update(self, message, current, total):
303
def note(self, fmt_string, *args, **kwargs):
304
"""See _BaseProgressBar.note()."""
306
def child_progress(self, **kwargs):
307
return DummyProgress(**kwargs)
310
class DotsProgressBar(_BaseProgressBar):
312
def __init__(self, **kwargs):
313
_BaseProgressBar.__init__(self, **kwargs)
320
def update(self, msg=None, current_cnt=None, total_cnt=None):
321
if msg and msg != self.last_msg:
323
self.to_file.write('\n')
324
self.to_file.write(msg + ': ')
327
self.to_file.write('.')
331
self.to_file.write('\n')
334
def child_update(self, message, current, total):
340
class TTYProgressBar(_BaseProgressBar):
72
341
"""Progress bar display object.
74
343
Several options are available to control the display. These can
89
358
The output file should be in line-buffered or unbuffered mode.
91
360
SPIN_CHARS = r'/-\|'
92
MIN_PAUSE = 0.1 # seconds
104
object.__init__(self)
105
self.to_file = to_file
106
self.suppressed = not _supports_progress(self.to_file)
363
def __init__(self, **kwargs):
364
from bzrlib.osutils import terminal_width
365
_BaseProgressBar.__init__(self, **kwargs)
107
366
self.spin_pos = 0
111
self.last_total = None
112
self.show_pct = show_pct
113
self.show_spinner = show_spinner
114
self.show_eta = show_eta
115
self.show_bar = show_bar
116
self.show_count = show_count
118
self.width = _width()
367
self.width = terminal_width()
368
self.last_updates = []
369
self._max_last_updates = 10
370
self.child_fraction = 0
371
self._have_output = False
373
def throttle(self, old_msg):
374
"""Return True if the bar was updated too recently"""
375
# time.time consistently takes 40/4000 ms = 0.01 ms.
376
# time.clock() is faster, but gives us CPU time, not wall-clock time
378
if self.start_time is not None and (now - self.start_time) < 1:
380
if old_msg != self.last_msg:
382
interval = now - self.last_update
384
if interval < self.MIN_PAUSE:
387
self.last_updates.append(now - self.last_update)
388
# Don't let the queue grow without bound
389
self.last_updates = self.last_updates[-self._max_last_updates:]
390
self.last_update = now
122
self.update(self.last_msg, self.last_cnt, self.last_total)
126
def update(self, msg, current_cnt=None, total_cnt=None):
127
"""Update and redraw progress bar."""
394
self.update(self.last_msg, self.last_cnt, self.last_total,
397
def child_update(self, message, current, total):
398
if current is not None and total != 0:
399
child_fraction = float(current) / total
400
if self.last_cnt is None:
402
elif self.last_cnt + child_fraction <= self.last_total:
403
self.child_fraction = child_fraction
404
if self.last_msg is None:
408
def update(self, msg, current_cnt=None, total_cnt=None,
410
"""Update and redraw progress bar.
415
if total_cnt is None:
416
total_cnt = self.last_total
421
if current_cnt > total_cnt:
422
total_cnt = current_cnt
424
## # optional corner case optimisation
425
## # currently does not seem to fire so costs more than saved.
426
## # trivial optimal case:
427
## # NB if callers are doing a clear and restore with
428
## # the saved values, this will prevent that:
429
## # in that case add a restore method that calls
430
## # _do_update or some such
431
## if (self.last_msg == msg and
432
## self.last_cnt == current_cnt and
433
## self.last_total == total_cnt and
434
## self.child_fraction == child_fraction):
440
old_msg = self.last_msg
131
441
# save these for the tick() function
132
442
self.last_msg = msg
133
443
self.last_cnt = current_cnt
134
444
self.last_total = total_cnt
137
if self.start_time is None:
138
self.start_time = now
140
interval = now - self.last_update
141
if interval > 0 and interval < self.MIN_PAUSE:
144
self.last_update = now
147
assert current_cnt <= total_cnt
149
assert current_cnt >= 0
151
if self.show_eta and self.start_time and total_cnt:
152
eta = get_eta(self.start_time, current_cnt, total_cnt)
445
self.child_fraction = child_fraction
447
# each function call takes 20ms/4000 = 0.005 ms,
448
# but multiple that by 4000 calls -> starts to cost.
449
# so anything to make this function call faster
450
# will improve base 'diff' time by up to 0.1 seconds.
451
if self.throttle(old_msg):
454
if self.show_eta and self.start_time and self.last_total:
455
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
456
self.last_total, last_updates = self.last_updates)
153
457
eta_str = " " + str_tdelta(eta)
157
461
if self.show_spinner:
158
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
462
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
162
466
# always update this; it's also used for the bar
163
467
self.spin_pos += 1
165
if self.show_pct and total_cnt and current_cnt:
166
pct = 100.0 * current_cnt / total_cnt
469
if self.show_pct and self.last_total and self.last_cnt:
470
pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
167
471
pct_str = ' (%5.1f%%)' % pct
171
475
if not self.show_count:
173
elif current_cnt is None:
477
elif self.last_cnt is None:
175
elif total_cnt is None:
176
count_str = ' %i' % (current_cnt)
479
elif self.last_total is None:
480
count_str = ' %i' % (self.last_cnt)
178
482
# make both fields the same size
179
t = '%i' % (total_cnt)
180
c = '%*i' % (len(t), current_cnt)
181
count_str = ' ' + c + '/' + t
483
t = '%i' % (self.last_total)
484
c = '%*i' % (len(t), self.last_cnt)
485
count_str = ' ' + c + '/' + t
183
487
if self.show_bar:
184
488
# progress bar, if present, soaks up all remaining space
185
cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
489
cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
186
490
- len(eta_str) - len(count_str) - 3
189
493
# number of markers highlighted in bar
190
markers = int(round(float(cols) * current_cnt / total_cnt))
494
markers = int(round(float(cols) *
495
(self.last_cnt + self.child_fraction) / self.last_total))
191
496
bar_str = '[' + ('=' * markers).ljust(cols) + '] '
193
498
# don't know total, so can't show completion.
194
499
# so just show an expanded spinning thingy
195
500
m = self.spin_pos % cols
196
501
ms = (' ' * m + '*').ljust(cols)
198
503
bar_str = '[' + ms + '] '
204
m = spin_str + bar_str + msg + count_str + pct_str + eta_str
206
assert len(m) < self.width
207
self.to_file.write('\r' + m.ljust(self.width - 1))
208
#self.to_file.flush()
215
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
216
#self.to_file.flush()
509
m = spin_str + bar_str + self.last_msg + count_str \
511
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
512
self._have_output = True
513
#self.to_file.flush()
516
if self._have_output:
517
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
518
self._have_output = False
519
#self.to_file.flush()
524
class ChildProgress(_BaseProgressBar):
525
"""A progress indicator that pushes its data to the parent"""
527
def __init__(self, _stack, **kwargs):
528
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
529
self.parent = _stack.top()
532
self.child_fraction = 0
535
def update(self, msg, current_cnt=None, total_cnt=None):
536
self.current = current_cnt
537
if total_cnt is not None:
538
self.total = total_cnt
540
self.child_fraction = 0
543
def child_update(self, message, current, total):
544
if current is None or total == 0:
545
self.child_fraction = 0
547
self.child_fraction = float(current) / total
551
if self.current is None:
554
count = self.current+self.child_fraction
555
if count > self.total:
557
mutter('clamping count of %d to %d' % (count, self.total))
559
self.parent.child_update(self.message, count, self.total)
564
def note(self, *args, **kwargs):
565
self.parent.note(*args, **kwargs)
568
class InstrumentedProgress(TTYProgressBar):
569
"""TTYProgress variant that tracks outcomes"""
571
def __init__(self, *args, **kwargs):
572
self.always_throttled = True
573
self.never_throttle = False
574
TTYProgressBar.__init__(self, *args, **kwargs)
576
def throttle(self, old_message):
577
if self.never_throttle:
580
result = TTYProgressBar.throttle(self, old_message)
582
self.always_throttled = False
220
585
def str_tdelta(delt):