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.

ui.py 6.6 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. from __future__ import absolute_import
  2. from __future__ import division
  3. import itertools
  4. import sys
  5. from signal import signal, SIGINT, default_int_handler
  6. from pip.compat import WINDOWS
  7. from pip.utils import format_size
  8. from pip.utils.logging import get_indentation
  9. from pip._vendor import six
  10. from pip._vendor.progress.bar import Bar, IncrementalBar
  11. from pip._vendor.progress.helpers import WritelnMixin
  12. from pip._vendor.progress.spinner import Spinner
  13. try:
  14. from pip._vendor import colorama
  15. # Lots of different errors can come from this, including SystemError and
  16. # ImportError.
  17. except Exception:
  18. colorama = None
  19. def _select_progress_class(preferred, fallback):
  20. encoding = getattr(preferred.file, "encoding", None)
  21. # If we don't know what encoding this file is in, then we'll just assume
  22. # that it doesn't support unicode and use the ASCII bar.
  23. if not encoding:
  24. return fallback
  25. # Collect all of the possible characters we want to use with the preferred
  26. # bar.
  27. characters = [
  28. getattr(preferred, "empty_fill", six.text_type()),
  29. getattr(preferred, "fill", six.text_type()),
  30. ]
  31. characters += list(getattr(preferred, "phases", []))
  32. # Try to decode the characters we're using for the bar using the encoding
  33. # of the given file, if this works then we'll assume that we can use the
  34. # fancier bar and if not we'll fall back to the plaintext bar.
  35. try:
  36. six.text_type().join(characters).encode(encoding)
  37. except UnicodeEncodeError:
  38. return fallback
  39. else:
  40. return preferred
  41. _BaseBar = _select_progress_class(IncrementalBar, Bar)
  42. class InterruptibleMixin(object):
  43. """
  44. Helper to ensure that self.finish() gets called on keyboard interrupt.
  45. This allows downloads to be interrupted without leaving temporary state
  46. (like hidden cursors) behind.
  47. This class is similar to the progress library's existing SigIntMixin
  48. helper, but as of version 1.2, that helper has the following problems:
  49. 1. It calls sys.exit().
  50. 2. It discards the existing SIGINT handler completely.
  51. 3. It leaves its own handler in place even after an uninterrupted finish,
  52. which will have unexpected delayed effects if the user triggers an
  53. unrelated keyboard interrupt some time after a progress-displaying
  54. download has already completed, for example.
  55. """
  56. def __init__(self, *args, **kwargs):
  57. """
  58. Save the original SIGINT handler for later.
  59. """
  60. super(InterruptibleMixin, self).__init__(*args, **kwargs)
  61. self.original_handler = signal(SIGINT, self.handle_sigint)
  62. # If signal() returns None, the previous handler was not installed from
  63. # Python, and we cannot restore it. This probably should not happen,
  64. # but if it does, we must restore something sensible instead, at least.
  65. # The least bad option should be Python's default SIGINT handler, which
  66. # just raises KeyboardInterrupt.
  67. if self.original_handler is None:
  68. self.original_handler = default_int_handler
  69. def finish(self):
  70. """
  71. Restore the original SIGINT handler after finishing.
  72. This should happen regardless of whether the progress display finishes
  73. normally, or gets interrupted.
  74. """
  75. super(InterruptibleMixin, self).finish()
  76. signal(SIGINT, self.original_handler)
  77. def handle_sigint(self, signum, frame):
  78. """
  79. Call self.finish() before delegating to the original SIGINT handler.
  80. This handler should only be in place while the progress display is
  81. active.
  82. """
  83. self.finish()
  84. self.original_handler(signum, frame)
  85. class DownloadProgressMixin(object):
  86. def __init__(self, *args, **kwargs):
  87. super(DownloadProgressMixin, self).__init__(*args, **kwargs)
  88. self.message = (" " * (get_indentation() + 2)) + self.message
  89. @property
  90. def downloaded(self):
  91. return format_size(self.index)
  92. @property
  93. def download_speed(self):
  94. # Avoid zero division errors...
  95. if self.avg == 0.0:
  96. return "..."
  97. return format_size(1 / self.avg) + "/s"
  98. @property
  99. def pretty_eta(self):
  100. if self.eta:
  101. return "eta %s" % self.eta_td
  102. return ""
  103. def iter(self, it, n=1):
  104. for x in it:
  105. yield x
  106. self.next(n)
  107. self.finish()
  108. class WindowsMixin(object):
  109. def __init__(self, *args, **kwargs):
  110. # The Windows terminal does not support the hide/show cursor ANSI codes
  111. # even with colorama. So we'll ensure that hide_cursor is False on
  112. # Windows.
  113. # This call neds to go before the super() call, so that hide_cursor
  114. # is set in time. The base progress bar class writes the "hide cursor"
  115. # code to the terminal in its init, so if we don't set this soon
  116. # enough, we get a "hide" with no corresponding "show"...
  117. if WINDOWS and self.hide_cursor:
  118. self.hide_cursor = False
  119. super(WindowsMixin, self).__init__(*args, **kwargs)
  120. # Check if we are running on Windows and we have the colorama module,
  121. # if we do then wrap our file with it.
  122. if WINDOWS and colorama:
  123. self.file = colorama.AnsiToWin32(self.file)
  124. # The progress code expects to be able to call self.file.isatty()
  125. # but the colorama.AnsiToWin32() object doesn't have that, so we'll
  126. # add it.
  127. self.file.isatty = lambda: self.file.wrapped.isatty()
  128. # The progress code expects to be able to call self.file.flush()
  129. # but the colorama.AnsiToWin32() object doesn't have that, so we'll
  130. # add it.
  131. self.file.flush = lambda: self.file.wrapped.flush()
  132. class DownloadProgressBar(WindowsMixin, InterruptibleMixin,
  133. DownloadProgressMixin, _BaseBar):
  134. file = sys.stdout
  135. message = "%(percent)d%%"
  136. suffix = "%(downloaded)s %(download_speed)s %(pretty_eta)s"
  137. class DownloadProgressSpinner(WindowsMixin, InterruptibleMixin,
  138. DownloadProgressMixin, WritelnMixin, Spinner):
  139. file = sys.stdout
  140. suffix = "%(downloaded)s %(download_speed)s"
  141. def next_phase(self):
  142. if not hasattr(self, "_phaser"):
  143. self._phaser = itertools.cycle(self.phases)
  144. return next(self._phaser)
  145. def update(self):
  146. message = self.message % self
  147. phase = self.next_phase()
  148. suffix = self.suffix % self
  149. line = ''.join([
  150. message,
  151. " " if message else "",
  152. phase,
  153. " " if suffix else "",
  154. suffix,
  155. ])
  156. self.writeln(line)