exceptions.py 12 KB

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