Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/progress.py: 94%
279 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-04 09:52 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-04 09:52 +0000
1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6'''
7Inform users about the progress and success/fail state of long-running tasks.
8'''
10import sys
11import time
12import logging
14from .get_terminal_size import get_terminal_size
16logger = logging.getLogger('pyrocko.progress')
18# TODO: Refactor so that if multiple viewers are attached, they can do
19# their updates independently of each other (at independent time intervals).
21# TODO: Refactor so that different viewers can render task states differently.
23# spinner = u'\u25dc\u25dd\u25de\u25df'
24# spinner = '⣾⣽⣻⢿⡿⣟⣯⣷'
25spinner = '◴◷◶◵'
26skull = u'\u2620'
27check = u'\u2714'
28cross = u'\u2716'
29bar = u'[- ]'
30blocks = u'\u2588\u2589\u258a\u258b\u258c\u258d\u258e\u258f '
32symbol_done = check
33symbol_failed = cross # skull
35ansi_up = u'\033[%iA'
36ansi_down = u'\033[%iB'
37ansi_left = u'\033[%iC'
38ansi_right = u'\033[%iD'
39ansi_next_line = u'\033E'
41ansi_erase_display = u'\033[2J'
42ansi_window = u'\033[%i;%ir'
43ansi_move_to = u'\033[%i;%iH'
45ansi_clear_down = u'\033[0J'
46ansi_clear_up = u'\033[1J'
47ansi_clear = u'\033[2J'
49ansi_clear_right = u'\033[0K'
51ansi_scroll_up = u'\033D'
52ansi_scroll_down = u'\033M'
54ansi_reset = u'\033c'
57g_force_viewer_off = False
59g_viewer = 'terminal'
62def set_default_viewer(viewer):
63 global g_viewer
64 g_viewer = viewer
67class StatusViewer(object):
69 def __init__(self, parent=None):
70 self._parent = parent
72 def __enter__(self):
73 return self
75 def __exit__(self, *_):
76 self.stop()
78 def stop(self):
79 if self._parent:
80 self._parent.hide(self)
82 def draw(self, lines):
83 pass
86class TerminalStatusViewer(StatusViewer):
87 def __init__(self, parent=None):
88 self._terminal_size = get_terminal_size()
89 self._height = 0
90 self._state = 0
91 self._parent = parent
93 def print(self, s):
94 print(s, end='', file=sys.stderr)
96 def flush(self):
97 print('', end='', flush=True, file=sys.stderr)
99 def start(self):
100 sx, sy = self._terminal_size
101 self._state = 1
103 def stop(self):
104 if self._state == 1:
105 sx, sy = self._terminal_size
106 self._resize(0)
107 self.print(ansi_move_to % (sy-self._height, 1))
108 self.flush()
110 self._state = 2
111 if self._parent:
112 self._parent.hide(self)
114 def _start_show(self):
115 sx, sy = self._terminal_size
116 self.print(ansi_move_to % (sy-self._height+1, 1))
118 def _end_show(self):
119 sx, sy = self._terminal_size
120 self.print(ansi_move_to % (sy-self._height, 1))
121 self.print(ansi_clear_right)
123 def _resize(self, height):
124 sx, sy = self._terminal_size
125 k = height - self._height
126 if k > 0:
127 self.print(ansi_scroll_up * k)
128 self.print(ansi_window % (1, sy-height))
129 if k < 0:
130 self.print(ansi_window % (1, sy-height))
131 self.print(ansi_scroll_down * abs(k))
133 self._height = height
135 def draw(self, lines):
136 if self._state == 0:
137 self.start()
139 if self._state != 1:
140 return
142 self._terminal_size = get_terminal_size()
143 sx, sy = self._terminal_size
144 nlines = len(lines)
145 self._resize(nlines)
146 self._start_show()
148 for iline, line in enumerate(reversed(lines)):
149 if len(line) > sx - 1:
150 line = line[:sx-1]
152 self.print(ansi_clear_right + line)
153 if iline != nlines - 1:
154 self.print(ansi_next_line)
156 self._end_show()
157 self.flush()
160class LogStatusViewer(StatusViewer):
162 def draw(self, lines):
163 if lines:
164 logger.info(
165 'Progress:\n%s' % '\n'.join(' '+line for line in lines))
168class DummyStatusViewer(StatusViewer):
169 pass
172class Task(object):
173 def __init__(
174 self, progress, id, name, n, state='working', logger=None,
175 group=None):
177 self._id = id
178 self._name = name
179 self._condition = ''
180 self._ispin = 0
181 self._i = None
182 self._n = n
183 self._done = False
184 assert state in ('waiting', 'working')
185 self._state = state
186 self._progress = progress
187 self._logger = logger
188 self._tcreate = time.time()
189 self._group = group
191 def __enter__(self):
192 return self
194 def __exit__(self, type, value, tb):
195 if type is None:
196 self.done()
197 else:
198 self.fail()
200 def __call__(self, it):
201 try:
202 self._n = len(it)
203 except TypeError:
204 self._n = None
206 clean = False
207 try:
208 n = 0
209 for obj in it:
210 self.update(n)
211 yield obj
212 n += 1
214 self.update(n)
215 clean = True
217 finally:
218 if clean:
219 self.done()
220 else:
221 self.fail()
223 def log(self, s):
224 if self._logger is not None:
225 self._logger.info(s)
227 def get_group_time_start(self):
228 if self._group:
229 return self._group.get_group_time_start()
230 else:
231 return self._tcreate
233 def task(self, *args, **kwargs):
234 kwargs['group'] = self
235 return self._progress.task(*args, **kwargs)
237 def update(self, i=None, condition=''):
238 self._state = 'working'
240 self._condition = condition
242 if i is not None:
243 if self._n is not None:
244 i = min(i, self._n)
246 self._i = i
248 self._progress._update()
250 def done(self, condition=''):
251 self.duration = time.time() - self._tcreate
253 if self._state in ('done', 'failed'):
254 return
256 self._condition = condition
257 self._state = 'done'
258 self._progress._end(self)
259 self.log(str(self))
261 def fail(self, condition=''):
262 self.duration = time.time() - self._tcreate
264 if self._state in ('done', 'failed'):
265 return
267 self._condition = condition
268 self._state = 'failed'
269 self._progress._end(self)
270 self.log(str(self))
272 def _str_state(self):
273 s = self._state
274 if s == 'waiting':
275 return ' '
276 elif s == 'working':
277 self._ispin += 1
278 return spinner[self._ispin % len(spinner)] + ' '
279 elif s == 'done':
280 return symbol_done + ' '
281 elif s == 'failed':
282 return symbol_failed + ' '
283 else:
284 return '? '
286 def _idisplay(self):
287 i = self._i
288 if self._n is not None and i > self._n:
289 i = self._n
290 return i
292 def _str_progress(self):
293 if self._i is None:
294 return self._state
295 elif self._n is None:
296 if self._state != 'working':
297 return '... %s (%i)' % (self._state, self._idisplay())
298 else:
299 return '%i' % self._idisplay()
300 else:
301 if self._state == 'working':
302 nw = len(str(self._n))
303 return (('%' + str(nw) + 'i / %i') % (
304 self._idisplay(), self._n)).center(11)
306 elif self._state == 'failed':
307 return '... %s (%i / %i)' % (
308 self._state, self._idisplay(), self._n)
309 else:
310 return '... %s (%i)' % (self._state, self._n)
312 def _str_percent(self):
313 if self._state == 'working' and self._n is not None and self._n >= 4 \
314 and self._i is not None:
315 return '%3.0f%%' % ((100. * self._i) / self._n)
316 else:
317 return ''
319 def _str_condition(self):
320 if self._condition:
321 return '%s' % self._condition
322 else:
323 return ''
325 def _str_bar(self):
326 if self._state == 'working' and self._n is not None and self._n >= 4 \
327 and self._i is not None:
328 nb = 20
329 fb = nb * float(self._i) / self._n
330 ib = int(fb)
331 ip = int((fb - ib) * (len(blocks)-1))
332 if ib == 0 and ip == 0:
333 ip = 1 # indication of start
334 s = blocks[0] * ib
335 if ib < nb:
336 s += blocks[-1-ip] + (nb - ib - 1) * blocks[-1] + blocks[-2]
338 # s = ' ' + bar[0] + bar[1] * ib + bar[2] * (nb - ib) + bar[3]
339 return s
340 else:
341 return ''
343 def __str__(self):
344 return '%s%-23s %-11s %s%-4s %s' % (
345 self._str_state(),
346 self._name,
347 self._str_progress(),
348 self._str_bar(),
349 self._str_percent(),
350 self._str_condition())
353class Progress(object):
355 def __init__(self):
356 self._current_id = 0
357 self._current_group_id = 0
358 self._tasks = {}
359 self._tasks_done = []
360 self._last_update = 0.0
361 self._terms = []
363 def view(self, viewer=None):
364 if g_force_viewer_off or self._terms:
365 viewer = 'off'
366 elif viewer is None:
367 viewer = g_viewer
369 try:
370 term = g_viewer_classes[viewer](self)
371 except KeyError:
372 raise ValueError('Invalid viewer choice: %s' % viewer)
374 self._terms.append(term)
375 return term
377 def hide(self, term):
378 self._update(force=True)
379 self._terms.remove(term)
381 def task(self, name, n=None, logger=None, group=None):
382 self._current_id += 1
383 task = Task(
384 self, self._current_id, name, n, logger=logger, group=group)
385 self._tasks[task._id] = task
386 self._update(force=True)
387 return task
389 def _end(self, task):
390 del self._tasks[task._id]
391 self._tasks_done.append(task)
392 self._update(force=True)
394 def _update(self, force=False):
395 now = time.time()
396 if self._last_update + 0.1 < now or force:
397 self._tasks_done = []
399 lines = self._lines()
400 for term in self._terms:
401 term.draw(lines)
403 self._last_update = now
405 def _lines(self):
406 task_ids = sorted(self._tasks)
407 lines = []
408 for task_id in task_ids:
409 task = self._tasks[task_id]
410 lines.extend(str(task).splitlines())
412 return lines
415g_viewer_classes = {
416 'terminal': TerminalStatusViewer,
417 'log': LogStatusViewer,
418 'off': DummyStatusViewer}
420progress = Progress()