Issues (161)

src/egon/data/subprocess.py (2 issues)

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
The variable output does not seem to be defined.
Loading history...
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
The variable error does not seem to be defined.
Loading history...
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