/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: Michael Ellerman
  • Date: 2006-03-09 00:24:48 UTC
  • mto: (1610.1.8 bzr.mbp.integration)
  • mto: This revision was merged to the branch mainline in revision 1616.
  • Revision ID: michael@ellerman.id.au-20060309002448-70cce15e3d605130
Make the "ignore line" in the commit message editor the "right" width, so
that if you make your message that wide it won't wrap in bzr log output.
Just as a visual aid.

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 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
def _supports_progress(f):
 
46
    if not hasattr(f, 'isatty'):
 
47
        return False
 
48
    if not f.isatty():
 
49
        return False
 
50
    if os.environ.get('TERM') == 'dumb':
 
51
        # e.g. emacs compile window
 
52
        return False
 
53
    return True
 
54
 
 
55
 
 
56
 
 
57
def ProgressBar(to_file=sys.stderr, **kwargs):
 
58
    """Abstract factory"""
 
59
    if _supports_progress(to_file):
 
60
        return TTYProgressBar(to_file=to_file, **kwargs)
 
61
    else:
 
62
        return DotsProgressBar(to_file=to_file, **kwargs)
 
63
    
 
64
    
 
65
class _BaseProgressBar(object):
 
66
    def __init__(self,
 
67
                 to_file=sys.stderr,
 
68
                 show_pct=False,
 
69
                 show_spinner=False,
 
70
                 show_eta=True,
 
71
                 show_bar=True,
 
72
                 show_count=True,
 
73
                 to_messages_file=sys.stdout):
 
74
        object.__init__(self)
 
75
        self.to_file = to_file
 
76
        self.to_messages_file = to_messages_file
 
77
        self.last_msg = None
 
78
        self.last_cnt = None
 
79
        self.last_total = None
 
80
        self.show_pct = show_pct
 
81
        self.show_spinner = show_spinner
 
82
        self.show_eta = show_eta
 
83
        self.show_bar = show_bar
 
84
        self.show_count = show_count
 
85
 
 
86
    def note(self, fmt_string, *args, **kwargs):
 
87
        """Record a note without disrupting the progress bar."""
 
88
        self.clear()
 
89
        self.to_messages_file.write(fmt_string % args)
 
90
        self.to_messages_file.write('\n')
 
91
 
 
92
 
 
93
class DummyProgress(_BaseProgressBar):
 
94
    """Progress-bar standin that does nothing.
 
95
 
 
96
    This can be used as the default argument for methods that
 
97
    take an optional progress indicator."""
 
98
    def tick(self):
 
99
        pass
 
100
 
 
101
    def update(self, msg=None, current=None, total=None):
 
102
        pass
 
103
 
 
104
    def clear(self):
 
105
        pass
 
106
        
 
107
    def note(self, fmt_string, *args, **kwargs):
 
108
        """See _BaseProgressBar.note()."""
 
109
 
 
110
 
 
111
class DotsProgressBar(_BaseProgressBar):
 
112
    def __init__(self, **kwargs):
 
113
        _BaseProgressBar.__init__(self, **kwargs)
 
114
        self.last_msg = None
 
115
        self.need_nl = False
 
116
        
 
117
    def tick(self):
 
118
        self.update()
 
119
        
 
120
    def update(self, msg=None, current_cnt=None, total_cnt=None):
 
121
        if msg and msg != self.last_msg:
 
122
            if self.need_nl:
 
123
                self.to_file.write('\n')
 
124
            
 
125
            self.to_file.write(msg + ': ')
 
126
            self.last_msg = msg
 
127
        self.need_nl = True
 
128
        self.to_file.write('.')
 
129
        
 
130
    def clear(self):
 
131
        if self.need_nl:
 
132
            self.to_file.write('\n')
 
133
        
 
134
    
 
135
class TTYProgressBar(_BaseProgressBar):
 
136
    """Progress bar display object.
 
137
 
 
138
    Several options are available to control the display.  These can
 
139
    be passed as parameters to the constructor or assigned at any time:
 
140
 
 
141
    show_pct
 
142
        Show percentage complete.
 
143
    show_spinner
 
144
        Show rotating baton.  This ticks over on every update even
 
145
        if the values don't change.
 
146
    show_eta
 
147
        Show predicted time-to-completion.
 
148
    show_bar
 
149
        Show bar graph.
 
150
    show_count
 
151
        Show numerical counts.
 
152
 
 
153
    The output file should be in line-buffered or unbuffered mode.
 
154
    """
 
155
    SPIN_CHARS = r'/-\|'
 
156
    MIN_PAUSE = 0.1 # seconds
 
157
 
 
158
 
 
159
    def __init__(self, **kwargs):
 
160
        from bzrlib.osutils import terminal_width
 
161
        _BaseProgressBar.__init__(self, **kwargs)
 
162
        self.spin_pos = 0
 
163
        self.width = terminal_width()
 
164
        self.start_time = None
 
165
        self.last_update = None
 
166
        self.last_updates = deque()
 
167
    
 
168
 
 
169
    def throttle(self):
 
170
        """Return True if the bar was updated too recently"""
 
171
        now = time.time()
 
172
        if self.start_time is None:
 
173
            self.start_time = self.last_update = now
 
174
            return False
 
175
        else:
 
176
            interval = now - self.last_update
 
177
            if interval > 0 and interval < self.MIN_PAUSE:
 
178
                return True
 
179
 
 
180
        self.last_updates.append(now - self.last_update)
 
181
        self.last_update = now
 
182
        return False
 
183
        
 
184
 
 
185
    def tick(self):
 
186
        self.update(self.last_msg, self.last_cnt, self.last_total)
 
187
                 
 
188
 
 
189
 
 
190
    def update(self, msg, current_cnt=None, total_cnt=None):
 
191
        """Update and redraw progress bar."""
 
192
 
 
193
        if current_cnt < 0:
 
194
            current_cnt = 0
 
195
            
 
196
        if current_cnt > total_cnt:
 
197
            total_cnt = current_cnt
 
198
        
 
199
        old_msg = self.last_msg
 
200
        # save these for the tick() function
 
201
        self.last_msg = msg
 
202
        self.last_cnt = current_cnt
 
203
        self.last_total = total_cnt
 
204
            
 
205
        if old_msg == self.last_msg and self.throttle():
 
206
            return 
 
207
        
 
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)
 
211
            eta_str = " " + str_tdelta(eta)
 
212
        else:
 
213
            eta_str = ""
 
214
 
 
215
        if self.show_spinner:
 
216
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
 
217
        else:
 
218
            spin_str = ''
 
219
 
 
220
        # always update this; it's also used for the bar
 
221
        self.spin_pos += 1
 
222
 
 
223
        if self.show_pct and total_cnt and current_cnt:
 
224
            pct = 100.0 * current_cnt / total_cnt
 
225
            pct_str = ' (%5.1f%%)' % pct
 
226
        else:
 
227
            pct_str = ''
 
228
 
 
229
        if not self.show_count:
 
230
            count_str = ''
 
231
        elif current_cnt is None:
 
232
            count_str = ''
 
233
        elif total_cnt is None:
 
234
            count_str = ' %i' % (current_cnt)
 
235
        else:
 
236
            # make both fields the same size
 
237
            t = '%i' % (total_cnt)
 
238
            c = '%*i' % (len(t), current_cnt)
 
239
            count_str = ' ' + c + '/' + t 
 
240
 
 
241
        if self.show_bar:
 
242
            # progress bar, if present, soaks up all remaining space
 
243
            cols = self.width - 1 - len(msg) - len(spin_str) - len(pct_str) \
 
244
                   - len(eta_str) - len(count_str) - 3
 
245
 
 
246
            if total_cnt:
 
247
                # number of markers highlighted in bar
 
248
                markers = int(round(float(cols) * current_cnt / total_cnt))
 
249
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
 
250
            elif False:
 
251
                # don't know total, so can't show completion.
 
252
                # so just show an expanded spinning thingy
 
253
                m = self.spin_pos % cols
 
254
                ms = (' ' * m + '*').ljust(cols)
 
255
                
 
256
                bar_str = '[' + ms + '] '
 
257
            else:
 
258
                bar_str = ''
 
259
        else:
 
260
            bar_str = ''
 
261
 
 
262
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
 
263
 
 
264
        assert len(m) < self.width
 
265
        self.to_file.write('\r' + m.ljust(self.width - 1))
 
266
        #self.to_file.flush()
 
267
            
 
268
    def clear(self):        
 
269
        self.to_file.write('\r%s\r' % (' ' * (self.width - 1)))
 
270
        #self.to_file.flush()        
 
271
 
 
272
        
 
273
def str_tdelta(delt):
 
274
    if delt is None:
 
275
        return "-:--:--"
 
276
    delt = int(round(delt))
 
277
    return '%d:%02d:%02d' % (delt/3600,
 
278
                             (delt/60) % 60,
 
279
                             delt % 60)
 
280
 
 
281
 
 
282
def get_eta(start_time, current, total, enough_samples=3, last_updates=None, n_recent=10):
 
283
    if start_time is None:
 
284
        return None
 
285
 
 
286
    if not total:
 
287
        return None
 
288
 
 
289
    if current < enough_samples:
 
290
        return None
 
291
 
 
292
    if current > total:
 
293
        return None                     # wtf?
 
294
 
 
295
    elapsed = time.time() - start_time
 
296
 
 
297
    if elapsed < 2.0:                   # not enough time to estimate
 
298
        return None
 
299
    
 
300
    total_duration = float(elapsed) * float(total) / float(current)
 
301
 
 
302
    assert total_duration >= elapsed
 
303
 
 
304
    if last_updates and len(last_updates) >= n_recent:
 
305
        while len(last_updates) > n_recent:
 
306
            last_updates.popleft()
 
307
        avg = sum(last_updates) / float(len(last_updates))
 
308
        time_left = avg * (total - current)
 
309
 
 
310
        old_time_left = total_duration - elapsed
 
311
 
 
312
        # We could return the average, or some other value here
 
313
        return (time_left + old_time_left) / 2
 
314
 
 
315
    return total_duration - elapsed
 
316
 
 
317
 
 
318
def run_tests():
 
319
    import doctest
 
320
    result = doctest.testmod()
 
321
    if result[1] > 0:
 
322
        if result[0] == 0:
 
323
            print "All tests passed"
 
324
    else:
 
325
        print "No tests to run"
 
326
 
 
327
 
 
328
def demo():
 
329
    sleep = time.sleep
 
330
    
 
331
    print 'dumb-terminal test:'
 
332
    pb = DotsProgressBar()
 
333
    for i in range(100):
 
334
        pb.update('Leoparden', i, 99)
 
335
        sleep(0.1)
 
336
    sleep(1.5)
 
337
    pb.clear()
 
338
    sleep(1.5)
 
339
    
 
340
    print 'smart-terminal test:'
 
341
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
 
342
    for i in range(100):
 
343
        pb.update('Elephanten', i, 99)
 
344
        sleep(0.1)
 
345
    sleep(2)
 
346
    pb.clear()
 
347
    sleep(1)
 
348
 
 
349
    print 'done!'
 
350
 
 
351
if __name__ == "__main__":
 
352
    demo()