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'):
61
def ProgressBar(to_file=None, **kwargs):
57
def ProgressBar(to_file=sys.stderr, **kwargs):
62
58
"""Abstract factory"""
65
59
if _supports_progress(to_file):
66
60
return TTYProgressBar(to_file=to_file, **kwargs)
68
62
return DotsProgressBar(to_file=to_file, **kwargs)
71
class ProgressBarStack(object):
72
"""A stack of progress bars."""
81
to_messages_file=None,
83
"""Setup the stack with the parameters the progress bars should have."""
86
if to_messages_file is None:
87
to_messages_file = sys.stdout
88
self._to_file = to_file
89
self._show_pct = show_pct
90
self._show_spinner = show_spinner
91
self._show_eta = show_eta
92
self._show_bar = show_bar
93
self._show_count = show_count
94
self._to_messages_file = to_messages_file
96
self._klass = klass or TTYProgressBar
99
if len(self._stack) != 0:
100
return self._stack[-1]
105
if len(self._stack) != 0:
106
return self._stack[0]
110
def get_nested(self):
111
"""Return a nested progress bar."""
112
if len(self._stack) == 0:
115
func = self.top().child_progress
116
new_bar = func(to_file=self._to_file,
117
show_pct=self._show_pct,
118
show_spinner=self._show_spinner,
119
show_eta=self._show_eta,
120
show_bar=self._show_bar,
121
show_count=self._show_count,
122
to_messages_file=self._to_messages_file,
124
self._stack.append(new_bar)
127
def return_pb(self, bar):
128
"""Return bar after its been used."""
129
if bar is not self._stack[-1]:
130
raise errors.MissingProgressBarFinish()
134
65
class _BaseProgressBar(object):
136
66
def __init__(self,
139
69
show_spinner=False,
143
to_messages_file=None,
73
to_messages_file=sys.stdout):
145
74
object.__init__(self)
148
if to_messages_file is None:
149
to_messages_file = sys.stdout
150
75
self.to_file = to_file
151
76
self.to_messages_file = to_messages_file
152
77
self.last_msg = None
157
82
self.show_eta = show_eta
158
83
self.show_bar = show_bar
159
84
self.show_count = show_count
162
self.MIN_PAUSE = 0.1 # seconds
165
self.start_time = now
166
# next update should not throttle
167
self.last_update = now - self.MIN_PAUSE - 1
170
"""Return this bar to its progress stack."""
172
assert self._stack is not None
173
self._stack.return_pb(self)
175
86
def note(self, fmt_string, *args, **kwargs):
176
87
"""Record a note without disrupting the progress bar."""
178
89
self.to_messages_file.write(fmt_string % args)
179
90
self.to_messages_file.write('\n')
181
def child_progress(self, **kwargs):
182
return ChildProgress(**kwargs)
185
93
class DummyProgress(_BaseProgressBar):
186
94
"""Progress-bar standin that does nothing.
193
101
def update(self, msg=None, current=None, total=None):
196
def child_update(self, message, current, total):
202
107
def note(self, fmt_string, *args, **kwargs):
203
108
"""See _BaseProgressBar.note()."""
205
def child_progress(self, **kwargs):
206
return DummyProgress(**kwargs)
209
111
class DotsProgressBar(_BaseProgressBar):
211
112
def __init__(self, **kwargs):
212
113
_BaseProgressBar.__init__(self, **kwargs)
213
114
self.last_msg = None
263
162
self.spin_pos = 0
264
163
self.width = terminal_width()
265
164
self.start_time = None
165
self.last_update = None
266
166
self.last_updates = deque()
267
self.child_fraction = 0
270
169
def throttle(self):
271
170
"""Return True if the bar was updated too recently"""
272
# time.time consistently takes 40/4000 ms = 0.01 ms.
273
# but every single update to the pb invokes it.
274
# so we use time.clock which takes 20/4000 ms = 0.005ms
275
# on the downside, time.clock() appears to have approximately
276
# 10ms granularity, so we treat a zero-time change as 'throttled.'
279
interval = now - self.last_update
281
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:
284
180
self.last_updates.append(now - self.last_update)
285
181
self.last_update = now
289
self.update(self.last_msg, self.last_cnt, self.last_total,
292
def child_update(self, message, current, total):
293
if current is not None and total != 0:
294
child_fraction = float(current) / total
295
if self.last_cnt is None:
297
elif self.last_cnt + child_fraction <= self.last_total:
298
self.child_fraction = child_fraction
300
mutter('not updating child fraction')
301
if self.last_msg is None:
305
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):
307
191
"""Update and redraw progress bar."""
311
if total_cnt is None:
312
total_cnt = self.last_total
314
193
if current_cnt < 0:
317
196
if current_cnt > total_cnt:
318
197
total_cnt = current_cnt
320
## # optional corner case optimisation
321
## # currently does not seem to fire so costs more than saved.
322
## # trivial optimal case:
323
## # NB if callers are doing a clear and restore with
324
## # the saved values, this will prevent that:
325
## # in that case add a restore method that calls
326
## # _do_update or some such
327
## if (self.last_msg == msg and
328
## self.last_cnt == current_cnt and
329
## self.last_total == total_cnt and
330
## self.child_fraction == child_fraction):
333
199
old_msg = self.last_msg
334
200
# save these for the tick() function
335
201
self.last_msg = msg
336
202
self.last_cnt = current_cnt
337
203
self.last_total = total_cnt
338
self.child_fraction = child_fraction
340
# each function call takes 20ms/4000 = 0.005 ms,
341
# but multiple that by 4000 calls -> starts to cost.
342
# so anything to make this function call faster
343
# will improve base 'diff' time by up to 0.1 seconds.
344
205
if old_msg == self.last_msg and self.throttle():
347
if self.show_eta and self.start_time and self.last_total:
348
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
349
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)
350
211
eta_str = " " + str_tdelta(eta)
359
220
# always update this; it's also used for the bar
360
221
self.spin_pos += 1
362
if self.show_pct and self.last_total and self.last_cnt:
363
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
364
225
pct_str = ' (%5.1f%%)' % pct
368
229
if not self.show_count:
370
elif self.last_cnt is None:
231
elif current_cnt is None:
372
elif self.last_total is None:
373
count_str = ' %i' % (self.last_cnt)
233
elif total_cnt is None:
234
count_str = ' %i' % (current_cnt)
375
236
# make both fields the same size
376
t = '%i' % (self.last_total)
377
c = '%*i' % (len(t), self.last_cnt)
237
t = '%i' % (total_cnt)
238
c = '%*i' % (len(t), current_cnt)
378
239
count_str = ' ' + c + '/' + t
380
241
if self.show_bar:
381
242
# progress bar, if present, soaks up all remaining space
382
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) \
383
244
- len(eta_str) - len(count_str) - 3
386
247
# number of markers highlighted in bar
387
markers = int(round(float(cols) *
388
(self.last_cnt + self.child_fraction) / self.last_total))
248
markers = int(round(float(cols) * current_cnt / total_cnt))
389
249
bar_str = '[' + ('=' * markers).ljust(cols) + '] '
391
251
# don't know total, so can't show completion.
409
269
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
410
270
#self.to_file.flush()
413
class ChildProgress(_BaseProgressBar):
414
"""A progress indicator that pushes its data to the parent"""
416
def __init__(self, _stack, **kwargs):
417
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
418
self.parent = _stack.top()
421
self.child_fraction = 0
424
def update(self, msg, current_cnt=None, total_cnt=None):
425
self.current = current_cnt
426
self.total = total_cnt
428
self.child_fraction = 0
431
def child_update(self, message, current, total):
432
if current is None or total == 0:
433
self.child_fraction = 0
435
self.child_fraction = float(current) / total
439
if self.current is None:
442
count = self.current+self.child_fraction
443
if count > self.total:
445
mutter('clamping count of %d to %d' % (count, self.total))
447
self.parent.child_update(self.message, count, self.total)
452
def note(self, *args, **kwargs):
453
self.parent.note(*args, **kwargs)
456
273
def str_tdelta(delt):
498
315
return total_duration - elapsed
501
class ProgressPhase(object):
502
"""Update progress object with the current phase"""
503
def __init__(self, message, total, pb):
504
object.__init__(self)
506
self.message = message
508
self.cur_phase = None
510
def next_phase(self):
511
if self.cur_phase is None:
515
assert self.cur_phase < self.total
516
self.pb.update(self.message, self.cur_phase, self.total)
521
320
result = doctest.testmod()