/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/progress.py

Got propogation under test

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
 
2
# Copyright (C) 2005, 2006 Canonical <canonical.com>
 
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
 
 
18
 
 
19
"""Simple text-mode progress indicator.
 
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
 
 
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
 
 
33
# TODO: If not on a tty perhaps just print '......' for the benefit of IDEs, etc
 
34
 
 
35
# TODO: Optionally show elapsed time instead/as well as ETA; nicer
 
36
# when the rate is unpredictable
 
37
 
 
38
 
 
39
import sys
 
40
import time
 
41
import os
 
42
from collections import deque
 
43
 
 
44
 
 
45
import bzrlib.errors as errors
 
46
from bzrlib.trace import mutter 
 
47
 
 
48
 
 
49
def _supports_progress(f):
 
50
    if not hasattr(f, 'isatty'):
 
51
        return False
 
52
    if not f.isatty():
 
53
        return False
 
54
    if os.environ.get('TERM') == 'dumb':
 
55
        # e.g. emacs compile window
 
56
        return False
 
57
    return True
 
58
 
 
59
 
 
60
 
 
61
def ProgressBar(to_file=sys.stderr, **kwargs):
 
62
    """Abstract factory"""
 
63
    if _supports_progress(to_file):
 
64
        return TTYProgressBar(to_file=to_file, **kwargs)
 
65
    else:
 
66
        return DotsProgressBar(to_file=to_file, **kwargs)
 
67
    
 
68
 
 
69
class ProgressBarStack(object):
 
70
    """A stack of progress bars."""
 
71
 
 
72
    def __init__(self,
 
73
                 to_file=sys.stderr,
 
74
                 show_pct=False,
 
75
                 show_spinner=False,
 
76
                 show_eta=True,
 
77
                 show_bar=True,
 
78
                 show_count=True,
 
79
                 to_messages_file=sys.stdout,
 
80
                 klass=None):
 
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
 
89
        self._stack = []
 
90
        self._klass = klass or TTYProgressBar
 
91
 
 
92
    def get_nested(self):
 
93
        """Return a nested progress bar."""
 
94
        # initial implementation - return a new bar each time.
 
95
        new_bar = self._klass(to_file=self._to_file,
 
96
                              show_pct=self._show_pct,
 
97
                              show_spinner=self._show_spinner,
 
98
                              show_eta=self._show_eta,
 
99
                              show_bar=self._show_bar,
 
100
                              show_count=self._show_count,
 
101
                              to_messages_file=self._to_messages_file,
 
102
                              _stack=self)
 
103
        self._stack.append(new_bar)
 
104
        return new_bar
 
105
 
 
106
    def return_pb(self, bar):
 
107
        """Return bar after its been used."""
 
108
        if bar is not self._stack[-1]:
 
109
            raise errors.MissingProgressBarFinish()
 
110
        self._stack.pop()
 
111
 
 
112
 
 
113
class _BaseProgressBar(object):
 
114
 
 
115
    def __init__(self,
 
116
                 to_file=sys.stderr,
 
117
                 show_pct=False,
 
118
                 show_spinner=False,
 
119
                 show_eta=True,
 
120
                 show_bar=True,
 
121
                 show_count=True,
 
122
                 to_messages_file=sys.stdout,
 
123
                 _stack=None):
 
124
        object.__init__(self)
 
125
        self.to_file = to_file
 
126
        self.to_messages_file = to_messages_file
 
127
        self.last_msg = None
 
128
        self.last_cnt = None
 
129
        self.last_total = None
 
130
        self.show_pct = show_pct
 
131
        self.show_spinner = show_spinner
 
132
        self.show_eta = show_eta
 
133
        self.show_bar = show_bar
 
134
        self.show_count = show_count
 
135
        self._stack = _stack
 
136
 
 
137
    def finished(self):
 
138
        """Return this bar to its progress stack."""
 
139
        self.clear()
 
140
        assert self._stack is not None
 
141
        self._stack.return_pb(self)
 
142
 
 
143
    def note(self, fmt_string, *args, **kwargs):
 
144
        """Record a note without disrupting the progress bar."""
 
145
        self.clear()
 
146
        self.to_messages_file.write(fmt_string % args)
 
147
        self.to_messages_file.write('\n')
 
148
 
 
149
 
 
150
class DummyProgress(_BaseProgressBar):
 
151
    """Progress-bar standin that does nothing.
 
152
 
 
153
    This can be used as the default argument for methods that
 
154
    take an optional progress indicator."""
 
155
    def tick(self):
 
156
        pass
 
157
 
 
158
    def update(self, msg=None, current=None, total=None):
 
159
        pass
 
160
 
 
161
    def child_update(self, message, current, total):
 
162
        pass
 
163
 
 
164
    def clear(self):
 
165
        pass
 
166
        
 
167
    def note(self, fmt_string, *args, **kwargs):
 
168
        """See _BaseProgressBar.note()."""
 
169
 
 
170
 
 
171
class DotsProgressBar(_BaseProgressBar):
 
172
 
 
173
    def __init__(self, **kwargs):
 
174
        _BaseProgressBar.__init__(self, **kwargs)
 
175
        self.last_msg = None
 
176
        self.need_nl = False
 
177
        
 
178
    def tick(self):
 
179
        self.update()
 
180
        
 
181
    def update(self, msg=None, current_cnt=None, total_cnt=None):
 
182
        if msg and msg != self.last_msg:
 
183
            if self.need_nl:
 
184
                self.to_file.write('\n')
 
185
            
 
186
            self.to_file.write(msg + ': ')
 
187
            self.last_msg = msg
 
188
        self.need_nl = True
 
189
        self.to_file.write('.')
 
190
        
 
191
    def clear(self):
 
192
        if self.need_nl:
 
193
            self.to_file.write('\n')
 
194
        
 
195
    
 
196
class TTYProgressBar(_BaseProgressBar):
 
197
    """Progress bar display object.
 
198
 
 
199
    Several options are available to control the display.  These can
 
200
    be passed as parameters to the constructor or assigned at any time:
 
201
 
 
202
    show_pct
 
203
        Show percentage complete.
 
204
    show_spinner
 
205
        Show rotating baton.  This ticks over on every update even
 
206
        if the values don't change.
 
207
    show_eta
 
208
        Show predicted time-to-completion.
 
209
    show_bar
 
210
        Show bar graph.
 
211
    show_count
 
212
        Show numerical counts.
 
213
 
 
214
    The output file should be in line-buffered or unbuffered mode.
 
215
    """
 
216
    SPIN_CHARS = r'/-\|'
 
217
    MIN_PAUSE = 0.1 # seconds
 
218
 
 
219
 
 
220
    def __init__(self, **kwargs):
 
221
        from bzrlib.osutils import terminal_width
 
222
        _BaseProgressBar.__init__(self, **kwargs)
 
223
        self.spin_pos = 0
 
224
        self.width = terminal_width()
 
225
        self.start_time = None
 
226
        self.last_update = None
 
227
        self.last_updates = deque()
 
228
    
 
229
 
 
230
    def throttle(self):
 
231
        """Return True if the bar was updated too recently"""
 
232
        now = time.time()
 
233
        if self.start_time is None:
 
234
            self.start_time = self.last_update = now
 
235
            return False
 
236
        else:
 
237
            interval = now - self.last_update
 
238
            if interval > 0 and interval < self.MIN_PAUSE:
 
239
                return True
 
240
 
 
241
        self.last_updates.append(now - self.last_update)
 
242
        self.last_update = now
 
243
        return False
 
244
        
 
245
 
 
246
    def tick(self):
 
247
        self.update(self.last_msg, self.last_cnt, self.last_total, 
 
248
                    self.child_fraction)
 
249
 
 
250
    def update(self, msg, current_cnt=None, total_cnt=None, 
 
251
               child_fraction=0):
 
252
        """Update and redraw progress bar."""
 
253
        self.child_fraction = child_fraction
 
254
 
 
255
        if current_cnt < 0:
 
256
            current_cnt = 0
 
257
            
 
258
        if current_cnt > total_cnt:
 
259
            total_cnt = current_cnt
 
260
        
 
261
        old_msg = self.last_msg
 
262
        # save these for the tick() function
 
263
        self.last_msg = msg
 
264
        self.last_cnt = current_cnt
 
265
        self.last_total = total_cnt
 
266
            
 
267
        if old_msg == self.last_msg and self.throttle():
 
268
            return 
 
269
        
 
270
        if self.show_eta and self.start_time and total_cnt:
 
271
            eta = get_eta(self.start_time, current_cnt, total_cnt,
 
272
                    last_updates = self.last_updates)
 
273
            eta_str = " " + str_tdelta(eta)
 
274
        else:
 
275
            eta_str = ""
 
276
 
 
277
        if self.show_spinner:
 
278
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
 
279
        else:
 
280
            spin_str = ''
 
281
 
 
282
        # always update this; it's also used for the bar
 
283
        self.spin_pos += 1
 
284
 
 
285
        if self.show_pct and total_cnt and current_cnt:
 
286
            pct = 100.0 * (current_cnt / total_cnt + child_fraction)
 
287
            pct_str = ' (%5.1f%%)' % pct
 
288
        else:
 
289
            pct_str = ''
 
290
 
 
291
        if not self.show_count:
 
292
            count_str = ''
 
293
        elif current_cnt is None:
 
294
            count_str = ''
 
295
        elif total_cnt is None:
 
296
            count_str = ' %i' % (current_cnt)
 
297
        else:
 
298
            # make both fields the same size
 
299
            t = '%i' % (total_cnt)
 
300
            c = '%*i' % (len(t), current_cnt)
 
301
            count_str = ' ' + c + '/' + t 
 
302
 
 
303
        if self.show_bar:
 
304
            # progress bar, if present, soaks up all remaining space
 
305
            cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
 
306
                   - len(eta_str) - len(count_str) - 3
 
307
 
 
308
            if total_cnt:
 
309
                # number of markers highlighted in bar
 
310
                markers = int(round(float(cols) * current_cnt / total_cnt))
 
311
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
 
312
            elif False:
 
313
                # don't know total, so can't show completion.
 
314
                # so just show an expanded spinning thingy
 
315
                m = self.spin_pos % cols
 
316
                ms = (' ' * m + '*').ljust(cols)
 
317
                
 
318
                bar_str = '[' + ms + '] '
 
319
            else:
 
320
                bar_str = ''
 
321
        else:
 
322
            bar_str = ''
 
323
 
 
324
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
 
325
 
 
326
        assert len(m) < self.width
 
327
        self.to_file.write('\r' + m.ljust(self.width - 1))
 
328
        #self.to_file.flush()
 
329
            
 
330
    def clear(self):        
 
331
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
332
        #self.to_file.flush()        
 
333
 
 
334
 
 
335
class ChildProgress(object):
 
336
    """A progress indicator that pushes its data to the parent"""
 
337
    def __init__(self, stack, *kwargs):
 
338
        self.parent = stack[-1]
 
339
        self.current = None
 
340
        self.total = None
 
341
        self.child_fraction = 0
 
342
        self.message = None
 
343
 
 
344
    def update(self, msg, current_cnt=None, total_cnt=None):
 
345
        self.current = current_cnt
 
346
        self.total = total_cnt
 
347
        self.message = msg
 
348
        self.child_fraction = 0
 
349
        self.tick()
 
350
 
 
351
    def child_update(self, message, current, total):
 
352
        self.child_fraction = float(current) / total
 
353
        self.tick()
 
354
 
 
355
    def tick(self):
 
356
        count = self.current+self.child_fraction
 
357
        if count > self.total:
 
358
            mutter('clamping count of %d to %d' % (count, self.total))
 
359
            count = self.total
 
360
        self.parent.child_update(self.message, count, self.total)
 
361
 
 
362
 
 
363
def str_tdelta(delt):
 
364
    if delt is None:
 
365
        return "-:--:--"
 
366
    delt = int(round(delt))
 
367
    return '%d:%02d:%02d' % (delt/3600,
 
368
                             (delt/60) % 60,
 
369
                             delt % 60)
 
370
 
 
371
 
 
372
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
 
373
    if start_time is None:
 
374
        return None
 
375
 
 
376
    if not total:
 
377
        return None
 
378
 
 
379
    if current < enough_samples:
 
380
        return None
 
381
 
 
382
    if current > total:
 
383
        return None                     # wtf?
 
384
 
 
385
    elapsed = time.time() - start_time
 
386
 
 
387
    if elapsed < 2.0:                   # not enough time to estimate
 
388
        return None
 
389
    
 
390
    total_duration = float(elapsed) * float(total) / float(current)
 
391
 
 
392
    assert total_duration >= elapsed
 
393
 
 
394
    if last_updates and len(last_updates) >= n_recent:
 
395
        while len(last_updates) > n_recent:
 
396
            last_updates.popleft()
 
397
        avg = sum(last_updates) / float(len(last_updates))
 
398
        time_left = avg * (total - current)
 
399
 
 
400
        old_time_left = total_duration - elapsed
 
401
 
 
402
        # We could return the average, or some other value here
 
403
        return (time_left + old_time_left) / 2
 
404
 
 
405
    return total_duration - elapsed
 
406
 
 
407
 
 
408
def run_tests():
 
409
    import doctest
 
410
    result = doctest.testmod()
 
411
    if result[1] > 0:
 
412
        if result[0] == 0:
 
413
            print "All tests passed"
 
414
    else:
 
415
        print "No tests to run"
 
416
 
 
417
 
 
418
def demo():
 
419
    sleep = time.sleep
 
420
    
 
421
    print 'dumb-terminal test:'
 
422
    pb = DotsProgressBar()
 
423
    for i in range(100):
 
424
        pb.update('Leoparden', i, 99)
 
425
        sleep(0.1)
 
426
    sleep(1.5)
 
427
    pb.clear()
 
428
    sleep(1.5)
 
429
    
 
430
    print 'smart-terminal test:'
 
431
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
 
432
    for i in range(100):
 
433
        pb.update('Elephanten', i, 99)
 
434
        sleep(0.1)
 
435
    sleep(2)
 
436
    pb.clear()
 
437
    sleep(1)
 
438
 
 
439
    print 'done!'
 
440
 
 
441
if __name__ == "__main__":
 
442
    demo()