##// END OF EJS Templates
progress: use %d to format ints instead of %s...
Augie Fackler -
r36170:7f5108e5 default
parent child Browse files
Show More
@@ -1,303 +1,307
1 1 # progress.py progress bars related code
2 2 #
3 3 # Copyright (C) 2010 Augie Fackler <durin42@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import threading
12 12 import time
13 13
14 14 from .i18n import _
15 15 from . import encoding
16 16
17 17 def spacejoin(*args):
18 18 return ' '.join(s for s in args if s)
19 19
20 20 def shouldprint(ui):
21 21 return not (ui.quiet or ui.plain('progress')) and (
22 22 ui._isatty(ui.ferr) or ui.configbool('progress', 'assume-tty'))
23 23
24 24 def fmtremaining(seconds):
25 25 """format a number of remaining seconds in human readable way
26 26
27 27 This will properly display seconds, minutes, hours, days if needed"""
28 28 if seconds < 60:
29 29 # i18n: format XX seconds as "XXs"
30 30 return _("%02ds") % (seconds)
31 31 minutes = seconds // 60
32 32 if minutes < 60:
33 33 seconds -= minutes * 60
34 34 # i18n: format X minutes and YY seconds as "XmYYs"
35 35 return _("%dm%02ds") % (minutes, seconds)
36 36 # we're going to ignore seconds in this case
37 37 minutes += 1
38 38 hours = minutes // 60
39 39 minutes -= hours * 60
40 40 if hours < 30:
41 41 # i18n: format X hours and YY minutes as "XhYYm"
42 42 return _("%dh%02dm") % (hours, minutes)
43 43 # we're going to ignore minutes in this case
44 44 hours += 1
45 45 days = hours // 24
46 46 hours -= days * 24
47 47 if days < 15:
48 48 # i18n: format X days and YY hours as "XdYYh"
49 49 return _("%dd%02dh") % (days, hours)
50 50 # we're going to ignore hours in this case
51 51 days += 1
52 52 weeks = days // 7
53 53 days -= weeks * 7
54 54 if weeks < 55:
55 55 # i18n: format X weeks and YY days as "XwYYd"
56 56 return _("%dw%02dd") % (weeks, days)
57 57 # we're going to ignore days and treat a year as 52 weeks
58 58 weeks += 1
59 59 years = weeks // 52
60 60 weeks -= years * 52
61 61 # i18n: format X years and YY weeks as "XyYYw"
62 62 return _("%dy%02dw") % (years, weeks)
63 63
64 64 # file_write() and file_flush() of Python 2 do not restart on EINTR if
65 65 # the file is attached to a "slow" device (e.g. a terminal) and raise
66 66 # IOError. We cannot know how many bytes would be written by file_write(),
67 67 # but a progress text is known to be short enough to be written by a
68 68 # single write() syscall, so we can just retry file_write() with the whole
69 69 # text. (issue5532)
70 70 #
71 71 # This should be a short-term workaround. We'll need to fix every occurrence
72 72 # of write() to a terminal or pipe.
73 73 def _eintrretry(func, *args):
74 74 while True:
75 75 try:
76 76 return func(*args)
77 77 except IOError as err:
78 78 if err.errno == errno.EINTR:
79 79 continue
80 80 raise
81 81
82 82 class progbar(object):
83 83 def __init__(self, ui):
84 84 self.ui = ui
85 85 self._refreshlock = threading.Lock()
86 86 self.resetstate()
87 87
88 88 def resetstate(self):
89 89 self.topics = []
90 90 self.topicstates = {}
91 91 self.starttimes = {}
92 92 self.startvals = {}
93 93 self.printed = False
94 94 self.lastprint = time.time() + float(self.ui.config(
95 95 'progress', 'delay'))
96 96 self.curtopic = None
97 97 self.lasttopic = None
98 98 self.indetcount = 0
99 99 self.refresh = float(self.ui.config(
100 100 'progress', 'refresh'))
101 101 self.changedelay = max(3 * self.refresh,
102 102 float(self.ui.config(
103 103 'progress', 'changedelay')))
104 104 self.order = self.ui.configlist('progress', 'format')
105 105 self.estimateinterval = self.ui.configwith(
106 106 float, 'progress', 'estimateinterval')
107 107
108 108 def show(self, now, topic, pos, item, unit, total):
109 109 if not shouldprint(self.ui):
110 110 return
111 111 termwidth = self.width()
112 112 self.printed = True
113 113 head = ''
114 114 needprogress = False
115 115 tail = ''
116 116 for indicator in self.order:
117 117 add = ''
118 118 if indicator == 'topic':
119 119 add = topic
120 120 elif indicator == 'number':
121 121 if total:
122 122 padamount = '%d' % len(str(total))
123 add = ('% '+ padamount + 's/%s') % (pos, total)
123 # '% 1d' % 1 adds an extra space compared to '% 1s' % 1.
124 # To avoid this change in output, we convert to a string
125 # first, then do the padding.
126 spos = '%d' % pos
127 add = ('% '+ padamount + 's/%d') % (spos, total)
124 128 else:
125 129 add = str(pos)
126 130 elif indicator.startswith('item') and item:
127 131 slice = 'end'
128 132 if '-' in indicator:
129 133 wid = int(indicator.split('-')[1])
130 134 elif '+' in indicator:
131 135 slice = 'beginning'
132 136 wid = int(indicator.split('+')[1])
133 137 else:
134 138 wid = 20
135 139 if slice == 'end':
136 140 add = encoding.trim(item, wid, leftside=True)
137 141 else:
138 142 add = encoding.trim(item, wid)
139 143 add += (wid - encoding.colwidth(add)) * ' '
140 144 elif indicator == 'bar':
141 145 add = ''
142 146 needprogress = True
143 147 elif indicator == 'unit' and unit:
144 148 add = unit
145 149 elif indicator == 'estimate':
146 150 add = self.estimate(topic, pos, total, now)
147 151 elif indicator == 'speed':
148 152 add = self.speed(topic, pos, unit, now)
149 153 if not needprogress:
150 154 head = spacejoin(head, add)
151 155 else:
152 156 tail = spacejoin(tail, add)
153 157 if needprogress:
154 158 used = 0
155 159 if head:
156 160 used += encoding.colwidth(head) + 1
157 161 if tail:
158 162 used += encoding.colwidth(tail) + 1
159 163 progwidth = termwidth - used - 3
160 164 if total and pos <= total:
161 165 amt = pos * progwidth // total
162 166 bar = '=' * (amt - 1)
163 167 if amt > 0:
164 168 bar += '>'
165 169 bar += ' ' * (progwidth - amt)
166 170 else:
167 171 progwidth -= 3
168 172 self.indetcount += 1
169 173 # mod the count by twice the width so we can make the
170 174 # cursor bounce between the right and left sides
171 175 amt = self.indetcount % (2 * progwidth)
172 176 amt -= progwidth
173 177 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
174 178 ' ' * int(abs(amt)))
175 179 prog = ''.join(('[', bar, ']'))
176 180 out = spacejoin(head, prog, tail)
177 181 else:
178 182 out = spacejoin(head, tail)
179 183 self._writeerr('\r' + encoding.trim(out, termwidth))
180 184 self.lasttopic = topic
181 185 self._flusherr()
182 186
183 187 def clear(self):
184 188 if not self.printed or not self.lastprint or not shouldprint(self.ui):
185 189 return
186 190 self._writeerr('\r%s\r' % (' ' * self.width()))
187 191 if self.printed:
188 192 # force immediate re-paint of progress bar
189 193 self.lastprint = 0
190 194
191 195 def complete(self):
192 196 if not shouldprint(self.ui):
193 197 return
194 198 if self.ui.configbool('progress', 'clear-complete'):
195 199 self.clear()
196 200 else:
197 201 self._writeerr('\n')
198 202 self._flusherr()
199 203
200 204 def _flusherr(self):
201 205 _eintrretry(self.ui.ferr.flush)
202 206
203 207 def _writeerr(self, msg):
204 208 _eintrretry(self.ui.ferr.write, msg)
205 209
206 210 def width(self):
207 211 tw = self.ui.termwidth()
208 212 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
209 213
210 214 def estimate(self, topic, pos, total, now):
211 215 if total is None:
212 216 return ''
213 217 initialpos = self.startvals[topic]
214 218 target = total - initialpos
215 219 delta = pos - initialpos
216 220 if delta > 0:
217 221 elapsed = now - self.starttimes[topic]
218 222 seconds = (elapsed * (target - delta)) // delta + 1
219 223 return fmtremaining(seconds)
220 224 return ''
221 225
222 226 def speed(self, topic, pos, unit, now):
223 227 initialpos = self.startvals[topic]
224 228 delta = pos - initialpos
225 229 elapsed = now - self.starttimes[topic]
226 230 if elapsed > 0:
227 231 return _('%d %s/sec') % (delta / elapsed, unit)
228 232 return ''
229 233
230 234 def _oktoprint(self, now):
231 235 '''Check if conditions are met to print - e.g. changedelay elapsed'''
232 236 if (self.lasttopic is None # first time we printed
233 237 # not a topic change
234 238 or self.curtopic == self.lasttopic
235 239 # it's been long enough we should print anyway
236 240 or now - self.lastprint >= self.changedelay):
237 241 return True
238 242 else:
239 243 return False
240 244
241 245 def _calibrateestimate(self, topic, now, pos):
242 246 '''Adjust starttimes and startvals for topic so ETA works better
243 247
244 248 If progress is non-linear (ex. get much slower in the last minute),
245 249 it's more friendly to only use a recent time span for ETA and speed
246 250 calculation.
247 251
248 252 [======================================> ]
249 253 ^^^^^^^
250 254 estimateinterval, only use this for estimation
251 255 '''
252 256 interval = self.estimateinterval
253 257 if interval <= 0:
254 258 return
255 259 elapsed = now - self.starttimes[topic]
256 260 if elapsed > interval:
257 261 delta = pos - self.startvals[topic]
258 262 newdelta = delta * interval / elapsed
259 263 # If a stall happens temporarily, ETA could change dramatically
260 264 # frequently. This is to avoid such dramatical change and make ETA
261 265 # smoother.
262 266 if newdelta < 0.1:
263 267 return
264 268 self.startvals[topic] = pos - newdelta
265 269 self.starttimes[topic] = now - interval
266 270
267 271 def progress(self, topic, pos, item='', unit='', total=None):
268 272 now = time.time()
269 273 self._refreshlock.acquire()
270 274 try:
271 275 if pos is None:
272 276 self.starttimes.pop(topic, None)
273 277 self.startvals.pop(topic, None)
274 278 self.topicstates.pop(topic, None)
275 279 # reset the progress bar if this is the outermost topic
276 280 if self.topics and self.topics[0] == topic and self.printed:
277 281 self.complete()
278 282 self.resetstate()
279 283 # truncate the list of topics assuming all topics within
280 284 # this one are also closed
281 285 if topic in self.topics:
282 286 self.topics = self.topics[:self.topics.index(topic)]
283 287 # reset the last topic to the one we just unwound to,
284 288 # so that higher-level topics will be stickier than
285 289 # lower-level topics
286 290 if self.topics:
287 291 self.lasttopic = self.topics[-1]
288 292 else:
289 293 self.lasttopic = None
290 294 else:
291 295 if topic not in self.topics:
292 296 self.starttimes[topic] = now
293 297 self.startvals[topic] = pos
294 298 self.topics.append(topic)
295 299 self.topicstates[topic] = pos, item, unit, total
296 300 self.curtopic = topic
297 301 self._calibrateestimate(topic, now, pos)
298 302 if now - self.lastprint >= self.refresh and self.topics:
299 303 if self._oktoprint(now):
300 304 self.lastprint = now
301 305 self.show(now, topic, *self.topicstates[topic])
302 306 finally:
303 307 self._refreshlock.release()
General Comments 0
You need to be logged in to leave comments. Login now