Mercurial > repos > guerler > hhblits
comparison lib/python3.8/site-packages/pip/_internal/utils/ui.py @ 1:64071f2a4cf0 draft default tip
Deleted selected files
author | guerler |
---|---|
date | Mon, 27 Jul 2020 03:55:49 -0400 |
parents | 9e54283cc701 |
children |
comparison
equal
deleted
inserted
replaced
0:9e54283cc701 | 1:64071f2a4cf0 |
---|---|
1 # The following comment should be removed at some point in the future. | |
2 # mypy: strict-optional=False | |
3 # mypy: disallow-untyped-defs=False | |
4 | |
5 from __future__ import absolute_import, division | |
6 | |
7 import contextlib | |
8 import itertools | |
9 import logging | |
10 import sys | |
11 import time | |
12 from signal import SIGINT, default_int_handler, signal | |
13 | |
14 from pip._vendor import six | |
15 from pip._vendor.progress import HIDE_CURSOR, SHOW_CURSOR | |
16 from pip._vendor.progress.bar import Bar, FillingCirclesBar, IncrementalBar | |
17 from pip._vendor.progress.spinner import Spinner | |
18 | |
19 from pip._internal.utils.compat import WINDOWS | |
20 from pip._internal.utils.logging import get_indentation | |
21 from pip._internal.utils.misc import format_size | |
22 from pip._internal.utils.typing import MYPY_CHECK_RUNNING | |
23 | |
24 if MYPY_CHECK_RUNNING: | |
25 from typing import Any, Iterator, IO | |
26 | |
27 try: | |
28 from pip._vendor import colorama | |
29 # Lots of different errors can come from this, including SystemError and | |
30 # ImportError. | |
31 except Exception: | |
32 colorama = None | |
33 | |
34 logger = logging.getLogger(__name__) | |
35 | |
36 | |
37 def _select_progress_class(preferred, fallback): | |
38 encoding = getattr(preferred.file, "encoding", None) | |
39 | |
40 # If we don't know what encoding this file is in, then we'll just assume | |
41 # that it doesn't support unicode and use the ASCII bar. | |
42 if not encoding: | |
43 return fallback | |
44 | |
45 # Collect all of the possible characters we want to use with the preferred | |
46 # bar. | |
47 characters = [ | |
48 getattr(preferred, "empty_fill", six.text_type()), | |
49 getattr(preferred, "fill", six.text_type()), | |
50 ] | |
51 characters += list(getattr(preferred, "phases", [])) | |
52 | |
53 # Try to decode the characters we're using for the bar using the encoding | |
54 # of the given file, if this works then we'll assume that we can use the | |
55 # fancier bar and if not we'll fall back to the plaintext bar. | |
56 try: | |
57 six.text_type().join(characters).encode(encoding) | |
58 except UnicodeEncodeError: | |
59 return fallback | |
60 else: | |
61 return preferred | |
62 | |
63 | |
64 _BaseBar = _select_progress_class(IncrementalBar, Bar) # type: Any | |
65 | |
66 | |
67 class InterruptibleMixin(object): | |
68 """ | |
69 Helper to ensure that self.finish() gets called on keyboard interrupt. | |
70 | |
71 This allows downloads to be interrupted without leaving temporary state | |
72 (like hidden cursors) behind. | |
73 | |
74 This class is similar to the progress library's existing SigIntMixin | |
75 helper, but as of version 1.2, that helper has the following problems: | |
76 | |
77 1. It calls sys.exit(). | |
78 2. It discards the existing SIGINT handler completely. | |
79 3. It leaves its own handler in place even after an uninterrupted finish, | |
80 which will have unexpected delayed effects if the user triggers an | |
81 unrelated keyboard interrupt some time after a progress-displaying | |
82 download has already completed, for example. | |
83 """ | |
84 | |
85 def __init__(self, *args, **kwargs): | |
86 """ | |
87 Save the original SIGINT handler for later. | |
88 """ | |
89 super(InterruptibleMixin, self).__init__(*args, **kwargs) | |
90 | |
91 self.original_handler = signal(SIGINT, self.handle_sigint) | |
92 | |
93 # If signal() returns None, the previous handler was not installed from | |
94 # Python, and we cannot restore it. This probably should not happen, | |
95 # but if it does, we must restore something sensible instead, at least. | |
96 # The least bad option should be Python's default SIGINT handler, which | |
97 # just raises KeyboardInterrupt. | |
98 if self.original_handler is None: | |
99 self.original_handler = default_int_handler | |
100 | |
101 def finish(self): | |
102 """ | |
103 Restore the original SIGINT handler after finishing. | |
104 | |
105 This should happen regardless of whether the progress display finishes | |
106 normally, or gets interrupted. | |
107 """ | |
108 super(InterruptibleMixin, self).finish() | |
109 signal(SIGINT, self.original_handler) | |
110 | |
111 def handle_sigint(self, signum, frame): | |
112 """ | |
113 Call self.finish() before delegating to the original SIGINT handler. | |
114 | |
115 This handler should only be in place while the progress display is | |
116 active. | |
117 """ | |
118 self.finish() | |
119 self.original_handler(signum, frame) | |
120 | |
121 | |
122 class SilentBar(Bar): | |
123 | |
124 def update(self): | |
125 pass | |
126 | |
127 | |
128 class BlueEmojiBar(IncrementalBar): | |
129 | |
130 suffix = "%(percent)d%%" | |
131 bar_prefix = " " | |
132 bar_suffix = " " | |
133 phases = (u"\U0001F539", u"\U0001F537", u"\U0001F535") # type: Any | |
134 | |
135 | |
136 class DownloadProgressMixin(object): | |
137 | |
138 def __init__(self, *args, **kwargs): | |
139 super(DownloadProgressMixin, self).__init__(*args, **kwargs) | |
140 self.message = (" " * (get_indentation() + 2)) + self.message | |
141 | |
142 @property | |
143 def downloaded(self): | |
144 return format_size(self.index) | |
145 | |
146 @property | |
147 def download_speed(self): | |
148 # Avoid zero division errors... | |
149 if self.avg == 0.0: | |
150 return "..." | |
151 return format_size(1 / self.avg) + "/s" | |
152 | |
153 @property | |
154 def pretty_eta(self): | |
155 if self.eta: | |
156 return "eta %s" % self.eta_td | |
157 return "" | |
158 | |
159 def iter(self, it): | |
160 for x in it: | |
161 yield x | |
162 self.next(len(x)) | |
163 self.finish() | |
164 | |
165 | |
166 class WindowsMixin(object): | |
167 | |
168 def __init__(self, *args, **kwargs): | |
169 # The Windows terminal does not support the hide/show cursor ANSI codes | |
170 # even with colorama. So we'll ensure that hide_cursor is False on | |
171 # Windows. | |
172 # This call needs to go before the super() call, so that hide_cursor | |
173 # is set in time. The base progress bar class writes the "hide cursor" | |
174 # code to the terminal in its init, so if we don't set this soon | |
175 # enough, we get a "hide" with no corresponding "show"... | |
176 if WINDOWS and self.hide_cursor: | |
177 self.hide_cursor = False | |
178 | |
179 super(WindowsMixin, self).__init__(*args, **kwargs) | |
180 | |
181 # Check if we are running on Windows and we have the colorama module, | |
182 # if we do then wrap our file with it. | |
183 if WINDOWS and colorama: | |
184 self.file = colorama.AnsiToWin32(self.file) | |
185 # The progress code expects to be able to call self.file.isatty() | |
186 # but the colorama.AnsiToWin32() object doesn't have that, so we'll | |
187 # add it. | |
188 self.file.isatty = lambda: self.file.wrapped.isatty() | |
189 # The progress code expects to be able to call self.file.flush() | |
190 # but the colorama.AnsiToWin32() object doesn't have that, so we'll | |
191 # add it. | |
192 self.file.flush = lambda: self.file.wrapped.flush() | |
193 | |
194 | |
195 class BaseDownloadProgressBar(WindowsMixin, InterruptibleMixin, | |
196 DownloadProgressMixin): | |
197 | |
198 file = sys.stdout | |
199 message = "%(percent)d%%" | |
200 suffix = "%(downloaded)s %(download_speed)s %(pretty_eta)s" | |
201 | |
202 # NOTE: The "type: ignore" comments on the following classes are there to | |
203 # work around https://github.com/python/typing/issues/241 | |
204 | |
205 | |
206 class DefaultDownloadProgressBar(BaseDownloadProgressBar, | |
207 _BaseBar): | |
208 pass | |
209 | |
210 | |
211 class DownloadSilentBar(BaseDownloadProgressBar, SilentBar): # type: ignore | |
212 pass | |
213 | |
214 | |
215 class DownloadBar(BaseDownloadProgressBar, # type: ignore | |
216 Bar): | |
217 pass | |
218 | |
219 | |
220 class DownloadFillingCirclesBar(BaseDownloadProgressBar, # type: ignore | |
221 FillingCirclesBar): | |
222 pass | |
223 | |
224 | |
225 class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, # type: ignore | |
226 BlueEmojiBar): | |
227 pass | |
228 | |
229 | |
230 class DownloadProgressSpinner(WindowsMixin, InterruptibleMixin, | |
231 DownloadProgressMixin, Spinner): | |
232 | |
233 file = sys.stdout | |
234 suffix = "%(downloaded)s %(download_speed)s" | |
235 | |
236 def next_phase(self): | |
237 if not hasattr(self, "_phaser"): | |
238 self._phaser = itertools.cycle(self.phases) | |
239 return next(self._phaser) | |
240 | |
241 def update(self): | |
242 message = self.message % self | |
243 phase = self.next_phase() | |
244 suffix = self.suffix % self | |
245 line = ''.join([ | |
246 message, | |
247 " " if message else "", | |
248 phase, | |
249 " " if suffix else "", | |
250 suffix, | |
251 ]) | |
252 | |
253 self.writeln(line) | |
254 | |
255 | |
256 BAR_TYPES = { | |
257 "off": (DownloadSilentBar, DownloadSilentBar), | |
258 "on": (DefaultDownloadProgressBar, DownloadProgressSpinner), | |
259 "ascii": (DownloadBar, DownloadProgressSpinner), | |
260 "pretty": (DownloadFillingCirclesBar, DownloadProgressSpinner), | |
261 "emoji": (DownloadBlueEmojiProgressBar, DownloadProgressSpinner) | |
262 } | |
263 | |
264 | |
265 def DownloadProgressProvider(progress_bar, max=None): | |
266 if max is None or max == 0: | |
267 return BAR_TYPES[progress_bar][1]().iter | |
268 else: | |
269 return BAR_TYPES[progress_bar][0](max=max).iter | |
270 | |
271 | |
272 ################################################################ | |
273 # Generic "something is happening" spinners | |
274 # | |
275 # We don't even try using progress.spinner.Spinner here because it's actually | |
276 # simpler to reimplement from scratch than to coerce their code into doing | |
277 # what we need. | |
278 ################################################################ | |
279 | |
280 @contextlib.contextmanager | |
281 def hidden_cursor(file): | |
282 # type: (IO[Any]) -> Iterator[None] | |
283 # The Windows terminal does not support the hide/show cursor ANSI codes, | |
284 # even via colorama. So don't even try. | |
285 if WINDOWS: | |
286 yield | |
287 # We don't want to clutter the output with control characters if we're | |
288 # writing to a file, or if the user is running with --quiet. | |
289 # See https://github.com/pypa/pip/issues/3418 | |
290 elif not file.isatty() or logger.getEffectiveLevel() > logging.INFO: | |
291 yield | |
292 else: | |
293 file.write(HIDE_CURSOR) | |
294 try: | |
295 yield | |
296 finally: | |
297 file.write(SHOW_CURSOR) | |
298 | |
299 | |
300 class RateLimiter(object): | |
301 def __init__(self, min_update_interval_seconds): | |
302 # type: (float) -> None | |
303 self._min_update_interval_seconds = min_update_interval_seconds | |
304 self._last_update = 0 # type: float | |
305 | |
306 def ready(self): | |
307 # type: () -> bool | |
308 now = time.time() | |
309 delta = now - self._last_update | |
310 return delta >= self._min_update_interval_seconds | |
311 | |
312 def reset(self): | |
313 # type: () -> None | |
314 self._last_update = time.time() | |
315 | |
316 | |
317 class SpinnerInterface(object): | |
318 def spin(self): | |
319 # type: () -> None | |
320 raise NotImplementedError() | |
321 | |
322 def finish(self, final_status): | |
323 # type: (str) -> None | |
324 raise NotImplementedError() | |
325 | |
326 | |
327 class InteractiveSpinner(SpinnerInterface): | |
328 def __init__(self, message, file=None, spin_chars="-\\|/", | |
329 # Empirically, 8 updates/second looks nice | |
330 min_update_interval_seconds=0.125): | |
331 self._message = message | |
332 if file is None: | |
333 file = sys.stdout | |
334 self._file = file | |
335 self._rate_limiter = RateLimiter(min_update_interval_seconds) | |
336 self._finished = False | |
337 | |
338 self._spin_cycle = itertools.cycle(spin_chars) | |
339 | |
340 self._file.write(" " * get_indentation() + self._message + " ... ") | |
341 self._width = 0 | |
342 | |
343 def _write(self, status): | |
344 assert not self._finished | |
345 # Erase what we wrote before by backspacing to the beginning, writing | |
346 # spaces to overwrite the old text, and then backspacing again | |
347 backup = "\b" * self._width | |
348 self._file.write(backup + " " * self._width + backup) | |
349 # Now we have a blank slate to add our status | |
350 self._file.write(status) | |
351 self._width = len(status) | |
352 self._file.flush() | |
353 self._rate_limiter.reset() | |
354 | |
355 def spin(self): | |
356 # type: () -> None | |
357 if self._finished: | |
358 return | |
359 if not self._rate_limiter.ready(): | |
360 return | |
361 self._write(next(self._spin_cycle)) | |
362 | |
363 def finish(self, final_status): | |
364 # type: (str) -> None | |
365 if self._finished: | |
366 return | |
367 self._write(final_status) | |
368 self._file.write("\n") | |
369 self._file.flush() | |
370 self._finished = True | |
371 | |
372 | |
373 # Used for dumb terminals, non-interactive installs (no tty), etc. | |
374 # We still print updates occasionally (once every 60 seconds by default) to | |
375 # act as a keep-alive for systems like Travis-CI that take lack-of-output as | |
376 # an indication that a task has frozen. | |
377 class NonInteractiveSpinner(SpinnerInterface): | |
378 def __init__(self, message, min_update_interval_seconds=60): | |
379 # type: (str, float) -> None | |
380 self._message = message | |
381 self._finished = False | |
382 self._rate_limiter = RateLimiter(min_update_interval_seconds) | |
383 self._update("started") | |
384 | |
385 def _update(self, status): | |
386 assert not self._finished | |
387 self._rate_limiter.reset() | |
388 logger.info("%s: %s", self._message, status) | |
389 | |
390 def spin(self): | |
391 # type: () -> None | |
392 if self._finished: | |
393 return | |
394 if not self._rate_limiter.ready(): | |
395 return | |
396 self._update("still running...") | |
397 | |
398 def finish(self, final_status): | |
399 # type: (str) -> None | |
400 if self._finished: | |
401 return | |
402 self._update("finished with status '%s'" % (final_status,)) | |
403 self._finished = True | |
404 | |
405 | |
406 @contextlib.contextmanager | |
407 def open_spinner(message): | |
408 # type: (str) -> Iterator[SpinnerInterface] | |
409 # Interactive spinner goes directly to sys.stdout rather than being routed | |
410 # through the logging system, but it acts like it has level INFO, | |
411 # i.e. it's only displayed if we're at level INFO or better. | |
412 # Non-interactive spinner goes through the logging system, so it is always | |
413 # in sync with logging configuration. | |
414 if sys.stdout.isatty() and logger.getEffectiveLevel() <= logging.INFO: | |
415 spinner = InteractiveSpinner(message) # type: SpinnerInterface | |
416 else: | |
417 spinner = NonInteractiveSpinner(message) | |
418 try: | |
419 with hidden_cursor(sys.stdout): | |
420 yield spinner | |
421 except KeyboardInterrupt: | |
422 spinner.finish("canceled") | |
423 raise | |
424 except Exception: | |
425 spinner.finish("error") | |
426 raise | |
427 else: | |
428 spinner.finish("done") |