Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/humanfriendly/terminal/spinners.py @ 1:56ad4e20f292 draft
"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
| author | guerler |
|---|---|
| date | Fri, 31 Jul 2020 00:32:28 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 0:d30785e31577 | 1:56ad4e20f292 |
|---|---|
| 1 # Human friendly input/output in Python. | |
| 2 # | |
| 3 # Author: Peter Odding <peter@peterodding.com> | |
| 4 # Last Change: March 1, 2020 | |
| 5 # URL: https://humanfriendly.readthedocs.io | |
| 6 | |
| 7 """ | |
| 8 Support for spinners that represent progress on interactive terminals. | |
| 9 | |
| 10 The :class:`Spinner` class shows a "spinner" on the terminal to let the user | |
| 11 know that something is happening during long running operations that would | |
| 12 otherwise be silent (leaving the user to wonder what they're waiting for). | |
| 13 Below are some visual examples that should illustrate the point. | |
| 14 | |
| 15 **Simple spinners:** | |
| 16 | |
| 17 Here's a screen capture that shows the simplest form of spinner: | |
| 18 | |
| 19 .. image:: images/spinner-basic.gif | |
| 20 :alt: Animated screen capture of a simple spinner. | |
| 21 | |
| 22 The following code was used to create the spinner above: | |
| 23 | |
| 24 .. code-block:: python | |
| 25 | |
| 26 import itertools | |
| 27 import time | |
| 28 from humanfriendly import Spinner | |
| 29 | |
| 30 with Spinner(label="Downloading") as spinner: | |
| 31 for i in itertools.count(): | |
| 32 # Do something useful here. | |
| 33 time.sleep(0.1) | |
| 34 # Advance the spinner. | |
| 35 spinner.step() | |
| 36 | |
| 37 **Spinners that show elapsed time:** | |
| 38 | |
| 39 Here's a spinner that shows the elapsed time since it started: | |
| 40 | |
| 41 .. image:: images/spinner-with-timer.gif | |
| 42 :alt: Animated screen capture of a spinner showing elapsed time. | |
| 43 | |
| 44 The following code was used to create the spinner above: | |
| 45 | |
| 46 .. code-block:: python | |
| 47 | |
| 48 import itertools | |
| 49 import time | |
| 50 from humanfriendly import Spinner, Timer | |
| 51 | |
| 52 with Spinner(label="Downloading", timer=Timer()) as spinner: | |
| 53 for i in itertools.count(): | |
| 54 # Do something useful here. | |
| 55 time.sleep(0.1) | |
| 56 # Advance the spinner. | |
| 57 spinner.step() | |
| 58 | |
| 59 **Spinners that show progress:** | |
| 60 | |
| 61 Here's a spinner that shows a progress percentage: | |
| 62 | |
| 63 .. image:: images/spinner-with-progress.gif | |
| 64 :alt: Animated screen capture of spinner showing progress. | |
| 65 | |
| 66 The following code was used to create the spinner above: | |
| 67 | |
| 68 .. code-block:: python | |
| 69 | |
| 70 import itertools | |
| 71 import random | |
| 72 import time | |
| 73 from humanfriendly import Spinner, Timer | |
| 74 | |
| 75 with Spinner(label="Downloading", total=100) as spinner: | |
| 76 progress = 0 | |
| 77 while progress < 100: | |
| 78 # Do something useful here. | |
| 79 time.sleep(0.1) | |
| 80 # Advance the spinner. | |
| 81 spinner.step(progress) | |
| 82 # Determine the new progress value. | |
| 83 progress += random.random() * 5 | |
| 84 | |
| 85 If you want to provide user feedback during a long running operation but it's | |
| 86 not practical to periodically call the :func:`~Spinner.step()` method consider | |
| 87 using :class:`AutomaticSpinner` instead. | |
| 88 | |
| 89 As you may already have noticed in the examples above, :class:`Spinner` objects | |
| 90 can be used as context managers to automatically call :func:`Spinner.clear()` | |
| 91 when the spinner ends. | |
| 92 """ | |
| 93 | |
| 94 # Standard library modules. | |
| 95 import multiprocessing | |
| 96 import sys | |
| 97 import time | |
| 98 | |
| 99 # Modules included in our package. | |
| 100 from humanfriendly import Timer | |
| 101 from humanfriendly.deprecation import deprecated_args | |
| 102 from humanfriendly.terminal import ANSI_ERASE_LINE | |
| 103 | |
| 104 # Public identifiers that require documentation. | |
| 105 __all__ = ("AutomaticSpinner", "GLYPHS", "MINIMUM_INTERVAL", "Spinner") | |
| 106 | |
| 107 GLYPHS = ["-", "\\", "|", "/"] | |
| 108 """A list of strings with characters that together form a crude animation :-).""" | |
| 109 | |
| 110 MINIMUM_INTERVAL = 0.2 | |
| 111 """Spinners are redrawn with a frequency no higher than this number (a floating point number of seconds).""" | |
| 112 | |
| 113 | |
| 114 class Spinner(object): | |
| 115 | |
| 116 """Show a spinner on the terminal as a simple means of feedback to the user.""" | |
| 117 | |
| 118 @deprecated_args('label', 'total', 'stream', 'interactive', 'timer') | |
| 119 def __init__(self, **options): | |
| 120 """ | |
| 121 Initialize a :class:`Spinner` object. | |
| 122 | |
| 123 :param label: | |
| 124 | |
| 125 The label for the spinner (a string or :data:`None`, defaults to | |
| 126 :data:`None`). | |
| 127 | |
| 128 :param total: | |
| 129 | |
| 130 The expected number of steps (an integer or :data:`None`). If this is | |
| 131 provided the spinner will show a progress percentage. | |
| 132 | |
| 133 :param stream: | |
| 134 | |
| 135 The output stream to show the spinner on (a file-like object, | |
| 136 defaults to :data:`sys.stderr`). | |
| 137 | |
| 138 :param interactive: | |
| 139 | |
| 140 :data:`True` to enable rendering of the spinner, :data:`False` to | |
| 141 disable (defaults to the result of ``stream.isatty()``). | |
| 142 | |
| 143 :param timer: | |
| 144 | |
| 145 A :class:`.Timer` object (optional). If this is given the spinner | |
| 146 will show the elapsed time according to the timer. | |
| 147 | |
| 148 :param interval: | |
| 149 | |
| 150 The spinner will be updated at most once every this many seconds | |
| 151 (a floating point number, defaults to :data:`MINIMUM_INTERVAL`). | |
| 152 | |
| 153 :param glyphs: | |
| 154 | |
| 155 A list of strings with single characters that are drawn in the same | |
| 156 place in succession to implement a simple animated effect (defaults | |
| 157 to :data:`GLYPHS`). | |
| 158 """ | |
| 159 # Store initializer arguments. | |
| 160 self.interactive = options.get('interactive') | |
| 161 self.interval = options.get('interval', MINIMUM_INTERVAL) | |
| 162 self.label = options.get('label') | |
| 163 self.states = options.get('glyphs', GLYPHS) | |
| 164 self.stream = options.get('stream', sys.stderr) | |
| 165 self.timer = options.get('timer') | |
| 166 self.total = options.get('total') | |
| 167 # Define instance variables. | |
| 168 self.counter = 0 | |
| 169 self.last_update = 0 | |
| 170 # Try to automatically discover whether the stream is connected to | |
| 171 # a terminal, but don't fail if no isatty() method is available. | |
| 172 if self.interactive is None: | |
| 173 try: | |
| 174 self.interactive = self.stream.isatty() | |
| 175 except Exception: | |
| 176 self.interactive = False | |
| 177 | |
| 178 def step(self, progress=0, label=None): | |
| 179 """ | |
| 180 Advance the spinner by one step and redraw it. | |
| 181 | |
| 182 :param progress: The number of the current step, relative to the total | |
| 183 given to the :class:`Spinner` constructor (an integer, | |
| 184 optional). If not provided the spinner will not show | |
| 185 progress. | |
| 186 :param label: The label to use while redrawing (a string, optional). If | |
| 187 not provided the label given to the :class:`Spinner` | |
| 188 constructor is used instead. | |
| 189 | |
| 190 This method advances the spinner by one step without starting a new | |
| 191 line, causing an animated effect which is very simple but much nicer | |
| 192 than waiting for a prompt which is completely silent for a long time. | |
| 193 | |
| 194 .. note:: This method uses time based rate limiting to avoid redrawing | |
| 195 the spinner too frequently. If you know you're dealing with | |
| 196 code that will call :func:`step()` at a high frequency, | |
| 197 consider using :func:`sleep()` to avoid creating the | |
| 198 equivalent of a busy loop that's rate limiting the spinner | |
| 199 99% of the time. | |
| 200 """ | |
| 201 if self.interactive: | |
| 202 time_now = time.time() | |
| 203 if time_now - self.last_update >= self.interval: | |
| 204 self.last_update = time_now | |
| 205 state = self.states[self.counter % len(self.states)] | |
| 206 label = label or self.label | |
| 207 if not label: | |
| 208 raise Exception("No label set for spinner!") | |
| 209 elif self.total and progress: | |
| 210 label = "%s: %.2f%%" % (label, progress / (self.total / 100.0)) | |
| 211 elif self.timer and self.timer.elapsed_time > 2: | |
| 212 label = "%s (%s)" % (label, self.timer.rounded) | |
| 213 self.stream.write("%s %s %s ..\r" % (ANSI_ERASE_LINE, state, label)) | |
| 214 self.counter += 1 | |
| 215 | |
| 216 def sleep(self): | |
| 217 """ | |
| 218 Sleep for a short period before redrawing the spinner. | |
| 219 | |
| 220 This method is useful when you know you're dealing with code that will | |
| 221 call :func:`step()` at a high frequency. It will sleep for the interval | |
| 222 with which the spinner is redrawn (less than a second). This avoids | |
| 223 creating the equivalent of a busy loop that's rate limiting the | |
| 224 spinner 99% of the time. | |
| 225 | |
| 226 This method doesn't redraw the spinner, you still have to call | |
| 227 :func:`step()` in order to do that. | |
| 228 """ | |
| 229 time.sleep(MINIMUM_INTERVAL) | |
| 230 | |
| 231 def clear(self): | |
| 232 """ | |
| 233 Clear the spinner. | |
| 234 | |
| 235 The next line which is shown on the standard output or error stream | |
| 236 after calling this method will overwrite the line that used to show the | |
| 237 spinner. | |
| 238 """ | |
| 239 if self.interactive: | |
| 240 self.stream.write(ANSI_ERASE_LINE) | |
| 241 | |
| 242 def __enter__(self): | |
| 243 """ | |
| 244 Enable the use of spinners as context managers. | |
| 245 | |
| 246 :returns: The :class:`Spinner` object. | |
| 247 """ | |
| 248 return self | |
| 249 | |
| 250 def __exit__(self, exc_type=None, exc_value=None, traceback=None): | |
| 251 """Clear the spinner when leaving the context.""" | |
| 252 self.clear() | |
| 253 | |
| 254 | |
| 255 class AutomaticSpinner(object): | |
| 256 | |
| 257 """ | |
| 258 Show a spinner on the terminal that automatically starts animating. | |
| 259 | |
| 260 This class shows a spinner on the terminal (just like :class:`Spinner` | |
| 261 does) that automatically starts animating. This class should be used as a | |
| 262 context manager using the :keyword:`with` statement. The animation | |
| 263 continues for as long as the context is active. | |
| 264 | |
| 265 :class:`AutomaticSpinner` provides an alternative to :class:`Spinner` | |
| 266 for situations where it is not practical for the caller to periodically | |
| 267 call :func:`~Spinner.step()` to advance the animation, e.g. because | |
| 268 you're performing a blocking call and don't fancy implementing threading or | |
| 269 subprocess handling just to provide some user feedback. | |
| 270 | |
| 271 This works using the :mod:`multiprocessing` module by spawning a | |
| 272 subprocess to render the spinner while the main process is busy doing | |
| 273 something more useful. By using the :keyword:`with` statement you're | |
| 274 guaranteed that the subprocess is properly terminated at the appropriate | |
| 275 time. | |
| 276 """ | |
| 277 | |
| 278 def __init__(self, label, show_time=True): | |
| 279 """ | |
| 280 Initialize an automatic spinner. | |
| 281 | |
| 282 :param label: The label for the spinner (a string). | |
| 283 :param show_time: If this is :data:`True` (the default) then the spinner | |
| 284 shows elapsed time. | |
| 285 """ | |
| 286 self.label = label | |
| 287 self.show_time = show_time | |
| 288 self.shutdown_event = multiprocessing.Event() | |
| 289 self.subprocess = multiprocessing.Process(target=self._target) | |
| 290 | |
| 291 def __enter__(self): | |
| 292 """Enable the use of automatic spinners as context managers.""" | |
| 293 self.subprocess.start() | |
| 294 | |
| 295 def __exit__(self, exc_type=None, exc_value=None, traceback=None): | |
| 296 """Enable the use of automatic spinners as context managers.""" | |
| 297 self.shutdown_event.set() | |
| 298 self.subprocess.join() | |
| 299 | |
| 300 def _target(self): | |
| 301 try: | |
| 302 timer = Timer() if self.show_time else None | |
| 303 with Spinner(label=self.label, timer=timer) as spinner: | |
| 304 while not self.shutdown_event.is_set(): | |
| 305 spinner.step() | |
| 306 spinner.sleep() | |
| 307 except KeyboardInterrupt: | |
| 308 # Swallow Control-C signals without producing a nasty traceback that | |
| 309 # won't make any sense to the average user. | |
| 310 pass |
