~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
655 by Martin Pool
- better calculation of progress bar position
33
# TODO: Perhaps don't write updates faster than a certain rate, say
34
# 5/second.
35
934 by Martin Pool
todo
36
# TODO: Optionally show elapsed time instead/as well as ETA; nicer
37
# when the rate is unpredictable
38
649 by Martin Pool
- some cleanups for the progressbar method
39
648 by Martin Pool
- import aaron's progress-indicator code
40
import sys
660 by Martin Pool
- use plain unix time, not datetime module
41
import time
648 by Martin Pool
- import aaron's progress-indicator code
42
649 by Martin Pool
- some cleanups for the progressbar method
43
44
def _width():
45
    """Return estimated terminal width.
46
47
    TODO: Do something smart on Windows?
48
49
    TODO: Is there anything that gets a better update when the window
50
          is resized while the program is running?
51
    """
52
    import os
53
    try:
54
        return int(os.environ['COLUMNS'])
55
    except (IndexError, KeyError, ValueError):
56
        return 80
57
58
59
def _supports_progress(f):
695 by Martin Pool
- don't display progress bars on really dumb terminals
60
    if not hasattr(f, 'isatty'):
61
        return False
62
    if not f.isatty():
63
        return False
64
    import os
65
    if os.environ.get('TERM') == 'dumb':
66
        # e.g. emacs compile window
67
        return False
68
    return True
649 by Martin Pool
- some cleanups for the progressbar method
69
70
71
648 by Martin Pool
- import aaron's progress-indicator code
72
class ProgressBar(object):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
73
    """Progress bar display object.
74
75
    Several options are available to control the display.  These can
76
    be passed as parameters to the constructor or assigned at any time:
77
78
    show_pct
79
        Show percentage complete.
80
    show_spinner
81
        Show rotating baton.  This ticks over on every update even
82
        if the values don't change.
83
    show_eta
84
        Show predicted time-to-completion.
85
    show_bar
86
        Show bar graph.
87
    show_count
88
        Show numerical counts.
89
90
    The output file should be in line-buffered or unbuffered mode.
91
    """
92
    SPIN_CHARS = r'/-\|'
661 by Martin Pool
- limit rate at which progress bar is updated
93
    MIN_PAUSE = 0.1 # seconds
94
95
    start_time = None
96
    last_update = None
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
97
    
98
    def __init__(self,
99
                 to_file=sys.stderr,
100
                 show_pct=False,
101
                 show_spinner=False,
102
                 show_eta=True,
103
                 show_bar=True,
104
                 show_count=True):
649 by Martin Pool
- some cleanups for the progressbar method
105
        object.__init__(self)
106
        self.to_file = to_file
107
        self.suppressed = not _supports_progress(self.to_file)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
108
        self.spin_pos = 0
109
 
681 by Martin Pool
- assign missing fields in Progress object
110
        self.last_msg = None
111
        self.last_cnt = None
112
        self.last_total = None
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
113
        self.show_pct = show_pct
114
        self.show_spinner = show_spinner
115
        self.show_eta = show_eta
116
        self.show_bar = show_bar
117
        self.show_count = show_count
118
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
119
        self.width = _width()
120
        
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
121
122
    def tick(self):
123
        self.update(self.last_msg, self.last_cnt, self.last_total)
124
                 
125
126
667 by Martin Pool
- allow for progressbar updates with no count, only a message
127
    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
128
        """Update and redraw progress bar."""
661 by Martin Pool
- limit rate at which progress bar is updated
129
        if self.suppressed:
130
            return
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
131
132
        # save these for the tick() function
133
        self.last_msg = msg
134
        self.last_cnt = current_cnt
135
        self.last_total = total_cnt
136
            
661 by Martin Pool
- limit rate at which progress bar is updated
137
        now = time.time()
138
        if self.start_time is None:
139
            self.start_time = now
140
        else:
141
            interval = now - self.last_update
142
            if interval > 0 and interval < self.MIN_PAUSE:
143
                return
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
144
661 by Martin Pool
- limit rate at which progress bar is updated
145
        self.last_update = now
146
        
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
147
        if total_cnt:
148
            assert current_cnt <= total_cnt
149
        if current_cnt:
150
            assert current_cnt >= 0
151
        
152
        if self.show_eta and self.start_time and total_cnt:
153
            eta = get_eta(self.start_time, current_cnt, total_cnt)
154
            eta_str = " " + str_tdelta(eta)
155
        else:
156
            eta_str = ""
157
158
        if self.show_spinner:
159
            spin_str = self.SPIN_CHARS[self.spin_pos % 4] + ' '            
160
        else:
161
            spin_str = ''
162
163
        # always update this; it's also used for the bar
164
        self.spin_pos += 1
165
166
        if self.show_pct and total_cnt and current_cnt:
167
            pct = 100.0 * current_cnt / total_cnt
168
            pct_str = ' (%5.1f%%)' % pct
169
        else:
170
            pct_str = ''
171
172
        if not self.show_count:
173
            count_str = ''
174
        elif current_cnt is None:
175
            count_str = ''
176
        elif total_cnt is None:
177
            count_str = ' %i' % (current_cnt)
178
        else:
179
            # make both fields the same size
180
            t = '%i' % (total_cnt)
181
            c = '%*i' % (len(t), current_cnt)
182
            count_str = ' ' + c + '/' + t 
183
184
        if self.show_bar:
185
            # progress bar, if present, soaks up all remaining space
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
186
            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
187
                   - len(eta_str) - len(count_str) - 3
188
189
            if total_cnt:
190
                # number of markers highlighted in bar
191
                markers = int(round(float(cols) * current_cnt / total_cnt))
192
                bar_str = '[' + ('=' * markers).ljust(cols) + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
193
            elif False:
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
194
                # don't know total, so can't show completion.
195
                # so just show an expanded spinning thingy
196
                m = self.spin_pos % cols
668 by Martin Pool
- fix sweeping bar progress indicator
197
                ms = (' ' * m + '*').ljust(cols)
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
198
                
199
                bar_str = '[' + ms + '] '
669 by Martin Pool
- don't show progress bar unless completion is known
200
            else:
201
                bar_str = ''
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
202
        else:
203
            bar_str = ''
204
205
        m = spin_str + bar_str + msg + count_str + pct_str + eta_str
206
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
207
        assert len(m) < self.width
208
        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
209
        #self.to_file.flush()
210
            
649 by Martin Pool
- some cleanups for the progressbar method
211
212
    def clear(self):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
213
        if self.suppressed:
214
            return
215
        
929 by Martin Pool
- progress bar: avoid repeatedly checking screen width
216
        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
217
        #self.to_file.flush()        
649 by Martin Pool
- some cleanups for the progressbar method
218
    
219
648 by Martin Pool
- import aaron's progress-indicator code
220
        
221
def str_tdelta(delt):
222
    if delt is None:
223
        return "-:--:--"
660 by Martin Pool
- use plain unix time, not datetime module
224
    delt = int(round(delt))
225
    return '%d:%02d:%02d' % (delt/3600,
226
                             (delt/60) % 60,
227
                             delt % 60)
228
229
230
def get_eta(start_time, current, total, enough_samples=3):
231
    if start_time is None:
232
        return None
233
234
    if not total:
235
        return None
236
237
    if current < enough_samples:
238
        return None
239
240
    if current > total:
241
        return None                     # wtf?
242
243
    elapsed = time.time() - start_time
244
245
    if elapsed < 2.0:                   # not enough time to estimate
246
        return None
247
    
248
    total_duration = float(elapsed) * float(total) / float(current)
249
250
    assert total_duration >= elapsed
251
252
    return total_duration - elapsed
648 by Martin Pool
- import aaron's progress-indicator code
253
649 by Martin Pool
- some cleanups for the progressbar method
254
648 by Martin Pool
- import aaron's progress-indicator code
255
def run_tests():
256
    import doctest
257
    result = doctest.testmod()
258
    if result[1] > 0:
259
        if result[0] == 0:
260
            print "All tests passed"
261
    else:
262
        print "No tests to run"
649 by Martin Pool
- some cleanups for the progressbar method
263
264
265
def demo():
266
    from time import sleep
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
267
    pb = ProgressBar(show_pct=True, show_bar=True, show_spinner=False)
649 by Martin Pool
- some cleanups for the progressbar method
268
    for i in range(100):
658 by Martin Pool
- clean up and add a bunch of options to the progress indicator
269
        pb.update('Elephanten', i, 99)
270
        sleep(0.1)
271
    sleep(2)
272
    pb.clear()
273
    sleep(1)
649 by Martin Pool
- some cleanups for the progressbar method
274
    print 'done!'
275
648 by Martin Pool
- import aaron's progress-indicator code
276
if __name__ == "__main__":
649 by Martin Pool
- some cleanups for the progressbar method
277
    demo()