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