Coverage for /usr/local/lib/python3.13/dist-packages/pyrocko/progress.py: 74%
329 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +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
13import threading
15from shutil import get_terminal_size
17logger = logging.getLogger('pyrocko.progress')
19g_spinners = [
20 '⣾⣽⣻⢿⡿⣟⣯⣷',
21 '◴◷◶◵',
22 '\u25dc\u25dd\u25de\u25df',
23 '0123456789']
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'
40ansi_previous_line = u'\033[1F'
42ansi_erase_display = u'\033[2J'
43ansi_window = u'\033[%i;%ir'
44ansi_move_to = u'\033[%i;%iH'
46ansi_clear_down = u'\033[0J'
47ansi_clear_up = u'\033[1J'
48ansi_clear = u'\033[2J'
50ansi_clear_right = u'\033[0K'
52ansi_scroll_up = u'\033D'
53ansi_scroll_down = u'\033M'
55ansi_reset = u'\033c'
57ansi_save = u'\033 7'
58ansi_restore = u'\033 8'
61g_force_viewer_off = False
63g_viewer = 'terminal'
66def set_default_viewer(viewer):
67 '''
68 Set default viewer for progress indicators.
70 :param viewer:
71 Name of viewer, choices: ``'terminal'``, ``'log'``, ``'off'``, default:
72 ``'terminal'``.
73 :type viewer:
74 str
75 '''
77 global g_viewer
78 assert viewer in g_viewer_classes
79 g_viewer = viewer
82class StatusViewer(object):
84 def __init__(self, parent, interval=0.1, delay=1.0):
85 self._parent = parent
86 self._interval = interval
87 self._delay = delay
88 self._last_update = 0.0
89 self._created = time.time()
91 def __enter__(self):
92 return self
94 def __exit__(self, *_):
95 self.cleanup()
97 if self._parent:
98 self._parent._remove_viewer(self)
100 def cleanup(self):
101 pass
103 def update(self, force):
104 now = time.time()
105 if now < self._created + self._delay:
106 return
108 if self._last_update + self._interval < now or force:
109 self._draw()
110 self._last_update = now
112 def _draw(self):
113 pass
116class DummyStatusViewer(StatusViewer):
117 pass
120class LogStatusViewer(StatusViewer):
122 def __init__(self, parent):
123 StatusViewer.__init__(self, parent, delay=5., interval=5.)
125 def _draw(self):
126 lines = self._parent._render('log')
127 if lines:
128 logger.info(
129 'Progress:\n%s' % '\n'.join(' '+line for line in lines))
132class TerminalStatusViewer(StatusViewer):
133 def __init__(self, parent):
134 StatusViewer.__init__(self, parent)
135 self._terminal_size = get_terminal_size()
136 self._height = 0
137 self._state = 0
138 self._nlines_max = 0
139 self._isatty = sys.stdout.isatty()
141 def cleanup(self):
142 if self._state == 1:
143 sx, sy = self._terminal_size
144 self._reset()
145 self._flush()
147 self._state = 2
149 def _draw(self):
150 lines = self._parent._render('terminal')
151 if self._state == 0:
152 self._state = 1
154 if self._state != 1:
155 return
157 self._terminal_size = get_terminal_size()
158 sx, sy = self._terminal_size
159 nlines = len(lines)
160 if self._nlines_max < nlines:
161 self._nlines_max = nlines
162 self._resize(self._nlines_max)
164 self._start_show()
166 for i in range(self._nlines_max - nlines):
167 lines.append('')
169 for iline, line in enumerate(reversed(lines)):
170 if len(line) > sx - 1:
171 line = line[:sx-1]
173 self._print(ansi_clear_right + line)
174 if iline != self._nlines_max - 1:
175 self._print(ansi_next_line)
177 self._end_show()
178 self._flush()
180 def _print(self, s):
181 if self._isatty:
182 print(s, end='', file=sys.stderr)
184 def _flush(self):
185 if self._isatty:
186 print('', end='', flush=True, file=sys.stderr)
188 def _reset(self):
189 sx, sy = self._terminal_size
190 self._print(ansi_window % (1, sy))
191 self._print(ansi_move_to % (sy-self._height, 1))
192 self._print(ansi_clear_down)
193 self._height = 0
195 def _resize(self, height):
196 sx, sy = self._terminal_size
197 k = height - self._height
198 if k > 0:
199 self._print(ansi_scroll_up * k)
200 self._print(ansi_window % (1, sy-height))
201 if k < 0:
202 self._print(ansi_window % (1, sy-height))
203 self._print(ansi_scroll_down * abs(k))
205 self._height = height
207 def _start_show(self):
208 sx, sy = self._terminal_size
209 self._print(ansi_move_to % (sy-self._height+1, 1))
211 def _end_show(self):
212 sx, sy = self._terminal_size
213 self._print(ansi_move_to % (sy-self._height, 1))
214 self._print(ansi_clear_right)
217class Task(object):
218 def __init__(
219 self, progress, id, name, n, state='working', logger=None,
220 group=None, spinner=g_spinners[0]):
222 self._id = id
223 self._name = name
224 self._condition = ''
225 self._ispin0 = 0
226 self._ispin0_last = 0
227 self._ispin = 0
228 self._spinner = spinner
229 self._i = None
230 self._n = n
231 self._done = False
232 assert state in ('waiting', 'working')
233 self._state = state
234 self._progress = progress
235 self._logger = logger
236 self._tcreate = time.time()
237 self._group = group
238 self._lock = threading.RLock()
239 self.update(0)
241 def __enter__(self):
242 return self
244 def __exit__(self, type, value, tb):
245 if type is None:
246 self.done()
247 else:
248 self.fail()
250 def __call__(self, it):
251 try:
252 self._n = len(it)
253 except TypeError:
254 self._n = None
256 clean = False
257 try:
258 n = 0
259 for obj in it:
260 self.update(n)
261 yield obj
262 n += 1
264 self.update(n)
265 clean = True
267 finally:
268 if clean:
269 self.done()
270 else:
271 self.fail()
273 def task(self, *args, **kwargs):
274 kwargs['group'] = self
275 return self._progress.task(*args, **kwargs)
277 def update(self, i=None, condition=''):
278 with self._lock:
279 self._ispin0 += 1
280 self._state = 'working'
281 self._condition = condition
282 if i is not None:
283 if self._n is not None:
284 i = min(i, self._n)
286 self._i = i
288 self._progress._update(False)
290 def done(self, condition=''):
291 with self._lock:
292 self.duration = time.time() - self._tcreate
294 if self._state in ('done', 'failed'):
295 return
297 self._condition = condition
298 self._state = 'done'
299 self._log(str(self))
300 self._progress._task_end(self)
302 def fail(self, condition=''):
303 with self._lock:
304 self.duration = time.time() - self._tcreate
306 if self._state in ('done', 'failed'):
307 return
309 self._condition = condition
310 self._state = 'failed'
311 self._log(str(self))
312 self._progress._task_end(self)
314 def _log(self, s):
315 if self._logger is not None:
316 self._logger.debug(s)
318 def _get_group_time_start(self):
319 if self._group:
320 return self._group._get_group_time_start()
321 else:
322 return self._tcreate
324 def _str_state(self):
325 s = self._state
326 if s == 'waiting':
327 return ' '
328 elif s == 'working':
329 if self._ispin0_last != self._ispin0:
330 self._ispin += 1
331 self._ispin0_last = self._ispin0
333 return self._spinner[self._ispin % len(self._spinner)] + ' '
334 elif s == 'done':
335 return symbol_done + ' '
336 elif s == 'failed':
337 return symbol_failed + ' '
338 else:
339 return '? '
341 def _idisplay(self):
342 i = self._i
343 if self._n is not None and i > self._n:
344 i = self._n
345 return i
347 def _str_progress(self):
348 if self._i is None:
349 return self._state
350 elif self._n is None:
351 if self._state != 'working':
352 return '... %s (%i)' % (self._state, self._idisplay())
353 else:
354 return '%i' % self._idisplay()
355 else:
356 if self._state == 'working':
357 nw = len(str(self._n))
358 return (('%' + str(nw) + 'i / %i') % (
359 self._idisplay(), self._n)).center(11)
361 elif self._state == 'failed':
362 return '... %s (%i / %i)' % (
363 self._state, self._idisplay(), self._n)
364 else:
365 return '... %s (%i)' % (self._state, self._n)
367 def _str_percent(self):
368 if self._state == 'working' and self._n is not None and self._n >= 4 \
369 and self._i is not None:
370 return '%3.0f%%' % ((100. * self._i) / self._n)
371 else:
372 return ''
374 def _str_condition(self):
375 if self._condition:
376 return '%s' % self._condition
377 else:
378 return ''
380 def _str_bar(self):
381 if self._state == 'working' and self._n is not None and self._n >= 4 \
382 and self._i is not None:
383 nb = 20
384 fb = nb * float(self._i) / self._n
385 ib = int(fb)
386 ip = int((fb - ib) * (len(blocks)-1))
387 if ib == 0 and ip == 0:
388 ip = 1 # indication of start
389 s = blocks[0] * ib
390 if ib < nb:
391 s += blocks[-1-ip] + (nb - ib - 1) * blocks[-1] + blocks[-2]
393 # s = ' ' + bar[0] + bar[1] * ib + bar[2] * (nb - ib) + bar[3]
394 return s
395 else:
396 return ''
398 def _render(self, style):
399 if style == 'terminal':
400 return '%s%-40s %-11s %s%-4s %s' % (
401 self._str_state(),
402 self._name,
403 self._str_progress(),
404 self._str_bar(),
405 self._str_percent(),
406 self._str_condition())
408 elif style == 'log':
409 return '%s: %-40s %s%-4s %s' % (
410 self._state,
411 self._name,
412 self._str_progress(),
413 self._str_percent(),
414 self._str_condition())
415 else:
416 return ''
418 def __str__(self):
419 return '%s%-40s %s%-4s %s' % (
420 self._str_state(),
421 self._name,
422 self._str_progress(),
423 self._str_percent(),
424 self._str_condition())
427class Progress(object):
429 def __init__(self):
430 self._current_id = 0
431 self._tasks = {}
432 self._viewers = []
433 self._lock = threading.RLock()
434 self._isatty = sys.stdout.isatty()
436 def view(self, viewer=None):
437 if g_force_viewer_off or self._viewers:
438 viewer = 'off'
439 elif viewer is None:
440 viewer = g_viewer
442 if not self._isatty and viewer == 'terminal':
443 logger.debug('No tty attached, switching to log progress viewer.')
444 viewer = 'log'
446 try:
447 viewer = g_viewer_classes[viewer](self)
448 except KeyError:
449 raise ValueError('Invalid viewer choice: %s' % viewer)
451 self._viewers.append(viewer)
452 return viewer
454 def _remove_viewer(self, viewer):
455 self._update(True)
456 self._viewers.remove(viewer)
458 def task(self, name, n=None, logger=None, group=None):
459 with self._lock:
460 self._current_id += 1
461 task = Task(
462 self, self._current_id, name, n, logger=logger, group=group)
463 self._tasks[task._id] = task
464 self._update(True)
465 return task
467 def _task_end(self, task):
468 with self._lock:
469 self._update(True)
470 del self._tasks[task._id]
471 self._update(True)
473 def _update(self, force):
474 with self._lock:
475 for viewer in self._viewers:
476 viewer.update(force)
478 def _render(self, style):
479 task_ids = sorted(self._tasks)
480 lines = []
481 for task_id in task_ids:
482 task = self._tasks[task_id]
483 lines.extend(task._render(style).splitlines())
485 return lines
487 def _debug_log(self):
488 logger.debug(
489 'Viewers: %s\n Tasks active: %i\n Tasks total: %i',
490 ', '.join(viewer.__class__.__name__ for viewer in self._viewers)
491 if self._viewers else 'none',
492 len(self._tasks),
493 self._current_id)
496g_viewer_classes = {
497 'terminal': TerminalStatusViewer,
498 'log': LogStatusViewer,
499 'off': DummyStatusViewer}
501g_progress = Progress()
502progress = g_progress # compatibility
503view = g_progress.view
504task = g_progress.task