6
6
import sys
7
7
import time
8
8
from collections .abc import Generator
9
- from typing import IO
9
+ from typing import IO , Final
10
+
11
+ from pip ._vendor .rich .console import (
12
+ Console ,
13
+ ConsoleOptions ,
14
+ RenderableType ,
15
+ RenderResult ,
16
+ )
17
+ from pip ._vendor .rich .live import Live
18
+ from pip ._vendor .rich .measure import Measurement
19
+ from pip ._vendor .rich .text import Text
10
20
11
21
from pip ._internal .utils .compat import WINDOWS
12
- from pip ._internal .utils .logging import get_indentation
22
+ from pip ._internal .utils .logging import get_console , get_indentation
13
23
14
24
logger = logging .getLogger (__name__ )
15
25
26
+ SPINNER_CHARS : Final = r"-\|/"
27
+ SPINS_PER_SECOND : Final = 8
28
+
16
29
17
30
class SpinnerInterface :
18
31
def spin (self ) -> None :
@@ -27,9 +40,9 @@ def __init__(
27
40
self ,
28
41
message : str ,
29
42
file : IO [str ] | None = None ,
30
- spin_chars : str = "- \\ |/" ,
43
+ spin_chars : str = SPINNER_CHARS ,
31
44
# Empirically, 8 updates/second looks nice
32
- min_update_interval_seconds : float = 0.125 ,
45
+ min_update_interval_seconds : float = 1 / SPINS_PER_SECOND ,
33
46
):
34
47
self ._message = message
35
48
if file is None :
@@ -139,6 +152,66 @@ def open_spinner(message: str) -> Generator[SpinnerInterface, None, None]:
139
152
spinner .finish ("done" )
140
153
141
154
155
+ class _PipRichSpinner :
156
+ """
157
+ Custom rich spinner that matches the style of the legacy spinners.
158
+
159
+ (*) Updates will be handled in a background thread by a rich live panel
160
+ which will call render() automatically at the appropriate time.
161
+ """
162
+
163
+ def __init__ (self , label : str ) -> None :
164
+ self .label = label
165
+ self ._spin_cycle = itertools .cycle (SPINNER_CHARS )
166
+ self ._spinner_text = ""
167
+ self ._finished = False
168
+ self ._indent = get_indentation () * " "
169
+
170
+ def __rich_console__ (
171
+ self , console : Console , options : ConsoleOptions
172
+ ) -> RenderResult :
173
+ yield self .render ()
174
+
175
+ def __rich_measure__ (
176
+ self , console : Console , options : ConsoleOptions
177
+ ) -> Measurement :
178
+ text = self .render ()
179
+ return Measurement .get (console , options , text )
180
+
181
+ def render (self ) -> RenderableType :
182
+ if not self ._finished :
183
+ self ._spinner_text = next (self ._spin_cycle )
184
+
185
+ return Text .assemble (self ._indent , self .label , " ... " , self ._spinner_text )
186
+
187
+ def finish (self , status : str ) -> None :
188
+ """Stop spinning and set a final status message."""
189
+ self ._spinner_text = status
190
+ self ._finished = True
191
+
192
+
193
+ @contextlib .contextmanager
194
+ def open_rich_spinner (label : str , console : Console | None = None ) -> Generator [None ]:
195
+ if not logger .isEnabledFor (logging .INFO ):
196
+ # Don't show spinner if --quiet is given.
197
+ yield
198
+ return
199
+
200
+ console = console or get_console ()
201
+ spinner = _PipRichSpinner (label )
202
+ with Live (spinner , refresh_per_second = SPINS_PER_SECOND , console = console ):
203
+ try :
204
+ yield
205
+ except KeyboardInterrupt :
206
+ spinner .finish ("canceled" )
207
+ raise
208
+ except Exception :
209
+ spinner .finish ("error" )
210
+ raise
211
+ else :
212
+ spinner .finish ("done" )
213
+
214
+
142
215
HIDE_CURSOR = "\x1b [?25l"
143
216
SHOW_CURSOR = "\x1b [?25h"
144
217
0 commit comments