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
37
from bzrlib.trace import mutter
38
from bzrlib.symbol_versioning import (
49
44
def _supports_progress(f):
50
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)
54
55
if os.environ.get('TERM') == 'dumb':
55
56
# e.g. emacs compile window
61
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
"""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):
62
144
"""Abstract factory"""
63
if _supports_progress(to_file):
64
return TTYProgressBar(to_file=to_file, **kwargs)
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)
66
return DotsProgressBar(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)
69
166
class ProgressBarStack(object):
70
"""A stack of progress bars."""
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)))
72
174
def __init__(self,
75
177
show_spinner=True,
79
to_messages_file=sys.stdout,
181
to_messages_file=None,
81
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
82
188
self._to_file = to_file
83
189
self._show_pct = show_pct
84
190
self._show_spinner = show_spinner
192
303
def note(self, fmt_string, *args, **kwargs):
193
304
"""See _BaseProgressBar.note()."""
195
306
def child_progress(self, **kwargs):
196
307
return DummyProgress(**kwargs)
198
310
class DotsProgressBar(_BaseProgressBar):
200
312
def __init__(self, **kwargs):
201
313
_BaseProgressBar.__init__(self, **kwargs)
202
314
self.last_msg = None
203
315
self.need_nl = False
208
320
def update(self, msg=None, current_cnt=None, total_cnt=None):
209
321
if msg and msg != self.last_msg:
211
323
self.to_file.write('\n')
213
324
self.to_file.write(msg + ': ')
214
325
self.last_msg = msg
215
326
self.need_nl = True
216
327
self.to_file.write('.')
220
331
self.to_file.write('\n')
222
334
def child_update(self, message, current, total):
225
340
class TTYProgressBar(_BaseProgressBar):
226
341
"""Progress bar display object.
250
365
_BaseProgressBar.__init__(self, **kwargs)
251
366
self.spin_pos = 0
252
367
self.width = terminal_width()
253
self.start_time = None
254
self.last_updates = deque()
368
self.last_updates = []
369
self._max_last_updates = 10
255
370
self.child_fraction = 0
371
self._have_output = False
373
def throttle(self, old_msg):
259
374
"""Return True if the bar was updated too recently"""
260
375
# 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.'
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:
267
382
interval = now - self.last_update
268
383
# if interval > 0
269
384
if interval < self.MIN_PAUSE:
272
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:]
273
390
self.last_update = now
278
self.update(self.last_msg, self.last_cnt, self.last_total,
394
self.update(self.last_msg, self.last_cnt, self.last_total,
279
395
self.child_fraction)
281
397
def child_update(self, message, current, total):
286
402
elif self.last_cnt + child_fraction <= self.last_total:
287
403
self.child_fraction = child_fraction
289
mutter('not updating child fraction')
290
404
if self.last_msg is None:
291
405
self.last_msg = ''
408
def update(self, msg, current_cnt=None, total_cnt=None,
410
"""Update and redraw progress bar.
295
def update(self, msg, current_cnt=None, total_cnt=None,
297
"""Update and redraw progress bar."""
415
if total_cnt is None:
416
total_cnt = self.last_total
299
418
if current_cnt < 0:
302
421
if current_cnt > total_cnt:
303
422
total_cnt = current_cnt
305
## # optional corner case optimisation
424
## # optional corner case optimisation
306
425
## # currently does not seem to fire so costs more than saved.
307
426
## # trivial optimal case:
308
427
## # NB if callers are doing a clear and restore with
322
444
self.last_total = total_cnt
323
445
self.child_fraction = child_fraction
325
# each function call takes 20ms/4000 = 0.005 ms,
447
# each function call takes 20ms/4000 = 0.005 ms,
326
448
# but multiple that by 4000 calls -> starts to cost.
327
449
# so anything to make this function call faster
328
450
# will improve base 'diff' time by up to 0.1 seconds.
329
if old_msg == self.last_msg and self.throttle():
451
if self.throttle(old_msg):
332
454
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,
455
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
334
456
self.last_total, last_updates = self.last_updates)
335
457
eta_str = " " + str_tdelta(eta)
339
461
if self.show_spinner:
340
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
462
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
377
499
# so just show an expanded spinning thingy
378
500
m = self.spin_pos % cols
379
501
ms = (' ' * m + '*').ljust(cols)
381
503
bar_str = '[' + ms + '] '
387
m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
389
assert len(m) < self.width
390
self.to_file.write('\r' + m.ljust(self.width - 1))
391
#self.to_file.flush()
394
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
395
#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()
398
524
class ChildProgress(_BaseProgressBar):
399
525
"""A progress indicator that pushes its data to the parent"""
400
527
def __init__(self, _stack, **kwargs):
401
528
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
402
529
self.parent = _stack.top()
436
564
def note(self, *args, **kwargs):
437
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
440
585
def str_tdelta(delt):
496
637
self.cur_phase = 0
498
639
self.cur_phase += 1
499
assert self.cur_phase < self.total
500
640
self.pb.update(self.message, self.cur_phase, self.total)
505
result = doctest.testmod()
508
print "All tests passed"
510
print "No tests to run"
516
print 'dumb-terminal test:'
517
pb = DotsProgressBar()
519
pb.update('Leoparden', i, 99)
525
print 'smart-terminal test:'
526
pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
528
pb.update('Elephanten', i, 99)
536
if __name__ == "__main__":
643
_progress_bar_types = {}
644
_progress_bar_types['dummy'] = DummyProgress
645
_progress_bar_types['none'] = DummyProgress
646
_progress_bar_types['tty'] = TTYProgressBar
647
_progress_bar_types['dots'] = DotsProgressBar