1"""
2Custom exception classes.
3
4These vary in use case from "we needed a specific data structure layout in
5exceptions used for message-passing" to simply "we needed to express an error
6condition in a way easily told apart from other, truly unexpected errors".
7"""
8
9from pprint import pformat
10from traceback import format_exception
11from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
12
13if TYPE_CHECKING:
14 from .parser import ParserContext
15 from .runners import Result
16 from .util import ExceptionWrapper
17
18
19class CollectionNotFound(Exception):
20 def __init__(self, name: str, start: str) -> None:
21 self.name = name
22 self.start = start
23
24
25class Failure(Exception):
26 """
27 Exception subclass representing failure of a command execution.
28
29 "Failure" may mean the command executed and the shell indicated an unusual
30 result (usually, a non-zero exit code), or it may mean something else, like
31 a ``sudo`` command which was aborted when the supplied password failed
32 authentication.
33
34 Two attributes allow introspection to determine the nature of the problem:
35
36 * ``result``: a `.Result` instance with info about the command being
37 executed and, if it ran to completion, how it exited.
38 * ``reason``: a wrapped exception instance if applicable (e.g. a
39 `.StreamWatcher` raised `WatcherError`) or ``None`` otherwise, in which
40 case, it's probably a `Failure` subclass indicating its own specific
41 nature, such as `UnexpectedExit` or `CommandTimedOut`.
42
43 This class is only rarely raised by itself; most of the time `.Runner.run`
44 (or a wrapper of same, such as `.Context.sudo`) will raise a specific
45 subclass like `UnexpectedExit` or `AuthFailure`.
46
47 .. versionadded:: 1.0
48 """
49
50 def __init__(
51 self, result: "Result", reason: Optional["WatcherError"] = None
52 ) -> None:
53 self.result = result
54 self.reason = reason
55
56 def streams_for_display(self) -> Tuple[str, str]:
57 """
58 Return stdout/err streams as necessary for error display.
59
60 Subject to the following rules:
61
62 - If a given stream was *not* hidden during execution, a placeholder is
63 used instead, to avoid printing it twice.
64 - Only the last 10 lines of stream text is included.
65 - PTY-driven execution will lack stderr, and a specific message to this
66 effect is returned instead of a stderr dump.
67
68 :returns: Two-tuple of stdout, stderr strings.
69
70 .. versionadded:: 1.3
71 """
72 already_printed = " already printed"
73 if "stdout" not in self.result.hide:
74 stdout = already_printed
75 else:
76 stdout = self.result.tail("stdout")
77 if self.result.pty:
78 stderr = " n/a (PTYs have no stderr)"
79 else:
80 if "stderr" not in self.result.hide:
81 stderr = already_printed
82 else:
83 stderr = self.result.tail("stderr")
84 return stdout, stderr
85
86 def __repr__(self) -> str:
87 return self._repr()
88
89 def _repr(self, **kwargs: Any) -> str:
90 """
91 Return ``__repr__``-like value from inner result + any kwargs.
92 """
93 # TODO: expand?
94 # TODO: truncate command?
95 template = "<{}: cmd={!r}{}>"
96 rest = ""
97 if kwargs:
98 rest = " " + " ".join(
99 "{}={}".format(key, value) for key, value in kwargs.items()
100 )
101 return template.format(
102 self.__class__.__name__, self.result.command, rest
103 )
104
105
106class UnexpectedExit(Failure):
107 """
108 A shell command ran to completion but exited with an unexpected exit code.
109
110 Its string representation displays the following:
111
112 - Command executed;
113 - Exit code;
114 - The last 10 lines of stdout, if it was hidden;
115 - The last 10 lines of stderr, if it was hidden and non-empty (e.g.
116 pty=False; when pty=True, stderr never happens.)
117
118 .. versionadded:: 1.0
119 """
120
121 def __str__(self) -> str:
122 stdout, stderr = self.streams_for_display()
123 command = self.result.command
124 exited = self.result.exited
125 template = """Encountered a bad command exit code!
126
127Command: {!r}
128
129Exit code: {}
130
131Stdout:{}
132
133Stderr:{}
134
135"""
136 return template.format(command, exited, stdout, stderr)
137
138 def _repr(self, **kwargs: Any) -> str:
139 kwargs.setdefault("exited", self.result.exited)
140 return super()._repr(**kwargs)
141
142
143class CommandTimedOut(Failure):
144 """
145 Raised when a subprocess did not exit within a desired timeframe.
146 """
147
148 def __init__(self, result: "Result", timeout: int) -> None:
149 super().__init__(result)
150 self.timeout = timeout
151
152 def __repr__(self) -> str:
153 return self._repr(timeout=self.timeout)
154
155 def __str__(self) -> str:
156 stdout, stderr = self.streams_for_display()
157 command = self.result.command
158 template = """Command did not complete within {} seconds!
159
160Command: {!r}
161
162Stdout:{}
163
164Stderr:{}
165
166"""
167 return template.format(self.timeout, command, stdout, stderr)
168
169
170class AuthFailure(Failure):
171 """
172 An authentication failure, e.g. due to an incorrect ``sudo`` password.
173
174 .. note::
175 `.Result` objects attached to these exceptions typically lack exit code
176 information, since the command was never fully executed - the exception
177 was raised instead.
178
179 .. versionadded:: 1.0
180 """
181
182 def __init__(self, result: "Result", prompt: str) -> None:
183 self.result = result
184 self.prompt = prompt
185
186 def __str__(self) -> str:
187 err = "The password submitted to prompt {!r} was rejected."
188 return err.format(self.prompt)
189
190
191class ParseError(Exception):
192 """
193 An error arising from the parsing of command-line flags/arguments.
194
195 Ambiguous input, invalid task names, invalid flags, etc.
196
197 .. versionadded:: 1.0
198 """
199
200 def __init__(
201 self, msg: str, context: Optional["ParserContext"] = None
202 ) -> None:
203 super().__init__(msg)
204 self.context = context
205
206
207class Exit(Exception):
208 """
209 Simple custom stand-in for SystemExit.
210
211 Replaces scattered sys.exit calls, improves testability, allows one to
212 catch an exit request without intercepting real SystemExits (typically an
213 unfriendly thing to do, as most users calling `sys.exit` rather expect it
214 to truly exit.)
215
216 Defaults to a non-printing, exit-0 friendly termination behavior if the
217 exception is uncaught.
218
219 If ``code`` (an int) given, that code is used to exit.
220
221 If ``message`` (a string) given, it is printed to standard error, and the
222 program exits with code ``1`` by default (unless overridden by also giving
223 ``code`` explicitly.)
224
225 .. versionadded:: 1.0
226 """
227
228 def __init__(
229 self, message: Optional[str] = None, code: Optional[int] = None
230 ) -> None:
231 self.message = message
232 self._code = code
233
234 @property
235 def code(self) -> int:
236 if self._code is not None:
237 return self._code
238 return 1 if self.message else 0
239
240
241class PlatformError(Exception):
242 """
243 Raised when an illegal operation occurs for the current platform.
244
245 E.g. Windows users trying to use functionality requiring the ``pty``
246 module.
247
248 Typically used to present a clearer error message to the user.
249
250 .. versionadded:: 1.0
251 """
252
253 pass
254
255
256class AmbiguousEnvVar(Exception):
257 """
258 Raised when loading env var config keys has an ambiguous target.
259
260 .. versionadded:: 1.0
261 """
262
263 pass
264
265
266class UncastableEnvVar(Exception):
267 """
268 Raised on attempted env var loads whose default values are too rich.
269
270 E.g. trying to stuff ``MY_VAR="foo"`` into ``{'my_var': ['uh', 'oh']}``
271 doesn't make any sense until/if we implement some sort of transform option.
272
273 .. versionadded:: 1.0
274 """
275
276 pass
277
278
279class UnknownFileType(Exception):
280 """
281 A config file of an unknown type was specified and cannot be loaded.
282
283 .. versionadded:: 1.0
284 """
285
286 pass
287
288
289class UnpicklableConfigMember(Exception):
290 """
291 A config file contained module objects, which can't be pickled/copied.
292
293 We raise this more easily catchable exception instead of letting the
294 (unclearly phrased) TypeError bubble out of the pickle module. (However, to
295 avoid our own fragile catching of that error, we head it off by explicitly
296 testing for module members.)
297
298 .. versionadded:: 1.0.2
299 """
300
301 pass
302
303
304def _printable_kwargs(kwargs: Any) -> Dict[str, Any]:
305 """
306 Return print-friendly version of a thread-related ``kwargs`` dict.
307
308 Extra care is taken with ``args`` members which are very long iterables -
309 those need truncating to be useful.
310 """
311 printable = {}
312 for key, value in kwargs.items():
313 item = value
314 if key == "args":
315 item = []
316 for arg in value:
317 new_arg = arg
318 if hasattr(arg, "__len__") and len(arg) > 10:
319 msg = "<... remainder truncated during error display ...>"
320 new_arg = arg[:10] + [msg]
321 item.append(new_arg)
322 printable[key] = item
323 return printable
324
325
326class ThreadException(Exception):
327 """
328 One or more exceptions were raised within background threads.
329
330 The real underlying exceptions are stored in the `exceptions` attribute;
331 see its documentation for data structure details.
332
333 .. note::
334 Threads which did not encounter an exception, do not contribute to this
335 exception object and thus are not present inside `exceptions`.
336
337 .. versionadded:: 1.0
338 """
339
340 #: A tuple of `ExceptionWrappers <invoke.util.ExceptionWrapper>` containing
341 #: the initial thread constructor kwargs (because `threading.Thread`
342 #: subclasses should always be called with kwargs) and the caught exception
343 #: for that thread as seen by `sys.exc_info` (so: type, value, traceback).
344 #:
345 #: .. note::
346 #: The ordering of this attribute is not well-defined.
347 #:
348 #: .. note::
349 #: Thread kwargs which appear to be very long (e.g. IO
350 #: buffers) will be truncated when printed, to avoid huge
351 #: unreadable error display.
352 exceptions: Tuple["ExceptionWrapper", ...] = tuple()
353
354 def __init__(self, exceptions: List["ExceptionWrapper"]) -> None:
355 self.exceptions = tuple(exceptions)
356
357 def __str__(self) -> str:
358 details = []
359 for x in self.exceptions:
360 # Build useful display
361 detail = "Thread args: {}\n\n{}"
362 details.append(
363 detail.format(
364 pformat(_printable_kwargs(x.kwargs)),
365 "\n".join(format_exception(x.type, x.value, x.traceback)),
366 )
367 )
368 args = (
369 len(self.exceptions),
370 ", ".join(x.type.__name__ for x in self.exceptions),
371 "\n\n".join(details),
372 )
373 return """
374Saw {} exceptions within threads ({}):
375
376
377{}
378""".format(
379 *args
380 )
381
382
383class WatcherError(Exception):
384 """
385 Generic parent exception class for `.StreamWatcher`-related errors.
386
387 Typically, one of these exceptions indicates a `.StreamWatcher` noticed
388 something anomalous in an output stream, such as an authentication response
389 failure.
390
391 `.Runner` catches these and attaches them to `.Failure` exceptions so they
392 can be referenced by intermediate code and/or act as extra info for end
393 users.
394
395 .. versionadded:: 1.0
396 """
397
398 pass
399
400
401class ResponseNotAccepted(WatcherError):
402 """
403 A responder/watcher class noticed a 'bad' response to its submission.
404
405 Mostly used by `.FailingResponder` and subclasses, e.g. "oh dear I
406 autosubmitted a sudo password and it was incorrect."
407
408 .. versionadded:: 1.0
409 """
410
411 pass
412
413
414class SubprocessPipeError(Exception):
415 """
416 Some problem was encountered handling subprocess pipes (stdout/err/in).
417
418 Typically only for corner cases; most of the time, errors in this area are
419 raised by the interpreter or the operating system, and end up wrapped in a
420 `.ThreadException`.
421
422 .. versionadded:: 1.3
423 """
424
425 pass