/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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-03-08 00:37:41 UTC
  • mfrom: (1594.2.4 integration)
  • Revision ID: pqm@pqm.ubuntu.com-20060308003741-08afccbf89005e87
Merge in :
 * Knit repositories use knits
 * Nested progress bar support.
 * Ghost aware graph api.

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
 
 
47
 
 
48
def _supports_progress(f):
 
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
 
57
 
 
58
 
 
59
 
 
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
    
 
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,
 
78
                 to_messages_file=sys.stdout,
 
79
                 klass=None):
 
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 = []
 
89
        self._klass = klass or TTYProgressBar
 
90
 
 
91
    def get_nested(self):
 
92
        """Return a nested progress bar."""
 
93
        # initial implementation - return a new bar each time.
 
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)
 
102
        self._stack.append(new_bar)
 
103
        return new_bar
 
104
 
 
105
    def return_pb(self, bar):
 
106
        """Return bar after its been used."""
 
107
        if bar is not self._stack[-1]:
 
108
            raise errors.MissingProgressBarFinish()
 
109
        self._stack.pop()
 
110
 
 
111
 
 
112
class _BaseProgressBar(object):
 
113
 
 
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,
 
120
                 show_count=True,
 
121
                 to_messages_file=sys.stdout,
 
122
                 _stack=None):
 
123
        object.__init__(self)
 
124
        self.to_file = to_file
 
125
        self.to_messages_file = to_messages_file
 
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
 
134
        self._stack = _stack
 
135
 
 
136
    def finished(self):
 
137
        """Return this bar to its progress stack."""
 
138
        self.clear()
 
139
        assert self._stack is not None
 
140
        self._stack.return_pb(self)
 
141
 
 
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')
 
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
 
162
        
 
163
    def note(self, fmt_string, *args, **kwargs):
 
164
        """See _BaseProgressBar.note()."""
 
165
 
 
166
 
 
167
class DotsProgressBar(_BaseProgressBar):
 
168
 
 
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):
 
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'/-\|'
 
213
    MIN_PAUSE = 0.1 # seconds
 
214
 
 
215
 
 
216
    def __init__(self, **kwargs):
 
217
        from bzrlib.osutils import terminal_width
 
218
        _BaseProgressBar.__init__(self, **kwargs)
 
219
        self.spin_pos = 0
 
220
        self.width = terminal_width()
 
221
        self.start_time = None
 
222
        self.last_update = None
 
223
        self.last_updates = deque()
 
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
 
 
237
        self.last_updates.append(now - self.last_update)
 
238
        self.last_update = now
 
239
        return False
 
240
        
 
241
 
 
242
    def tick(self):
 
243
        self.update(self.last_msg, self.last_cnt, self.last_total)
 
244
                 
 
245
 
 
246
 
 
247
    def update(self, msg, current_cnt=None, total_cnt=None):
 
248
        """Update and redraw progress bar."""
 
249
 
 
250
        if current_cnt < 0:
 
251
            current_cnt = 0
 
252
            
 
253
        if current_cnt > total_cnt:
 
254
            total_cnt = current_cnt
 
255
        
 
256
        old_msg = self.last_msg
 
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
            
 
262
        if old_msg == self.last_msg and self.throttle():
 
263
            return 
 
264
        
 
265
        if self.show_eta and self.start_time and total_cnt:
 
266
            eta = get_eta(self.start_time, current_cnt, total_cnt,
 
267
                    last_updates = self.last_updates)
 
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
 
300
            cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
 
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) + '] '
 
307
            elif False:
 
308
                # don't know total, so can't show completion.
 
309
                # so just show an expanded spinning thingy
 
310
                m = self.spin_pos % cols
 
311
                ms = (' ' * m + '*').ljust(cols)
 
312
                
 
313
                bar_str = '[' + ms + '] '
 
314
            else:
 
315
                bar_str = ''
 
316
        else:
 
317
            bar_str = ''
 
318
 
 
319
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
 
320
 
 
321
        assert len(m) < self.width
 
322
        self.to_file.write('\r' + m.ljust(self.width - 1))
 
323
        #self.to_file.flush()
 
324
            
 
325
    def clear(self):        
 
326
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
327
        #self.to_file.flush()        
 
328
 
 
329
        
 
330
def str_tdelta(delt):
 
331
    if delt is None:
 
332
        return "-:--:--"
 
333
    delt = int(round(delt))
 
334
    return '%d:%02d:%02d' % (delt/3600,
 
335
                             (delt/60) % 60,
 
336
                             delt % 60)
 
337
 
 
338
 
 
339
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
 
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
 
 
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
 
 
372
    return total_duration - elapsed
 
373
 
 
374
 
 
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"
 
383
 
 
384
 
 
385
def demo():
 
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:'
 
398
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
 
399
    for i in range(100):
 
400
        pb.update('Elephanten', i, 99)
 
401
        sleep(0.1)
 
402
    sleep(2)
 
403
    pb.clear()
 
404
    sleep(1)
 
405
 
 
406
    print 'done!'
 
407
 
 
408
if __name__ == "__main__":
 
409
    demo()