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.
33
from bzrlib.trace import mutter
34
from bzrlib.symbol_versioning import (
40
def _supports_progress(f):
41
"""Detect if we can use pretty progress bars on file F.
43
If this returns true we expect that a human may be looking at that
44
output, and that we can repaint a line to update it.
46
This doesn't check the policy for whether we *should* use them.
48
isatty = getattr(f, 'isatty', None)
53
# The following case also handles Win32 - on that platform $TERM is
54
# typically never set, so the case None is treated as a smart terminal,
55
# not dumb. <https://bugs.launchpad.net/bugs/334808> win32 files do have
56
# isatty methods that return true.
57
if os.environ.get('TERM') == 'dumb':
58
# e.g. emacs compile window
63
class ProgressTask(object):
64
"""Model component of a progress indicator.
66
Most code that needs to indicate progress should update one of these,
67
and it will in turn update the display, if one is present.
69
Code updating the task may also set fields as hints about how to display
70
it: show_pct, show_spinner, show_eta, show_count, show_bar. UIs
71
will not necessarily respect all these fields.
73
:ivar update_latency: The interval (in seconds) at which the PB should be
74
updated. Setting this to zero suggests every update should be shown
77
:ivar show_transport_activity: If true (default), transport activity
78
will be shown when this task is drawn. Disable it if you're sure
79
that only irrelevant or uninteresting transport activity can occur
83
def __init__(self, parent_task=None, ui_factory=None, progress_view=None):
84
"""Construct a new progress task.
86
:param parent_task: Enclosing ProgressTask or None.
88
:param progress_view: ProgressView to display this ProgressTask.
90
:param ui_factory: The UI factory that will display updates;
91
deprecated in favor of passing progress_view directly.
93
Normally you should not call this directly but rather through
94
`ui_factory.nested_progress_bar`.
96
self._parent_task = parent_task
99
self.current_cnt = None
101
# TODO: deprecate passing ui_factory
102
self.ui_factory = ui_factory
103
self.progress_view = progress_view
104
self.show_pct = False
105
self.show_spinner = True
106
self.show_eta = False,
107
self.show_count = True
109
self.update_latency = 0.1
110
self.show_transport_activity = True
113
return '%s(%r/%r, msg=%r)' % (
114
self.__class__.__name__,
119
def update(self, msg, current_cnt=None, total_cnt=None):
121
self.current_cnt = current_cnt
123
self.total_cnt = total_cnt
124
if self.progress_view:
125
self.progress_view.show_progress(self)
127
self.ui_factory._progress_updated(self)
130
self.update(self.msg)
133
if self.progress_view:
134
self.progress_view.task_finished(self)
136
self.ui_factory._progress_finished(self)
138
def make_sub_task(self):
139
return ProgressTask(self, ui_factory=self.ui_factory,
140
progress_view=self.progress_view)
142
def _overall_completion_fraction(self, child_fraction=0.0):
143
"""Return fractional completion of this task and its parents
145
Returns None if no completion can be computed."""
146
if self.current_cnt is not None and self.total_cnt:
147
own_fraction = (float(self.current_cnt) + child_fraction) / self.total_cnt
149
# if this task has no estimation, it just passes on directly
150
# whatever the child has measured...
151
own_fraction = child_fraction
152
if self._parent_task is None:
155
if own_fraction is None:
157
return self._parent_task._overall_completion_fraction(own_fraction)
159
@deprecated_method(deprecated_in((2, 1, 0)))
160
def note(self, fmt_string, *args):
161
"""Record a note without disrupting the progress bar.
163
Deprecated: use ui_factory.note() instead or bzrlib.trace. Note that
164
ui_factory.note takes just one string as the argument, not a format
165
string and arguments.
168
self.ui_factory.note(fmt_string % args)
170
self.ui_factory.note(fmt_string)
173
# TODO: deprecate this method; the model object shouldn't be concerned
174
# with whether it's shown or not. Most callers use this because they
175
# want to write some different non-progress output to the screen, but
176
# they should probably instead use a stream that's synchronized with
177
# the progress output. It may be there is a model-level use for
178
# saying "this task's not active at the moment" but I don't see it. --
180
if self.progress_view:
181
self.progress_view.clear()
183
self.ui_factory.clear_term()
186
@deprecated_function(deprecated_in((1, 16, 0)))
187
def ProgressBar(to_file=None, **kwargs):
188
"""Construct a progress bar.
190
Deprecated; ask the ui_factory for a progress task instead.
194
requested_bar_type = os.environ.get('BZR_PROGRESS_BAR')
195
# An value of '' or not set reverts to standard processing
196
if requested_bar_type in (None, ''):
197
if _supports_progress(to_file):
198
return TTYProgressBar(to_file=to_file, **kwargs)
200
return DummyProgress(to_file=to_file, **kwargs)
202
# Minor sanitation to prevent spurious errors
203
requested_bar_type = requested_bar_type.lower().strip()
204
# TODO: jam 20060710 Arguably we shouldn't raise an exception
205
# but should instead just disable progress bars if we
206
# don't recognize the type
207
if requested_bar_type not in _progress_bar_types:
208
raise errors.InvalidProgressBarType(requested_bar_type,
209
_progress_bar_types.keys())
210
return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
213
# NOTE: This is also deprecated; you should provide a ProgressView instead.
214
class _BaseProgressBar(object):
223
to_messages_file=None,
225
object.__init__(self)
228
if to_messages_file is None:
229
to_messages_file = sys.stdout
230
self.to_file = to_file
231
self.to_messages_file = to_messages_file
234
self.last_total = None
235
self.show_pct = show_pct
236
self.show_spinner = show_spinner
237
self.show_eta = show_eta
238
self.show_bar = show_bar
239
self.show_count = show_count
242
self.MIN_PAUSE = 0.1 # seconds
245
self.start_time = now
246
# next update should not throttle
247
self.last_update = now - self.MIN_PAUSE - 1
250
"""Return this bar to its progress stack."""
252
self._stack.return_pb(self)
254
def note(self, fmt_string, *args, **kwargs):
255
"""Record a note without disrupting the progress bar."""
257
self.to_messages_file.write(fmt_string % args)
258
self.to_messages_file.write('\n')
260
@deprecated_function(deprecated_in((1, 16, 0)))
261
def child_progress(self, **kwargs):
262
return ChildProgress(**kwargs)
265
class DummyProgress(_BaseProgressBar):
266
"""Progress-bar standin that does nothing.
268
This can be used as the default argument for methods that
269
take an optional progress indicator."""
274
def update(self, msg=None, current=None, total=None):
277
def child_update(self, message, current, total):
283
def note(self, fmt_string, *args, **kwargs):
284
"""See _BaseProgressBar.note()."""
286
def child_progress(self, **kwargs):
287
return DummyProgress(**kwargs)
290
class DotsProgressBar(_BaseProgressBar):
292
@deprecated_function(deprecated_in((1, 16, 0)))
293
def __init__(self, **kwargs):
294
_BaseProgressBar.__init__(self, **kwargs)
301
def update(self, msg=None, current_cnt=None, total_cnt=None):
302
if msg and msg != self.last_msg:
304
self.to_file.write('\n')
305
self.to_file.write(msg + ': ')
308
self.to_file.write('.')
312
self.to_file.write('\n')
315
def child_update(self, message, current, total):
319
class TTYProgressBar(_BaseProgressBar):
320
"""Progress bar display object.
322
Several options are available to control the display. These can
323
be passed as parameters to the constructor or assigned at any time:
326
Show percentage complete.
328
Show rotating baton. This ticks over on every update even
329
if the values don't change.
331
Show predicted time-to-completion.
335
Show numerical counts.
337
The output file should be in line-buffered or unbuffered mode.
341
@deprecated_function(deprecated_in((1, 16, 0)))
342
def __init__(self, **kwargs):
343
from bzrlib.osutils import terminal_width
344
_BaseProgressBar.__init__(self, **kwargs)
346
self.width = terminal_width()
347
self.last_updates = []
348
self._max_last_updates = 10
349
self.child_fraction = 0
350
self._have_output = False
352
def throttle(self, old_msg):
353
"""Return True if the bar was updated too recently"""
354
# time.time consistently takes 40/4000 ms = 0.01 ms.
355
# time.clock() is faster, but gives us CPU time, not wall-clock time
357
if self.start_time is not None and (now - self.start_time) < 1:
359
if old_msg != self.last_msg:
361
interval = now - self.last_update
363
if interval < self.MIN_PAUSE:
366
self.last_updates.append(now - self.last_update)
367
# Don't let the queue grow without bound
368
self.last_updates = self.last_updates[-self._max_last_updates:]
369
self.last_update = now
373
self.update(self.last_msg, self.last_cnt, self.last_total,
376
def child_update(self, message, current, total):
377
if current is not None and total != 0:
378
child_fraction = float(current) / total
379
if self.last_cnt is None:
381
elif self.last_cnt + child_fraction <= self.last_total:
382
self.child_fraction = child_fraction
383
if self.last_msg is None:
387
def update(self, msg, current_cnt=None, total_cnt=None,
389
"""Update and redraw progress bar.
394
if total_cnt is None:
395
total_cnt = self.last_total
400
if current_cnt > total_cnt:
401
total_cnt = current_cnt
403
## # optional corner case optimisation
404
## # currently does not seem to fire so costs more than saved.
405
## # trivial optimal case:
406
## # NB if callers are doing a clear and restore with
407
## # the saved values, this will prevent that:
408
## # in that case add a restore method that calls
409
## # _do_update or some such
410
## if (self.last_msg == msg and
411
## self.last_cnt == current_cnt and
412
## self.last_total == total_cnt and
413
## self.child_fraction == child_fraction):
419
old_msg = self.last_msg
420
# save these for the tick() function
422
self.last_cnt = current_cnt
423
self.last_total = total_cnt
424
self.child_fraction = child_fraction
426
# each function call takes 20ms/4000 = 0.005 ms,
427
# but multiple that by 4000 calls -> starts to cost.
428
# so anything to make this function call faster
429
# will improve base 'diff' time by up to 0.1 seconds.
430
if self.throttle(old_msg):
433
if self.show_eta and self.start_time and self.last_total:
434
eta = get_eta(self.start_time, self.last_cnt + self.child_fraction,
435
self.last_total, last_updates = self.last_updates)
436
eta_str = " " + str_tdelta(eta)
440
if self.show_spinner:
441
spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '
445
# always update this; it's also used for the bar
448
if self.show_pct and self.last_total and self.last_cnt:
449
pct = 100.0 * ((self.last_cnt + self.child_fraction) / self.last_total)
450
pct_str = ' (%5.1f%%)' % pct
454
if not self.show_count:
456
elif self.last_cnt is None:
458
elif self.last_total is None:
459
count_str = ' %i' % (self.last_cnt)
461
# make both fields the same size
462
t = '%i' % (self.last_total)
463
c = '%*i' % (len(t), self.last_cnt)
464
count_str = ' ' + c + '/' + t
467
# progress bar, if present, soaks up all remaining space
468
cols = self.width - 1 - len(self.last_msg) - len(spin_str) - len(pct_str) \
469
- len(eta_str) - len(count_str) - 3
472
# number of markers highlighted in bar
473
markers = int(round(float(cols) *
474
(self.last_cnt + self.child_fraction) / self.last_total))
475
bar_str = '[' + ('=' * markers).ljust(cols) + '] '
477
# don't know total, so can't show completion.
478
# so just show an expanded spinning thingy
479
m = self.spin_pos % cols
480
ms = (' ' * m + '*').ljust(cols)
482
bar_str = '[' + ms + '] '
488
m = spin_str + bar_str + self.last_msg + count_str \
490
self.to_file.write('\r%-*.*s' % (self.width - 1, self.width - 1, m))
491
self._have_output = True
492
#self.to_file.flush()
495
if self._have_output:
496
self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
497
self._have_output = False
498
#self.to_file.flush()
503
class ChildProgress(_BaseProgressBar):
504
"""A progress indicator that pushes its data to the parent"""
506
@deprecated_function(deprecated_in((1, 16, 0)))
507
def __init__(self, _stack, **kwargs):
508
_BaseProgressBar.__init__(self, _stack=_stack, **kwargs)
509
self.parent = _stack.top()
512
self.child_fraction = 0
515
def update(self, msg, current_cnt=None, total_cnt=None):
516
self.current = current_cnt
517
if total_cnt is not None:
518
self.total = total_cnt
520
self.child_fraction = 0
523
def child_update(self, message, current, total):
524
if current is None or total == 0:
525
self.child_fraction = 0
527
self.child_fraction = float(current) / total
531
if self.current is None:
534
count = self.current+self.child_fraction
535
if count > self.total:
537
mutter('clamping count of %d to %d' % (count, self.total))
539
self.parent.child_update(self.message, count, self.total)
544
def note(self, *args, **kwargs):
545
self.parent.note(*args, **kwargs)
548
def str_tdelta(delt):
551
delt = int(round(delt))
552
return '%d:%02d:%02d' % (delt/3600,
557
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
558
if start_time is None:
564
if current < enough_samples:
570
elapsed = time.time() - start_time
572
if elapsed < 2.0: # not enough time to estimate
575
total_duration = float(elapsed) * float(total) / float(current)
577
if last_updates and len(last_updates) >= n_recent:
578
avg = sum(last_updates) / float(len(last_updates))
579
time_left = avg * (total - current)
581
old_time_left = total_duration - elapsed
583
# We could return the average, or some other value here
584
return (time_left + old_time_left) / 2
586
return total_duration - elapsed
589
class ProgressPhase(object):
590
"""Update progress object with the current phase"""
591
def __init__(self, message, total, pb):
592
object.__init__(self)
594
self.message = message
596
self.cur_phase = None
598
def next_phase(self):
599
if self.cur_phase is None:
603
self.pb.update(self.message, self.cur_phase, self.total)
606
_progress_bar_types = {}
607
_progress_bar_types['dummy'] = DummyProgress
608
_progress_bar_types['none'] = DummyProgress
609
_progress_bar_types['tty'] = TTYProgressBar
610
_progress_bar_types['dots'] = DotsProgressBar