~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>
2
# Copyright (C) 2005 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
45
def _supports_progress(f):
695 by Martin Pool
- don't display progress bars on really dumb terminals
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
649 by Martin Pool
- some cleanups for the progressbar method
54
55
56
964 by Martin Pool
- show progress on dumb terminals by printing dots
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,
1534.5.6 by Robert Collins
split out converter logic into per-format objects.
72
                 show_count=True,
73
                 to_messages_file=sys.stdout):
964 by Martin Pool
- show progress on dumb terminals by printing dots
74
        object.__init__(self)
75
        self.to_file = to_file
1534.5.6 by Robert Collins
split out converter logic into per-format objects.
76
        self.to_messages_file = to_messages_file
964 by Martin Pool
- show progress on dumb terminals by printing dots
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
1104 by Martin Pool
- Add a simple UIFactory
85
1534.5.6 by Robert Collins
split out converter logic into per-format objects.
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')
1104 by Martin Pool
- Add a simple UIFactory
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
964 by Martin Pool
- show progress on dumb terminals by printing dots
106
        
1534.5.6 by Robert Collins
split out converter logic into per-format objects.
107
    def note(self, fmt_string, *args, **kwargs):
108
        """See _BaseProgressBar.note()."""
1534.5.9 by Robert Collins
Advise users running upgrade on a checkout to also run it on the branch.
109
110
964 by Martin Pool
- show progress on dumb terminals by printing dots
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):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
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'/-\|'
661 by Martin Pool
- limit rate at which progress bar is updated
156
    MIN_PAUSE = 0.1 # seconds
157
964 by Martin Pool
- show progress on dumb terminals by printing dots
158
159
    def __init__(self, **kwargs):
1185.33.60 by Martin Pool
Use full terminal width for verbose test output.
160
        from bzrlib.osutils import terminal_width
964 by Martin Pool
- show progress on dumb terminals by printing dots
161
        _BaseProgressBar.__init__(self, **kwargs)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
162
        self.spin_pos = 0
1185.33.60 by Martin Pool
Use full terminal width for verbose test output.
163
        self.width = terminal_width()
964 by Martin Pool
- show progress on dumb terminals by printing dots
164
        self.start_time = None
165
        self.last_update = None
1185.16.75 by Martin Pool
- improved eta estimation for progress bar
166
        self.last_updates = deque()
964 by Martin Pool
- show progress on dumb terminals by printing dots
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
1185.16.75 by Martin Pool
- improved eta estimation for progress bar
180
        self.last_updates.append(now - self.last_update)
964 by Martin Pool
- show progress on dumb terminals by printing dots
181
        self.last_update = now
182
        return False
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
183
        
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
184
185
    def tick(self):
186
        self.update(self.last_msg, self.last_cnt, self.last_total)
187
                 
188
189
667 by Martin Pool
- allow for progressbar updates with no count, only a message
190
    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
191
        """Update and redraw progress bar."""
192
1308 by Martin Pool
- make progress bar more tolerant of out-of-range values
193
        if current_cnt < 0:
194
            current_cnt = 0
195
            
196
        if current_cnt > total_cnt:
197
            total_cnt = current_cnt
198
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
199
        # save these for the tick() function
200
        self.last_msg = msg
201
        self.last_cnt = current_cnt
202
        self.last_total = total_cnt
203
            
964 by Martin Pool
- show progress on dumb terminals by printing dots
204
        if self.throttle():
205
            return 
661 by Martin Pool
- limit rate at which progress bar is updated
206
        
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
207
        if self.show_eta and self.start_time and total_cnt:
1185.16.75 by Martin Pool
- improved eta estimation for progress bar
208
            eta = get_eta(self.start_time, current_cnt, total_cnt,
209
                    last_updates = self.last_updates)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
210
            eta_str = " " + str_tdelta(eta)
211
        else:
212
            eta_str = ""
213
214
        if self.show_spinner:
215
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
216
        else:
217
            spin_str = ''
218
219
        # always update this; it's also used for the bar
220
        self.spin_pos += 1
221
222
        if self.show_pct and total_cnt and current_cnt:
223
            pct = 100.0 * current_cnt / total_cnt
224
            pct_str = ' (%5.1f%%)' % pct
225
        else:
226
            pct_str = ''
227
228
        if not self.show_count:
229
            count_str = ''
230
        elif current_cnt is None:
231
            count_str = ''
232
        elif total_cnt is None:
233
            count_str = ' %i' % (current_cnt)
234
        else:
235
            # make both fields the same size
236
            t = '%i' % (total_cnt)
237
            c = '%*i' % (len(t), current_cnt)
238
            count_str = ' ' + c + '/' + t 
239
240
        if self.show_bar:
241
            # progress bar, if present, soaks up all remaining space
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
242
            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
243
                   - len(eta_str) - len(count_str) - 3
244
245
            if total_cnt:
246
                # number of markers highlighted in bar
247
                markers = int(round(float(cols) * current_cnt / total_cnt))
248
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
249
            elif False:
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
250
                # don't know total, so can't show completion.
251
                # so just show an expanded spinning thingy
252
                m = self.spin_pos % cols
668 by Martin Pool
- fix sweeping bar progress indicator
253
                ms = (' ' * m + '*').ljust(cols)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
254
                
255
                bar_str = '[' + ms + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
256
            else:
257
                bar_str = ''
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
258
        else:
259
            bar_str = ''
260
261
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
262
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
263
        assert len(m) < self.width
264
        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
265
        #self.to_file.flush()
266
            
964 by Martin Pool
- show progress on dumb terminals by printing dots
267
    def clear(self):        
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
268
        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
269
        #self.to_file.flush()        
649 by Martin Pool
- some cleanups for the progressbar method
270
648 by Martin Pool
- import aaron's progress-indicator code
271
        
272
def str_tdelta(delt):
273
    if delt is None:
274
        return "-:--:--"
660 by Martin Pool
- use plain unix time, not datetime module
275
    delt = int(round(delt))
276
    return '%d:%02d:%02d' % (delt/3600,
277
                             (delt/60) % 60,
278
                             delt % 60)
279
280
1185.16.75 by Martin Pool
- improved eta estimation for progress bar
281
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
282
    if start_time is None:
283
        return None
284
285
    if not total:
286
        return None
287
288
    if current < enough_samples:
289
        return None
290
291
    if current > total:
292
        return None                     # wtf?
293
294
    elapsed = time.time() - start_time
295
296
    if elapsed < 2.0:                   # not enough time to estimate
297
        return None
298
    
299
    total_duration = float(elapsed) * float(total) / float(current)
300
301
    assert total_duration >= elapsed
302
1185.16.75 by Martin Pool
- improved eta estimation for progress bar
303
    if last_updates and len(last_updates) >= n_recent:
304
        while len(last_updates) > n_recent:
305
            last_updates.popleft()
306
        avg = sum(last_updates) / float(len(last_updates))
307
        time_left = avg * (total - current)
308
309
        old_time_left = total_duration - elapsed
310
311
        # We could return the average, or some other value here
312
        return (time_left + old_time_left) / 2
313
660 by Martin Pool
- use plain unix time, not datetime module
314
    return total_duration - elapsed
648 by Martin Pool
- import aaron's progress-indicator code
315
649 by Martin Pool
- some cleanups for the progressbar method
316
648 by Martin Pool
- import aaron's progress-indicator code
317
def run_tests():
318
    import doctest
319
    result = doctest.testmod()
320
    if result[1] > 0:
321
        if result[0] == 0:
322
            print "All tests passed"
323
    else:
324
        print "No tests to run"
649 by Martin Pool
- some cleanups for the progressbar method
325
326
327
def demo():
964 by Martin Pool
- show progress on dumb terminals by printing dots
328
    sleep = time.sleep
329
    
330
    print 'dumb-terminal test:'
331
    pb = DotsProgressBar()
332
    for i in range(100):
333
        pb.update('Leoparden', i, 99)
334
        sleep(0.1)
335
    sleep(1.5)
336
    pb.clear()
337
    sleep(1.5)
338
    
339
    print 'smart-terminal test:'
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
340
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
649 by Martin Pool
- some cleanups for the progressbar method
341
    for i in range(100):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
342
        pb.update('Elephanten', i, 99)
343
        sleep(0.1)
344
    sleep(2)
345
    pb.clear()
346
    sleep(1)
964 by Martin Pool
- show progress on dumb terminals by printing dots
347
649 by Martin Pool
- some cleanups for the progressbar method
348
    print 'done!'
349
648 by Martin Pool
- import aaron's progress-indicator code
350
if __name__ == "__main__":
649 by Martin Pool
- some cleanups for the progressbar method
351
    demo()