~bzr-pqm/bzr/bzr.dev

649 by Martin Pool
- some cleanups for the progressbar method
1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
1594.1.1 by Robert Collins
Introduce new bzr progress bar api. ui_factory.nested_progress_bar.
2
# Copyright (C) 2005, 2006 Canonical <canonical.com>
648 by Martin Pool
- import aaron's progress-indicator code
3
#
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.
8
#
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.
13
#
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
17
649 by Martin Pool
- some cleanups for the progressbar method
18
889 by Martin Pool
- show progress bar during inventory conversion to weave, and make profiling optional
19
"""Simple text-mode progress indicator.
649 by Martin Pool
- some cleanups for the progressbar method
20
21
To display an indicator, create a ProgressBar object.  Call it,
22
passing Progress objects indicating the current state.  When done,
23
call clear().
24
25
Progress is suppressed when output is not sent to a terminal, so as
26
not to clutter log files.
27
"""
28
652 by Martin Pool
doc
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.
32
962 by Martin Pool
todo
33
# TODO: If not on a tty perhaps just print '......' for the benefit of IDEs, etc
655 by Martin Pool
- better calculation of progress bar position
34
934 by Martin Pool
todo
35
# TODO: Optionally show elapsed time instead/as well as ETA; nicer
36
# when the rate is unpredictable
37
649 by Martin Pool
- some cleanups for the progressbar method
38
648 by Martin Pool
- import aaron's progress-indicator code
39
import sys
660 by Martin Pool
- use plain unix time, not datetime module
40
import time
964 by Martin Pool
- show progress on dumb terminals by printing dots
41
import os
1185.16.75 by Martin Pool
- improved eta estimation for progress bar
42
from collections import deque
648 by Martin Pool
- import aaron's progress-indicator code
43
649 by Martin Pool
- some cleanups for the progressbar method
44
1594.1.1 by Robert Collins
Introduce new bzr progress bar api. ui_factory.nested_progress_bar.
45
import bzrlib.errors as errors
46
47
649 by Martin Pool
- some cleanups for the progressbar method
48
def _supports_progress(f):
695 by Martin Pool
- don't display progress bars on really dumb terminals
49
    if not hasattr(f, 'isatty'):
50
        return False
51
    if not f.isatty():
52
        return False
53
    if os.environ.get('TERM') == 'dumb':
54
        # e.g. emacs compile window
55
        return False
56
    return True
649 by Martin Pool
- some cleanups for the progressbar method
57
58
59
964 by Martin Pool
- show progress on dumb terminals by printing dots
60
def ProgressBar(to_file=sys.stderr, **kwargs):
61
    """Abstract factory"""
62
    if _supports_progress(to_file):
63
        return TTYProgressBar(to_file=to_file, **kwargs)
64
    else:
65
        return DotsProgressBar(to_file=to_file, **kwargs)
66
    
1594.1.1 by Robert Collins
Introduce new bzr progress bar api. ui_factory.nested_progress_bar.
67
 
68
class ProgressBarStack(object):
69
    """A stack of progress bars."""
70
71
    def __init__(self,
72
                 to_file=sys.stderr,
73
                 show_pct=False,
74
                 show_spinner=False,
75
                 show_eta=True,
76
                 show_bar=True,
77
                 show_count=True,
1594.1.3 by Robert Collins
Fixup pb usage to use nested_progress_bar.
78
                 to_messages_file=sys.stdout,
79
                 klass=None):
1594.1.1 by Robert Collins
Introduce new bzr progress bar api. ui_factory.nested_progress_bar.
80
        """Setup the stack with the parameters the progress bars should have."""
81
        self._to_file = to_file
82
        self._show_pct = show_pct
83
        self._show_spinner = show_spinner
84
        self._show_eta = show_eta
85
        self._show_bar = show_bar
86
        self._show_count = show_count
87
        self._to_messages_file = to_messages_file
88
        self._stack = []
1594.1.3 by Robert Collins
Fixup pb usage to use nested_progress_bar.
89
        self._klass = klass or TTYProgressBar
1594.1.1 by Robert Collins
Introduce new bzr progress bar api. ui_factory.nested_progress_bar.
90
91
    def get_nested(self):
92
        """Return a nested progress bar."""
93
        # initial implementation - return a new bar each time.
1594.1.3 by Robert Collins
Fixup pb usage to use nested_progress_bar.
94
        new_bar = self._klass(to_file=self._to_file,
95
                              show_pct=self._show_pct,
96
                              show_spinner=self._show_spinner,
97
                              show_eta=self._show_eta,
98
                              show_bar=self._show_bar,
99
                              show_count=self._show_count,
100
                              to_messages_file=self._to_messages_file,
101
                              _stack=self)
1594.1.1 by Robert Collins
Introduce new bzr progress bar api. ui_factory.nested_progress_bar.
102
        self._stack.append(new_bar)
103
        return new_bar
104
105
    def return_pb(self, bar):
106
        """Return bar after its been used."""
1594.1.4 by Robert Collins
Fix identity test in ProgressBarStack.return_pb
107
        if bar is not self._stack[-1]:
1594.1.1 by Robert Collins
Introduce new bzr progress bar api. ui_factory.nested_progress_bar.
108
            raise errors.MissingProgressBarFinish()
109
        self._stack.pop()
110
111
 
964 by Martin Pool
- show progress on dumb terminals by printing dots
112
class _BaseProgressBar(object):
1594.1.1 by Robert Collins
Introduce new bzr progress bar api. ui_factory.nested_progress_bar.
113
964 by Martin Pool
- show progress on dumb terminals by printing dots
114
    def __init__(self,
115
                 to_file=sys.stderr,
116
                 show_pct=False,
117
                 show_spinner=False,
118
                 show_eta=True,
119
                 show_bar=True,
1534.5.6 by Robert Collins
split out converter logic into per-format objects.
120
                 show_count=True,
1594.1.1 by Robert Collins
Introduce new bzr progress bar api. ui_factory.nested_progress_bar.
121
                 to_messages_file=sys.stdout,
122
                 _stack=None):
964 by Martin Pool
- show progress on dumb terminals by printing dots
123
        object.__init__(self)
124
        self.to_file = to_file
1534.5.6 by Robert Collins
split out converter logic into per-format objects.
125
        self.to_messages_file = to_messages_file
964 by Martin Pool
- show progress on dumb terminals by printing dots
126
        self.last_msg = None
127
        self.last_cnt = None
128
        self.last_total = None
129
        self.show_pct = show_pct
130
        self.show_spinner = show_spinner
131
        self.show_eta = show_eta
132
        self.show_bar = show_bar
133
        self.show_count = show_count
1594.1.1 by Robert Collins
Introduce new bzr progress bar api. ui_factory.nested_progress_bar.
134
        self._stack = _stack
135
136
    def finished(self):
137
        """Return this bar to its progress stack."""
138
        self.clear()
1594.1.3 by Robert Collins
Fixup pb usage to use nested_progress_bar.
139
        assert self._stack is not None
1594.1.1 by Robert Collins
Introduce new bzr progress bar api. ui_factory.nested_progress_bar.
140
        self._stack.return_pb(self)
1104 by Martin Pool
- Add a simple UIFactory
141
1534.5.6 by Robert Collins
split out converter logic into per-format objects.
142
    def note(self, fmt_string, *args, **kwargs):
143
        """Record a note without disrupting the progress bar."""
144
        self.clear()
145
        self.to_messages_file.write(fmt_string % args)
146
        self.to_messages_file.write('\n')
1104 by Martin Pool
- Add a simple UIFactory
147
148
149
class DummyProgress(_BaseProgressBar):
150
    """Progress-bar standin that does nothing.
151
152
    This can be used as the default argument for methods that
153
    take an optional progress indicator."""
154
    def tick(self):
155
        pass
156
157
    def update(self, msg=None, current=None, total=None):
158
        pass
159
160
    def clear(self):
161
        pass
964 by Martin Pool
- show progress on dumb terminals by printing dots
162
        
1534.5.6 by Robert Collins
split out converter logic into per-format objects.
163
    def note(self, fmt_string, *args, **kwargs):
164
        """See _BaseProgressBar.note()."""
1534.5.9 by Robert Collins
Advise users running upgrade on a checkout to also run it on the branch.
165
166
964 by Martin Pool
- show progress on dumb terminals by printing dots
167
class DotsProgressBar(_BaseProgressBar):
1594.1.3 by Robert Collins
Fixup pb usage to use nested_progress_bar.
168
964 by Martin Pool
- show progress on dumb terminals by printing dots
169
    def __init__(self, **kwargs):
170
        _BaseProgressBar.__init__(self, **kwargs)
171
        self.last_msg = None
172
        self.need_nl = False
173
        
174
    def tick(self):
175
        self.update()
176
        
177
    def update(self, msg=None, current_cnt=None, total_cnt=None):
178
        if msg and msg != self.last_msg:
179
            if self.need_nl:
180
                self.to_file.write('\n')
181
            
182
            self.to_file.write(msg + ': ')
183
            self.last_msg = msg
184
        self.need_nl = True
185
        self.to_file.write('.')
186
        
187
    def clear(self):
188
        if self.need_nl:
189
            self.to_file.write('\n')
190
        
191
    
192
class TTYProgressBar(_BaseProgressBar):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
193
    """Progress bar display object.
194
195
    Several options are available to control the display.  These can
196
    be passed as parameters to the constructor or assigned at any time:
197
198
    show_pct
199
        Show percentage complete.
200
    show_spinner
201
        Show rotating baton.  This ticks over on every update even
202
        if the values don't change.
203
    show_eta
204
        Show predicted time-to-completion.
205
    show_bar
206
        Show bar graph.
207
    show_count
208
        Show numerical counts.
209
210
    The output file should be in line-buffered or unbuffered mode.
211
    """
212
    SPIN_CHARS = r'/-\|'
661 by Martin Pool
- limit rate at which progress bar is updated
213
    MIN_PAUSE = 0.1 # seconds
214
964 by Martin Pool
- show progress on dumb terminals by printing dots
215
216
    def __init__(self, **kwargs):
1185.33.60 by Martin Pool
Use full terminal width for verbose test output.
217
        from bzrlib.osutils import terminal_width
964 by Martin Pool
- show progress on dumb terminals by printing dots
218
        _BaseProgressBar.__init__(self, **kwargs)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
219
        self.spin_pos = 0
1185.33.60 by Martin Pool
Use full terminal width for verbose test output.
220
        self.width = terminal_width()
964 by Martin Pool
- show progress on dumb terminals by printing dots
221
        self.start_time = None
222
        self.last_update = None
1185.16.75 by Martin Pool
- improved eta estimation for progress bar
223
        self.last_updates = deque()
964 by Martin Pool
- show progress on dumb terminals by printing dots
224
    
225
226
    def throttle(self):
227
        """Return True if the bar was updated too recently"""
228
        now = time.time()
229
        if self.start_time is None:
230
            self.start_time = self.last_update = now
231
            return False
232
        else:
233
            interval = now - self.last_update
234
            if interval > 0 and interval < self.MIN_PAUSE:
235
                return True
236
1185.16.75 by Martin Pool
- improved eta estimation for progress bar
237
        self.last_updates.append(now - self.last_update)
964 by Martin Pool
- show progress on dumb terminals by printing dots
238
        self.last_update = now
239
        return False
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
240
        
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
241
242
    def tick(self):
243
        self.update(self.last_msg, self.last_cnt, self.last_total)
244
                 
245
246
667 by Martin Pool
- allow for progressbar updates with no count, only a message
247
    def update(self, msg, current_cnt=None, total_cnt=None):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
248
        """Update and redraw progress bar."""
249
1308 by Martin Pool
- make progress bar more tolerant of out-of-range values
250
        if current_cnt < 0:
251
            current_cnt = 0
252
            
253
        if current_cnt > total_cnt:
254
            total_cnt = current_cnt
1570.1.9 by Robert Collins
Do not throttle updates to progress bars that change the message.
255
        
256
        old_msg = self.last_msg
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
257
        # save these for the tick() function
258
        self.last_msg = msg
259
        self.last_cnt = current_cnt
260
        self.last_total = total_cnt
261
            
1570.1.9 by Robert Collins
Do not throttle updates to progress bars that change the message.
262
        if old_msg == self.last_msg and self.throttle():
964 by Martin Pool
- show progress on dumb terminals by printing dots
263
            return 
661 by Martin Pool
- limit rate at which progress bar is updated
264
        
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
265
        if self.show_eta and self.start_time and total_cnt:
1185.16.75 by Martin Pool
- improved eta estimation for progress bar
266
            eta = get_eta(self.start_time, current_cnt, total_cnt,
267
                    last_updates = self.last_updates)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
268
            eta_str = " " + str_tdelta(eta)
269
        else:
270
            eta_str = ""
271
272
        if self.show_spinner:
273
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
274
        else:
275
            spin_str = ''
276
277
        # always update this; it's also used for the bar
278
        self.spin_pos += 1
279
280
        if self.show_pct and total_cnt and current_cnt:
281
            pct = 100.0 * current_cnt / total_cnt
282
            pct_str = ' (%5.1f%%)' % pct
283
        else:
284
            pct_str = ''
285
286
        if not self.show_count:
287
            count_str = ''
288
        elif current_cnt is None:
289
            count_str = ''
290
        elif total_cnt is None:
291
            count_str = ' %i' % (current_cnt)
292
        else:
293
            # make both fields the same size
294
            t = '%i' % (total_cnt)
295
            c = '%*i' % (len(t), current_cnt)
296
            count_str = ' ' + c + '/' + t 
297
298
        if self.show_bar:
299
            # progress bar, if present, soaks up all remaining space
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
300
            cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
301
                   - len(eta_str) - len(count_str) - 3
302
303
            if total_cnt:
304
                # number of markers highlighted in bar
305
                markers = int(round(float(cols) * current_cnt / total_cnt))
306
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
307
            elif False:
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
308
                # don't know total, so can't show completion.
309
                # so just show an expanded spinning thingy
310
                m = self.spin_pos % cols
668 by Martin Pool
- fix sweeping bar progress indicator
311
                ms = (' ' * m + '*').ljust(cols)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
312
                
313
                bar_str = '[' + ms + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
314
            else:
315
                bar_str = ''
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
316
        else:
317
            bar_str = ''
318
319
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
320
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
321
        assert len(m) < self.width
322
        self.to_file.write('\r' + m.ljust(self.width - 1))
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
323
        #self.to_file.flush()
324
            
964 by Martin Pool
- show progress on dumb terminals by printing dots
325
    def clear(self):        
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
326
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
327
        #self.to_file.flush()        
649 by Martin Pool
- some cleanups for the progressbar method
328
648 by Martin Pool
- import aaron's progress-indicator code
329
        
330
def str_tdelta(delt):
331
    if delt is None:
332
        return "-:--:--"
660 by Martin Pool
- use plain unix time, not datetime module
333
    delt = int(round(delt))
334
    return '%d:%02d:%02d' % (delt/3600,
335
                             (delt/60) % 60,
336
                             delt % 60)
337
338
1185.16.75 by Martin Pool
- improved eta estimation for progress bar
339
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
660 by Martin Pool
- use plain unix time, not datetime module
340
    if start_time is None:
341
        return None
342
343
    if not total:
344
        return None
345
346
    if current < enough_samples:
347
        return None
348
349
    if current > total:
350
        return None                     # wtf?
351
352
    elapsed = time.time() - start_time
353
354
    if elapsed < 2.0:                   # not enough time to estimate
355
        return None
356
    
357
    total_duration = float(elapsed) * float(total) / float(current)
358
359
    assert total_duration >= elapsed
360
1185.16.75 by Martin Pool
- improved eta estimation for progress bar
361
    if last_updates and len(last_updates) >= n_recent:
362
        while len(last_updates) > n_recent:
363
            last_updates.popleft()
364
        avg = sum(last_updates) / float(len(last_updates))
365
        time_left = avg * (total - current)
366
367
        old_time_left = total_duration - elapsed
368
369
        # We could return the average, or some other value here
370
        return (time_left + old_time_left) / 2
371
660 by Martin Pool
- use plain unix time, not datetime module
372
    return total_duration - elapsed
648 by Martin Pool
- import aaron's progress-indicator code
373
649 by Martin Pool
- some cleanups for the progressbar method
374
648 by Martin Pool
- import aaron's progress-indicator code
375
def run_tests():
376
    import doctest
377
    result = doctest.testmod()
378
    if result[1] > 0:
379
        if result[0] == 0:
380
            print "All tests passed"
381
    else:
382
        print "No tests to run"
649 by Martin Pool
- some cleanups for the progressbar method
383
384
385
def demo():
964 by Martin Pool
- show progress on dumb terminals by printing dots
386
    sleep = time.sleep
387
    
388
    print 'dumb-terminal test:'
389
    pb = DotsProgressBar()
390
    for i in range(100):
391
        pb.update('Leoparden', i, 99)
392
        sleep(0.1)
393
    sleep(1.5)
394
    pb.clear()
395
    sleep(1.5)
396
    
397
    print 'smart-terminal test:'
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
398
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
649 by Martin Pool
- some cleanups for the progressbar method
399
    for i in range(100):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
400
        pb.update('Elephanten', i, 99)
401
        sleep(0.1)
402
    sleep(2)
403
    pb.clear()
404
    sleep(1)
964 by Martin Pool
- show progress on dumb terminals by printing dots
405
649 by Martin Pool
- some cleanups for the progressbar method
406
    print 'done!'
407
648 by Martin Pool
- import aaron's progress-indicator code
408
if __name__ == "__main__":
649 by Martin Pool
- some cleanups for the progressbar method
409
    demo()