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