1 | """Exensions to Python's :py:mod:`subprocess` module. |
||
2 | |||
3 | More specifically, this module provides a customized version of |
||
4 | :py:func:`subprocess.run`, which always sets `check=True`, |
||
5 | `capture_output=True`, enhances the raised exceptions string representation |
||
6 | with additional output information and makes it slightly more readable when |
||
7 | encountered in a stack trace. |
||
8 | """ |
||
9 | |||
10 | from textwrap import indent, wrap |
||
11 | import itertools |
||
12 | import subprocess |
||
13 | |||
14 | |||
15 | class CalledProcessError(subprocess.CalledProcessError): |
||
16 | """A more verbose version of :py:class:`subprocess.CalledProcessError`. |
||
17 | |||
18 | Replaces the standard string representation of a |
||
19 | :py:class:`subprocess.CalledProcessError` with one that has more output and |
||
20 | error information and is formatted to be more readable in a stack trace. |
||
21 | """ |
||
22 | |||
23 | def __str__(self): |
||
24 | errors = self.stderr.split("\n") |
||
25 | outputs = self.stdout.split("\n") |
||
26 | |||
27 | lines = itertools.chain( |
||
28 | wrap(f"{super().__str__()}"), |
||
29 | ["Output:"], |
||
30 | *( |
||
31 | wrap(output, initial_indent=" ", subsequent_indent=" ") |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
32 | for output in outputs |
||
33 | ), |
||
34 | ["Errors:"], |
||
35 | *( |
||
36 | wrap(error, initial_indent=" ", subsequent_indent=" ") |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
37 | for error in errors |
||
38 | ), |
||
39 | ) |
||
40 | lines = indent("\n".join(lines), "| ") |
||
41 | return f"\n{lines}" |
||
42 | |||
43 | |||
44 | def run(*args, **kwargs): |
||
45 | """A "safer" version of :py:func:`subprocess.run`. |
||
46 | |||
47 | "Safer" in this context means that this version always raises |
||
48 | :py:class:`CalledProcessError` if the process in question returns a |
||
49 | non-zero exit status. This is done by setting `check=True` and |
||
50 | `capture_output=True`, so you don't have to specify these yourself anymore. |
||
51 | You can though, if you want to override these defaults. |
||
52 | Other than that, the function accepts the same parameters as |
||
53 | :py:func:`subprocess.run`. |
||
54 | """ |
||
55 | for default in ["capture_output", "check", "text"]: |
||
56 | kwargs[default] = kwargs.get(default, True) |
||
57 | try: |
||
58 | result = subprocess.run(*args, **kwargs) |
||
59 | except subprocess.CalledProcessError as cpe: |
||
60 | raise CalledProcessError( |
||
61 | cpe.returncode, cpe.cmd, output=cpe.output, stderr=cpe.stderr |
||
62 | ) from None |
||
63 | return result |
||
64 |