comparison env/lib/python3.9/site-packages/humanfriendly/terminal/spinners.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
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