1
1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
2
# Copyright (C) 2005, 2006 Canonical <canonical.com>
2
# Copyright (C) 2005 Canonical <canonical.com>
4
4
# This program is free software; you can redistribute it and/or modify
5
5
# it under the terms of the GNU General Public License as published by
42
42
from collections import deque
45
import bzrlib.errors as errors
46
from bzrlib.trace import mutter
49
45
def _supports_progress(f):
50
46
if not hasattr(f, 'isatty'):
66
62
return DotsProgressBar(to_file=to_file, **kwargs)
69
class ProgressBarStack(object):
70
"""A stack of progress bars."""
79
to_messages_file=sys.stdout,
81
"""Setup the stack with the parameters the progress bars should have."""
82
self._to_file = to_file
83
self._show_pct = show_pct
84
self._show_spinner = show_spinner
85
self._show_eta = show_eta
86
self._show_bar = show_bar
87
self._show_count = show_count
88
self._to_messages_file = to_messages_file
90
self._klass = klass or TTYProgressBar
93
if len(self._stack) != 0:
94
return self._stack[-1]
99
if len(self._stack) != 0:
100
return self._stack[0]
104
def get_nested(self):
105
"""Return a nested progress bar."""
106
if len(self._stack) == 0:
109
func = self.top().child_progress
110
new_bar = func(to_file=self._to_file,
111
show_pct=self._show_pct,
112
show_spinner=self._show_spinner,
113
show_eta=self._show_eta,
114
show_bar=self._show_bar,
115
show_count=self._show_count,
116
to_messages_file=self._to_messages_file,
118
self._stack.append(new_bar)
121
def return_pb(self, bar):
122
"""Return bar after its been used."""
123
if bar is not self._stack[-1]:
124
raise errors.MissingProgressBarFinish()
128
65
class _BaseProgressBar(object):
130
66
def __init__(self,
131
67
to_file=sys.stderr,
147
82
self.show_eta = show_eta
148
83
self.show_bar = show_bar
149
84
self.show_count = show_count
152
self.MIN_PAUSE = 0.1 # seconds
155
self.start_time = now
156
# next update should not throttle
157
self.last_update = now - self.MIN_PAUSE - 1
160
"""Return this bar to its progress stack."""
162
assert self._stack is not None
163
self._stack.return_pb(self)
165
86
def note(self, fmt_string, *args, **kwargs):
166
87
"""Record a note without disrupting the progress bar."""
168
89
self.to_messages_file.write(fmt_string % args)
169
90
self.to_messages_file.write('\n')
171
def child_progress(self, **kwargs):
172
return ChildProgress(**kwargs)
175
93
class DummyProgress(_BaseProgressBar):
176
94
"""Progress-bar standin that does nothing.
183
101
def update(self, msg=None, current=None, total=None):
186
def child_update(self, message, current, total):
192
107
def note(self, fmt_string, *args, **kwargs):
193
108
"""See _BaseProgressBar.note()."""
195
def child_progress(self, **kwargs):
196
return DummyProgress(**kwargs)
198
111
class DotsProgressBar(_BaseProgressBar):
200
112
def __init__(self, **kwargs):
201
113
_BaseProgressBar.__init__(self, **kwargs)
202
114
self.last_msg = None
220
132
self.to_file.write('\n')
222
def child_update(self, message, current, total):
225
135
class TTYProgressBar(_BaseProgressBar):
226
136
"""Progress bar display object.
251
162
self.spin_pos = 0
252
163
self.width = terminal_width()
253
164
self.start_time = None
165
self.last_update = None
254
166
self.last_updates = deque()
255
self.child_fraction = 0
258
169
def throttle(self):
259
170
"""Return True if the bar was updated too recently"""
260
# 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.'
267
interval = now - self.last_update
269
if interval < self.MIN_PAUSE:
172
if self.start_time is None:
173
self.start_time = self.last_update = now
176
interval = now - self.last_update
177
if interval > 0 and interval < self.MIN_PAUSE:
272
180
self.last_updates.append(now - self.last_update)
273
181
self.last_update = now
278
self.update(self.last_msg, self.last_cnt, self.last_total,
281
def child_update(self, message, current, total):
282
if current is not None and total != 0:
283
child_fraction = float(current) / total
284
if self.last_cnt is None:
286
elif self.last_cnt + child_fraction <= self.last_total:
287
self.child_fraction = child_fraction
289
mutter('not updating child fraction')
290
if self.last_msg is None:
295
def update(self, msg, current_cnt=None, total_cnt=None,
186
self.update(self.last_msg, self.last_cnt, self.last_total)
190
def update(self, msg, current_cnt=None, total_cnt=None):
297
191
"""Update and redraw progress bar."""
299
193
if current_cnt < 0:
302
196
if current_cnt > total_cnt:
303
197
total_cnt = current_cnt
305
## # optional corner case optimisation
306
## # currently does not seem to fire so costs more than saved.
307
## # trivial optimal case:
308
## # NB if callers are doing a clear and restore with
309
## # the saved values, this will prevent that:
310
## # in that case add a restore method that calls
311
## # _do_update or some such
312
## if (self.last_msg == msg and
313
## self.last_cnt == current_cnt and
314
## self.last_total == total_cnt and
315
## self.child_fraction == child_fraction):
318
199
old_msg = self.last_msg
319
200
# save these for the tick() function
320
201
self.last_msg = msg
321
202
self.last_cnt = current_cnt
322
203
self.last_total = total_cnt
323
self.child_fraction = child_fraction
325
# each function call takes 20ms/4000 = 0.005 ms,
326
# but multiple that by 4000 calls -> starts to cost.
327
# so anything to make this function call faster
328
# will improve base 'diff' time by up to 0.1 seconds.
329
205
if old_msg == self.last_msg and self.throttle():
332
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,
334
self.last_total, last_updates = self.last_updates)
208
if self.show_eta and self.start_time and total_cnt:
209
eta = get_eta(self.start_time, current_cnt, total_cnt,
210
last_updates = self.last_updates)
335
211
eta_str = " " + str_tdelta(eta)
344
220
# always update this; it's also used for the bar
345
221
self.spin_pos += 1
347
if self.show_pct and self.last_total and self.last_cnt:
348
pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
223
if self.show_pct and total_cnt and current_cnt:
224
pct = 100.0 * current_cnt / total_cnt
349
225
pct_str = ' (%5.1f%%)' % pct
353
229
if not self.show_count:
355
elif self.last_cnt is None:
231
elif current_cnt is None:
357
elif self.last_total is None:
358
count_str = ' %i' % (self.last_cnt)
233
elif total_cnt is None:
234
count_str = ' %i' % (current_cnt)
360
236
# make both fields the same size
361
t = '%i' % (self.last_total)
362
c = '%*i' % (len(t), self.last_cnt)
237
t = '%i' % (total_cnt)
238
c = '%*i' % (len(t), current_cnt)
363
239
count_str = ' ' + c + '/' + t
365
241
if self.show_bar:
366
242
# progress bar, if present, soaks up all remaining space
367
cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
243
cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
368
244
- len(eta_str) - len(count_str) - 3
371
247
# number of markers highlighted in bar
372
markers = int(round(float(cols) *
373
(self.last_cnt + self.child_fraction) / self.last_total))
248
markers = int(round(float(cols) * current_cnt / total_cnt))
374
249
bar_str = '[' + ('=' * markers).ljust(cols) + '] '
376
251
# don't know total, so can't show completion.
387
m = spin_str + bar_str + self.last_msg + count_str + pct_str + eta_str
262
m = spin_str + bar_str + msg + count_str + pct_str + eta_str
389
264
assert len(m) < self.width
390
265
self.to_file.write('\r' + m.ljust(self.width - 1))
394
269
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
395
270
#self.to_file.flush()
398
class ChildProgress(_BaseProgressBar):
399
"""A progress indicator that pushes its data to the parent"""
400
def __init__(self, _stack, **kwargs):
401
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
402
self.parent = _stack.top()
405
self.child_fraction = 0
408
def update(self, msg, current_cnt=None, total_cnt=None):
409
self.current = current_cnt
410
self.total = total_cnt
412
self.child_fraction = 0
415
def child_update(self, message, current, total):
416
if current is None or total == 0:
417
self.child_fraction = 0
419
self.child_fraction = float(current) / total
423
if self.current is None:
426
count = self.current+self.child_fraction
427
if count > self.total:
429
mutter('clamping count of %d to %d' % (count, self.total))
431
self.parent.child_update(self.message, count, self.total)
436
def note(self, *args, **kwargs):
437
self.parent.note(*args, **kwargs)
440
273
def str_tdelta(delt):
482
315
return total_duration - elapsed
485
class ProgressPhase(object):
486
"""Update progress object with the current phase"""
487
def __init__(self, message, total, pb):
488
object.__init__(self)
490
self.message = message
492
self.cur_phase = None
494
def next_phase(self):
495
if self.cur_phase is None:
499
assert self.cur_phase < self.total
500
self.pb.update(self.message, self.cur_phase, self.total)
505
320
result = doctest.testmod()