1"""Base pip exception classes."""
2
3from __future__ import annotations
4
5import re
6from typing import TYPE_CHECKING, Literal
7
8from pip._vendor.rich.text import Text
9
10if TYPE_CHECKING:
11 from pip._vendor.rich.console import Console, ConsoleOptions, RenderResult
12
13
14def _is_kebab_case(s: str) -> bool:
15 return re.match(r"^[a-z]+(-[a-z]+)*$", s) is not None
16
17
18def _prefix_with_indent(
19 s: Text | str,
20 console: Console,
21 *,
22 prefix: str,
23 indent: str,
24) -> Text:
25 if isinstance(s, Text):
26 text = s
27 else:
28 text = console.render_str(s)
29
30 return console.render_str(prefix, overflow="ignore") + console.render_str(
31 f"\n{indent}", overflow="ignore"
32 ).join(text.split(allow_blank=True))
33
34
35class PipError(Exception):
36 """The base pip error."""
37
38
39class InstallationError(PipError):
40 """General exception during installation"""
41
42
43class DiagnosticPipError(PipError):
44 """An error, that presents diagnostic information to the user.
45
46 This contains a bunch of logic, to enable pretty presentation of our error
47 messages. Each error gets a unique reference. Each error can also include
48 additional context, a hint and/or a note -- which are presented with the
49 main error message in a consistent style.
50
51 This is adapted from the error output styling in `sphinx-theme-builder`.
52 """
53
54 reference: str
55
56 def __init__(
57 self,
58 *,
59 kind: Literal["error", "warning"] = "error",
60 reference: str | None = None,
61 message: str | Text,
62 context: str | Text | None,
63 hint_stmt: str | Text | None,
64 note_stmt: str | Text | None = None,
65 link: str | None = None,
66 ) -> None:
67 # Ensure a proper reference is provided.
68 if reference is None:
69 assert hasattr(self, "reference"), "error reference not provided!"
70 reference = self.reference
71 assert _is_kebab_case(reference), "error reference must be kebab-case!"
72
73 self.kind = kind
74 self.reference = reference
75
76 self.message = message
77 self.context = context
78
79 self.note_stmt = note_stmt
80 self.hint_stmt = hint_stmt
81
82 self.link = link
83
84 super().__init__(f"<{self.__class__.__name__}: {self.reference}>")
85
86 def __repr__(self) -> str:
87 return (
88 f"<{self.__class__.__name__}("
89 f"reference={self.reference!r}, "
90 f"message={self.message!r}, "
91 f"context={self.context!r}, "
92 f"note_stmt={self.note_stmt!r}, "
93 f"hint_stmt={self.hint_stmt!r}"
94 ")>"
95 )
96
97 def __rich_console__(
98 self,
99 console: Console,
100 options: ConsoleOptions,
101 ) -> RenderResult:
102 colour = "red" if self.kind == "error" else "yellow"
103
104 yield f"[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]"
105 yield ""
106
107 if not options.ascii_only:
108 # Present the main message, with relevant context indented.
109 if self.context is not None:
110 yield _prefix_with_indent(
111 self.message,
112 console,
113 prefix=f"[{colour}]×[/] ",
114 indent=f"[{colour}]│[/] ",
115 )
116 yield _prefix_with_indent(
117 self.context,
118 console,
119 prefix=f"[{colour}]╰─>[/] ",
120 indent=f"[{colour}] [/] ",
121 )
122 else:
123 yield _prefix_with_indent(
124 self.message,
125 console,
126 prefix="[red]×[/] ",
127 indent=" ",
128 )
129 else:
130 yield self.message
131 if self.context is not None:
132 yield ""
133 yield self.context
134
135 if self.note_stmt is not None or self.hint_stmt is not None:
136 yield ""
137
138 if self.note_stmt is not None:
139 yield _prefix_with_indent(
140 self.note_stmt,
141 console,
142 prefix="[magenta bold]note[/]: ",
143 indent=" ",
144 )
145 if self.hint_stmt is not None:
146 yield _prefix_with_indent(
147 self.hint_stmt,
148 console,
149 prefix="[cyan bold]hint[/]: ",
150 indent=" ",
151 )
152
153 if self.link is not None:
154 yield ""
155 yield f"Link: {self.link}"