3
3
import configparser
4
4
import re
5
5
from itertools import chain , groupby , repeat
6
- from typing import TYPE_CHECKING , Dict , Iterator , List , Optional , Union
6
+ from typing import TYPE_CHECKING , Dict , List , Optional , Union
7
7
8
8
from pip ._vendor .pkg_resources import Distribution
9
9
from pip ._vendor .requests .models import Request , Response
10
+ from pip ._vendor .rich .console import Console , ConsoleOptions , RenderResult
11
+ from pip ._vendor .rich .text import Text
10
12
11
13
if TYPE_CHECKING :
12
14
from hashlib import _Hash
@@ -23,75 +25,146 @@ def _is_kebab_case(s: str) -> bool:
23
25
return re .match (r"^[a-z]+(-[a-z]+)*$" , s ) is not None
24
26
25
27
26
- def _prefix_with_indent (prefix : str , s : str , indent : Optional [str ] = None ) -> str :
27
- if indent is None :
28
- indent = " " * len (prefix )
28
+ def _prefix_with_indent (
29
+ s : Union [Text , str ],
30
+ console : Console ,
31
+ * ,
32
+ width_offset : int = 0 ,
33
+ prefix : str ,
34
+ indent : str ,
35
+ ) -> Text :
36
+ if isinstance (s , Text ):
37
+ text = s
29
38
else :
30
- assert len (indent ) == len (prefix )
31
- message = s .replace ("\n " , "\n " + indent )
32
- return f"{ prefix } { message } \n "
39
+ text = console .render_str (s )
40
+
41
+ lines = text .wrap (console , console .width - width_offset )
42
+
43
+ return console .render_str (prefix ) + console .render_str (f"\n { indent } " ).join (lines )
33
44
34
45
35
46
class PipError (Exception ):
36
47
"""The base pip error."""
37
48
38
49
39
50
class DiagnosticPipError (PipError ):
40
- """A pip error, that presents diagnostic information to the user.
51
+ """An error, that presents diagnostic information to the user.
41
52
42
53
This contains a bunch of logic, to enable pretty presentation of our error
43
54
messages. Each error gets a unique reference. Each error can also include
44
55
additional context, a hint and/or a note -- which are presented with the
45
56
main error message in a consistent style.
57
+
58
+ This is adapted from the error output styling in `sphinx-theme-builder`.
46
59
"""
47
60
48
61
reference : str
49
62
50
63
def __init__ (
51
64
self ,
52
65
* ,
53
- message : str ,
54
- context : Optional [str ],
55
- hint_stmt : Optional [str ],
56
- attention_stmt : Optional [str ] = None ,
57
- reference : Optional [str ] = None ,
58
66
kind : 'Literal["error", "warning"]' = "error" ,
67
+ reference : Optional [str ] = None ,
68
+ message : Union [str , Text ],
69
+ context : Optional [Union [str , Text ]],
70
+ hint_stmt : Optional [Union [str , Text ]],
71
+ attention_stmt : Optional [Union [str , Text ]] = None ,
72
+ link : Optional [str ] = None ,
59
73
) -> None :
60
-
61
74
# Ensure a proper reference is provided.
62
75
if reference is None :
63
76
assert hasattr (self , "reference" ), "error reference not provided!"
64
77
reference = self .reference
65
78
assert _is_kebab_case (reference ), "error reference must be kebab-case!"
66
79
67
- super ().__init__ (f"{ reference } : { message } " )
68
-
69
80
self .kind = kind
81
+ self .reference = reference
82
+
70
83
self .message = message
71
84
self .context = context
72
85
73
- self .reference = reference
74
86
self .attention_stmt = attention_stmt
75
87
self .hint_stmt = hint_stmt
76
88
77
- def __str__ (self ) -> str :
78
- return "" .join (self ._string_parts ())
89
+ self .link = link
79
90
80
- def _string_parts (self ) -> Iterator [str ]:
81
- # Present the main message, with relevant context indented.
82
- yield f"{ self .message } \n "
83
- if self .context is not None :
84
- yield f"\n { self .context } \n "
91
+ super ().__init__ (f"<{ self .__class__ .__name__ } : { self .reference } >" )
92
+
93
+ def __repr__ (self ) -> str :
94
+ return (
95
+ f"<{ self .__class__ .__name__ } ("
96
+ f"reference={ self .reference !r} , "
97
+ f"message={ self .message !r} , "
98
+ f"context={ self .context !r} , "
99
+ f"attention_stmt={ self .attention_stmt !r} , "
100
+ f"hint_stmt={ self .hint_stmt !r} "
101
+ ")>"
102
+ )
103
+
104
+ def __rich_console__ (
105
+ self ,
106
+ console : Console ,
107
+ options : ConsoleOptions ,
108
+ ) -> RenderResult :
109
+ colour = "red" if self .kind == "error" else "yellow"
110
+
111
+ yield f"[{ colour } bold]{ self .kind } [/]: [bold]{ self .reference } [/]"
112
+ yield ""
113
+
114
+ if not options .ascii_only :
115
+ # Present the main message, with relevant context indented.
116
+ if self .context is not None :
117
+ yield _prefix_with_indent (
118
+ self .message ,
119
+ console ,
120
+ width_offset = 2 ,
121
+ prefix = f"[{ colour } ]×[/] " ,
122
+ indent = f"[{ colour } ]│[/] " ,
123
+ )
124
+ yield _prefix_with_indent (
125
+ self .context ,
126
+ console ,
127
+ width_offset = 4 ,
128
+ prefix = f"[{ colour } ]╰─>[/] " ,
129
+ indent = f"[{ colour } ] [/] " ,
130
+ )
131
+ else :
132
+ yield _prefix_with_indent (
133
+ self .message ,
134
+ console ,
135
+ width_offset = 4 ,
136
+ prefix = "[red]×[/] " ,
137
+ indent = " " ,
138
+ )
139
+ else :
140
+ yield self .message
141
+ if self .context is not None :
142
+ yield ""
143
+ yield self .context
85
144
86
- # Space out the note/hint messages.
87
145
if self .attention_stmt is not None or self .hint_stmt is not None :
88
- yield "\n "
146
+ yield ""
89
147
90
148
if self .attention_stmt is not None :
91
- yield _prefix_with_indent ("Note: " , self .attention_stmt )
92
-
149
+ yield _prefix_with_indent (
150
+ self .attention_stmt ,
151
+ console ,
152
+ width_offset = 6 ,
153
+ prefix = "[magenta bold]note[/]: " ,
154
+ indent = " " ,
155
+ )
93
156
if self .hint_stmt is not None :
94
- yield _prefix_with_indent ("Hint: " , self .hint_stmt )
157
+ yield _prefix_with_indent (
158
+ self .hint_stmt ,
159
+ console ,
160
+ width_offset = 6 ,
161
+ prefix = "[cyan bold]hint[/]: " ,
162
+ indent = " " ,
163
+ )
164
+
165
+ if self .link is not None :
166
+ yield ""
167
+ yield f"Link: { self .link } "
95
168
96
169
97
170
#
@@ -119,12 +192,12 @@ def __init__(self, *, package: str) -> None:
119
192
message = f"Can not process { package } " ,
120
193
context = (
121
194
"This package has an invalid pyproject.toml file.\n "
122
- "The [build-system] table is missing the mandatory `requires` key."
195
+ R "The \ [build-system] table is missing the mandatory `requires` key."
123
196
),
124
197
attention_stmt = (
125
198
"This is an issue with the package mentioned above, not pip."
126
199
),
127
- hint_stmt = "See PEP 518 for the detailed specification." ,
200
+ hint_stmt = Text ( "See PEP 518 for the detailed specification." ) ,
128
201
)
129
202
130
203
@@ -136,12 +209,12 @@ class InvalidPyProjectBuildRequires(DiagnosticPipError):
136
209
def __init__ (self , * , package : str , reason : str ) -> None :
137
210
super ().__init__ (
138
211
message = f"Can not process { package } " ,
139
- context = (
212
+ context = Text (
140
213
"This package has an invalid `build-system.requires` key in "
141
214
"pyproject.toml.\n "
142
215
f"{ reason } "
143
216
),
144
- hint_stmt = "See PEP 518 for the detailed specification." ,
217
+ hint_stmt = Text ( "See PEP 518 for the detailed specification." ) ,
145
218
attention_stmt = (
146
219
"This is an issue with the package mentioned above, not pip."
147
220
),
0 commit comments