Passed
Push — dev ( d34095...d5729b )
by Stephan
01:00 queued 10s
created

data.subprocess   A

Complexity

Total Complexity 3

Size/Duplication

Total Lines 64
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 30
dl 0
loc 64
rs 10
c 0
b 0
f 0
wmc 3

1 Function

Rating   Name   Duplication   Size   Complexity  
A run() 0 19 2

1 Method

Rating   Name   Duplication   Size   Complexity  
A CalledProcessError.__str__() 0 19 1
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 locale import getpreferredencoding
11
from textwrap import indent, wrap
12
import itertools
13
import subprocess
14
15
16
class CalledProcessError(subprocess.CalledProcessError):
17
    """A more verbose version of :py:class:`subprocess.CalledProcessError`.
18
19
    Replaces the standard string representation of a
20
    :py:class:`subprocess.CalledProcessError` with one that has more output and
21
    error information and is formatted to be more readable in a stack trace.
22
    """
23
24
    def __str__(self):
25
        errors = self.stderr.decode(getpreferredencoding()).split("\n")
26
        outputs = self.stdout.decode(getpreferredencoding()).split("\n")
27
28
        lines = itertools.chain(
29
            wrap(f"{super().__str__()}"),
30
            ["Output:"],
31
            *(
32
                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...
33
                for output in outputs
34
            ),
35
            ["Errors:"],
36
            *(
37
                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...
38
                for error in errors
39
            ),
40
        )
41
        lines = indent("\n".join(lines), "| ")
42
        return f"\n{lines}"
43
44
45
def run(*args, **kwargs):
46
    """A "safer" version of :py:func:`subprocess.run`.
47
48
    "Safer" in this context means that this version always raises
49
    :py:class:`CalledProcessError` if the process in question returns a
50
    non-zero exit status. This is done by setting `check=True` and
51
    `capture_output=True`, so you don't have to specify these yourself anymore.
52
    You can though, if you want to override these defaults.
53
    Other than that, the function accepts the same parameters as
54
    :py:func:`subprocess.run`.
55
    """
56
    kwargs["check"] = kwargs.get("check", True)
57
    kwargs["capture_output"] = kwargs.get("capture_output", True)
58
    try:
59
        subprocess.run(*args, **kwargs)
60
    except subprocess.CalledProcessError as cpe:
61
        raise CalledProcessError(
62
            cpe.returncode, cpe.cmd, output=cpe.output, stderr=cpe.stderr
63
        ) from None
64