图片解析应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

310 lines
11 KiB

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