1
1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
2
# Copyright (C) 2005, 2006 Canonical Ltd
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
"""Progress indicators.
21
The usual way to use this is via bzrlib.ui.ui_factory.nested_progress_bar which
22
will maintain a ProgressBarStack for you.
24
For direct use, the factory ProgressBar will return an auto-detected progress
25
bar that should match your terminal type. You can manually create a
26
ProgressBarStack too if you need multiple levels of cooperating progress bars.
27
Note that bzrlib's internal functions use the ui module, so if you are using
28
bzrlib it really is best to use bzrlib.ui.ui_factory.
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.
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
31
35
# TODO: Optionally show elapsed time instead/as well as ETA; nicer
32
36
# when the rate is unpredictable
38
from bzrlib.lazy_import import lazy_import
39
lazy_import(globals(), """
45
from bzrlib.trace import mutter
42
from collections import deque
45
import bzrlib.errors as errors
46
from bzrlib.trace import mutter
48
50
def _supports_progress(f):
49
"""Detect if we can use pretty progress bars on the output stream f.
51
If this returns true we expect that a human may be looking at that
52
output, and that we can repaint a line to update it.
54
isatty = getattr(f, 'isatty', None)
51
if not hasattr(f, 'isatty'):
59
55
if os.environ.get('TERM') == 'dumb':
60
56
# e.g. emacs compile window
65
_progress_bar_types = {}
68
def ProgressBar(to_file=None, **kwargs):
62
def ProgressBar(to_file=sys.stderr, **kwargs):
69
63
"""Abstract factory"""
72
requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
73
# An value of '' or not set reverts to standard processing
74
if requested_bar_type in (None, ''):
75
if _supports_progress(to_file):
76
return TTYProgressBar(to_file=to_file, **kwargs)
78
return DummyProgress(to_file=to_file, **kwargs)
64
if _supports_progress(to_file):
65
return TTYProgressBar(to_file=to_file, **kwargs)
80
# Minor sanitation to prevent spurious errors
81
requested_bar_type = requested_bar_type.lower().strip()
82
# TODO: jam 20060710 Arguably we shouldn't raise an exception
83
# but should instead just disable progress bars if we
84
# don't recognize the type
85
if requested_bar_type not in _progress_bar_types:
86
raise errors.InvalidProgressBarType(requested_bar_type,
87
_progress_bar_types.keys())
88
return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
67
return DotsProgressBar(to_file=to_file, **kwargs)
91
70
class ProgressBarStack(object):
92
71
"""A stack of progress bars."""
101
to_messages_file=None,
80
to_messages_file=sys.stdout,
103
82
"""Setup the stack with the parameters the progress bars should have."""
106
if to_messages_file is None:
107
to_messages_file = sys.stdout
108
83
self._to_file = to_file
109
84
self._show_pct = show_pct
110
85
self._show_spinner = show_spinner
288
251
_BaseProgressBar.__init__(self, **kwargs)
289
252
self.spin_pos = 0
290
253
self.width = terminal_width()
291
self.last_updates = []
292
self._max_last_updates = 10
254
self.start_time = None
255
self.last_updates = deque()
293
256
self.child_fraction = 0
294
self._have_output = False
296
def throttle(self, old_msg):
297
260
"""Return True if the bar was updated too recently"""
298
261
# time.time consistently takes 40/4000 ms = 0.01 ms.
299
# time.clock() is faster, but gives us CPU time, not wall-clock time
301
if self.start_time is not None and (now - self.start_time) < 1:
303
if old_msg != self.last_msg:
262
# but every single update to the pb invokes it.
263
# so we use time.clock which takes 20/4000 ms = 0.005ms
264
# on the downside, time.clock() appears to have approximately
265
# 10ms granularity, so we treat a zero-time change as 'throttled.'
305
268
interval = now - self.last_update
306
269
# if interval > 0
307
270
if interval < self.MIN_PAUSE:
310
273
self.last_updates.append(now - self.last_update)
311
# Don't let the queue grow without bound
312
self.last_updates = self.last_updates[-self._max_last_updates:]
313
274
self.last_update = now
317
self.update(self.last_msg, self.last_cnt, self.last_total,
279
self.update(self.last_msg, self.last_cnt, self.last_total,
318
280
self.child_fraction)
320
282
def child_update(self, message, current, total):
325
287
elif self.last_cnt + child_fraction <= self.last_total:
326
288
self.child_fraction = child_fraction
290
mutter('not updating child fraction')
327
291
if self.last_msg is None:
328
292
self.last_msg = ''
331
def update(self, msg, current_cnt=None, total_cnt=None,
296
def update(self, msg, current_cnt=None, total_cnt=None,
332
297
child_fraction=0):
333
298
"""Update and redraw progress bar."""
337
if total_cnt is None:
338
total_cnt = self.last_total
340
300
if current_cnt < 0:
428
388
m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
429
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
430
self._have_output = True
390
assert len(m) < self.width
391
self.to_file.write('\r' + m.ljust(self.width - 1))
431
392
#self.to_file.flush()
434
if self._have_output:
435
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
436
self._have_output = False
395
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
437
396
#self.to_file.flush()
440
_progress_bar_types['tty'] = TTYProgressBar
443
399
class ChildProgress(_BaseProgressBar):
444
400
"""A progress indicator that pushes its data to the parent"""
446
401
def __init__(self, _stack, **kwargs):
447
402
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
448
403
self.parent = _stack.top()
483
def note(self, *args, **kwargs):
484
self.parent.note(*args, **kwargs)
487
class InstrumentedProgress(TTYProgressBar):
488
"""TTYProgress variant that tracks outcomes"""
490
def __init__(self, *args, **kwargs):
491
self.always_throttled = True
492
self.never_throttle = False
493
TTYProgressBar.__init__(self, *args, **kwargs)
495
def throttle(self, old_message):
496
if self.never_throttle:
499
result = TTYProgressBar.throttle(self, old_message)
501
self.always_throttled = False
504
438
def str_tdelta(delt):
556
494
self.cur_phase = 0
558
496
self.cur_phase += 1
497
assert self.cur_phase < self.total
559
498
self.pb.update(self.message, self.cur_phase, self.total)
503
result = doctest.testmod()
506
print "All tests passed"
508
print "No tests to run"
514
print 'dumb-terminal test:'
515
pb = DotsProgressBar()
517
pb.update('Leoparden', i, 99)
523
print 'smart-terminal test:'
524
pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
526
pb.update('Elephanten', i, 99)
534
if __name__ == "__main__":