1from __future__ import annotations
2
3import collections.abc as cabc
4import typing as t
5from gettext import gettext as _
6from gettext import ngettext
7
8from ._compat import get_text_stderr
9from .globals import resolve_color_default
10from .utils import echo
11from .utils import format_filename
12
13if t.TYPE_CHECKING:
14 from .core import Command
15 from .core import Context
16 from .core import Parameter
17
18
19def _join_param_hints(param_hint: cabc.Sequence[str] | str | None) -> str | None:
20 if param_hint is not None and not isinstance(param_hint, str):
21 return " / ".join(repr(x) for x in param_hint)
22
23 return param_hint
24
25
26class ClickException(Exception):
27 """An exception that Click can handle and show to the user."""
28
29 #: The exit code for this exception.
30 exit_code = 1
31
32 def __init__(self, message: str) -> None:
33 super().__init__(message)
34 # The context will be removed by the time we print the message, so cache
35 # the color settings here to be used later on (in `show`)
36 self.show_color: bool | None = resolve_color_default()
37 self.message = message
38
39 def format_message(self) -> str:
40 return self.message
41
42 def __str__(self) -> str:
43 return self.message
44
45 def show(self, file: t.IO[t.Any] | None = None) -> None:
46 if file is None:
47 file = get_text_stderr()
48
49 echo(
50 _("Error: {message}").format(message=self.format_message()),
51 file=file,
52 color=self.show_color,
53 )
54
55
56class UsageError(ClickException):
57 """An internal exception that signals a usage error. This typically
58 aborts any further handling.
59
60 :param message: the error message to display.
61 :param ctx: optionally the context that caused this error. Click will
62 fill in the context automatically in some situations.
63 """
64
65 exit_code = 2
66
67 def __init__(self, message: str, ctx: Context | None = None) -> None:
68 super().__init__(message)
69 self.ctx = ctx
70 self.cmd: Command | None = self.ctx.command if self.ctx else None
71
72 def show(self, file: t.IO[t.Any] | None = None) -> None:
73 if file is None:
74 file = get_text_stderr()
75 color = None
76 hint = ""
77 if (
78 self.ctx is not None
79 and self.ctx.command.get_help_option(self.ctx) is not None
80 ):
81 hint = _("Try '{command} {option}' for help.").format(
82 command=self.ctx.command_path, option=self.ctx.help_option_names[0]
83 )
84 hint = f"{hint}\n"
85 if self.ctx is not None:
86 color = self.ctx.color
87 echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color)
88 echo(
89 _("Error: {message}").format(message=self.format_message()),
90 file=file,
91 color=color,
92 )
93
94
95class BadParameter(UsageError):
96 """An exception that formats out a standardized error message for a
97 bad parameter. This is useful when thrown from a callback or type as
98 Click will attach contextual information to it (for instance, which
99 parameter it is).
100
101 .. versionadded:: 2.0
102
103 :param param: the parameter object that caused this error. This can
104 be left out, and Click will attach this info itself
105 if possible.
106 :param param_hint: a string that shows up as parameter name. This
107 can be used as alternative to `param` in cases
108 where custom validation should happen. If it is
109 a string it's used as such, if it's a list then
110 each item is quoted and separated.
111 """
112
113 def __init__(
114 self,
115 message: str,
116 ctx: Context | None = None,
117 param: Parameter | None = None,
118 param_hint: str | None = None,
119 ) -> None:
120 super().__init__(message, ctx)
121 self.param = param
122 self.param_hint = param_hint
123
124 def format_message(self) -> str:
125 if self.param_hint is not None:
126 param_hint = self.param_hint
127 elif self.param is not None:
128 param_hint = self.param.get_error_hint(self.ctx) # type: ignore
129 else:
130 return _("Invalid value: {message}").format(message=self.message)
131
132 return _("Invalid value for {param_hint}: {message}").format(
133 param_hint=_join_param_hints(param_hint), message=self.message
134 )
135
136
137class MissingParameter(BadParameter):
138 """Raised if click required an option or argument but it was not
139 provided when invoking the script.
140
141 .. versionadded:: 4.0
142
143 :param param_type: a string that indicates the type of the parameter.
144 The default is to inherit the parameter type from
145 the given `param`. Valid values are ``'parameter'``,
146 ``'option'`` or ``'argument'``.
147 """
148
149 def __init__(
150 self,
151 message: str | None = None,
152 ctx: Context | None = None,
153 param: Parameter | None = None,
154 param_hint: str | None = None,
155 param_type: str | None = None,
156 ) -> None:
157 super().__init__(message or "", ctx, param, param_hint)
158 self.param_type = param_type
159
160 def format_message(self) -> str:
161 if self.param_hint is not None:
162 param_hint: str | None = self.param_hint
163 elif self.param is not None:
164 param_hint = self.param.get_error_hint(self.ctx) # type: ignore
165 else:
166 param_hint = None
167
168 param_hint = _join_param_hints(param_hint)
169 param_hint = f" {param_hint}" if param_hint else ""
170
171 param_type = self.param_type
172 if param_type is None and self.param is not None:
173 param_type = self.param.param_type_name
174
175 msg = self.message
176 if self.param is not None:
177 msg_extra = self.param.type.get_missing_message(
178 param=self.param, ctx=self.ctx
179 )
180 if msg_extra:
181 if msg:
182 msg += f". {msg_extra}"
183 else:
184 msg = msg_extra
185
186 msg = f" {msg}" if msg else ""
187
188 # Translate param_type for known types.
189 if param_type == "argument":
190 missing = _("Missing argument")
191 elif param_type == "option":
192 missing = _("Missing option")
193 elif param_type == "parameter":
194 missing = _("Missing parameter")
195 else:
196 missing = _("Missing {param_type}").format(param_type=param_type)
197
198 return f"{missing}{param_hint}.{msg}"
199
200 def __str__(self) -> str:
201 if not self.message:
202 param_name = self.param.name if self.param else None
203 return _("Missing parameter: {param_name}").format(param_name=param_name)
204 else:
205 return self.message
206
207
208class NoSuchOption(UsageError):
209 """Raised if click attempted to handle an option that does not
210 exist.
211
212 .. versionadded:: 4.0
213 """
214
215 def __init__(
216 self,
217 option_name: str,
218 message: str | None = None,
219 possibilities: cabc.Sequence[str] | None = None,
220 ctx: Context | None = None,
221 ) -> None:
222 if message is None:
223 message = _("No such option: {name}").format(name=option_name)
224
225 super().__init__(message, ctx)
226 self.option_name = option_name
227 self.possibilities = possibilities
228
229 def format_message(self) -> str:
230 if not self.possibilities:
231 return self.message
232
233 possibility_str = ", ".join(sorted(self.possibilities))
234 suggest = ngettext(
235 "Did you mean {possibility}?",
236 "(Possible options: {possibilities})",
237 len(self.possibilities),
238 ).format(possibility=possibility_str, possibilities=possibility_str)
239 return f"{self.message} {suggest}"
240
241
242class BadOptionUsage(UsageError):
243 """Raised if an option is generally supplied but the use of the option
244 was incorrect. This is for instance raised if the number of arguments
245 for an option is not correct.
246
247 .. versionadded:: 4.0
248
249 :param option_name: the name of the option being used incorrectly.
250 """
251
252 def __init__(
253 self, option_name: str, message: str, ctx: Context | None = None
254 ) -> None:
255 super().__init__(message, ctx)
256 self.option_name = option_name
257
258
259class BadArgumentUsage(UsageError):
260 """Raised if an argument is generally supplied but the use of the argument
261 was incorrect. This is for instance raised if the number of values
262 for an argument is not correct.
263
264 .. versionadded:: 6.0
265 """
266
267
268class NoArgsIsHelpError(UsageError):
269 def __init__(self, ctx: Context) -> None:
270 self.ctx: Context
271 super().__init__(ctx.get_help(), ctx=ctx)
272
273 def show(self, file: t.IO[t.Any] | None = None) -> None:
274 echo(self.format_message(), file=file, err=True, color=self.ctx.color)
275
276
277class FileError(ClickException):
278 """Raised if a file cannot be opened."""
279
280 def __init__(self, filename: str, hint: str | None = None) -> None:
281 if hint is None:
282 hint = _("unknown error")
283
284 super().__init__(hint)
285 self.ui_filename: str = format_filename(filename)
286 self.filename = filename
287
288 def format_message(self) -> str:
289 return _("Could not open file {filename!r}: {message}").format(
290 filename=self.ui_filename, message=self.message
291 )
292
293
294class Abort(RuntimeError):
295 """An internal signalling exception that signals Click to abort."""
296
297
298class Exit(RuntimeError):
299 """An exception that indicates that the application should exit with some
300 status code.
301
302 :param code: the status code to exit with.
303 """
304
305 __slots__ = ("exit_code",)
306
307 def __init__(self, code: int = 0) -> None:
308 self.exit_code: int = code