Repository: pre-commit/pre-commit

Issue:
pip install crashes can easily confuse newbies
those that are not familiar with the usual annoying messaging that pip presents can get pretty easily confused by the output that happens when `pip` fails to install

here's an example:

```console
$ pre-commit run flake8 --all-files
[INFO] Initializing environment for https://gitlab.com/pycqa/flake8:flake8-walrus.
[INFO] Installing environment for https://gitlab.com/pycqa/flake8.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
An unexpected error has occurred: CalledProcessError: Command: ('/home/asottile/.cache/pre-commit/repoi6ij0tyu/py_env-python3/bin/python', '/home/asottile/.cache/pre-commit/repoi6ij0tyu/py_env-python3/bin/pip', 'install', '.', 'flake8-walrus')
Return code: 1
Expected return code: 0
Output: 
    Processing /home/asottile/.cache/pre-commit/repoi6ij0tyu
    Collecting flake8-walrus
    
Errors: 
      ERROR: Could not find a version that satisfies the requirement flake8-walrus (from versions: none)
    ERROR: No matching distribution found for flake8-walrus
    WARNING: You are using pip version 19.2.3, however version 19.3.1 is available.
    You should consider upgrading via the 'pip install --upgrade pip' command.
    
Check the log at /home/asottile/.cache/pre-commit/pre-commit.log
```

this ~admittedly is a bit garbled for a number of reasons:
- pip's error message here isn't great (it _could_ say something about `python_requires` or that there are versions available for other versions) **(the actual error is that the python is python3.6 and the plugin requires python3.8)**
- pip is out of date (when is it not? but admittedly who cares) -- **this is what a lot of people try and fix** -- unfortunately there's not really anything to fix here, the version of `pip` is from inside the virtualenv and doesn't really matter all that much
- `pre-commit` is currently splitting the output from stdout and stderr making it harder to read what's going on

I can't really fix the first one, and the second one I could silence but it doesn't quite feel like the right thing to do (and admittedly knowing the pip version is sometimes useful when debugging).  The third however I can pretty easily fix!


Discussion:
while actually implementing this, it turns out that combining stdout / stderr presents a pretty easy footgun in some cases (where `pre-commit` collects the output from a subprocess) so I'm probably just going to `s/Errors/Stderr/` (and commit the other cleanups I found while doing this)

Resolving patch:
diff --git a/pre_commit/util.py b/pre_commit/util.py
--- a/pre_commit/util.py
+++ b/pre_commit/util.py
@@ -74,36 +74,31 @@ def make_executable(filename):
 
 
 class CalledProcessError(RuntimeError):
-    def __init__(self, returncode, cmd, expected_returncode, output=None):
+    def __init__(self, returncode, cmd, expected_returncode, stdout, stderr):
         super(CalledProcessError, self).__init__(
-            returncode, cmd, expected_returncode, output,
+            returncode, cmd, expected_returncode, stdout, stderr,
         )
         self.returncode = returncode
         self.cmd = cmd
         self.expected_returncode = expected_returncode
-        self.output = output
+        self.stdout = stdout
+        self.stderr = stderr
 
     def to_bytes(self):
-        output = []
-        for maybe_text in self.output:
-            if maybe_text:
-                output.append(
-                    b'\n    ' +
-                    five.to_bytes(maybe_text).replace(b'\n', b'\n    '),
-                )
+        def _indent_or_none(part):
+            if part:
+                return b'\n    ' + part.replace(b'\n', b'\n    ')
             else:
-                output.append(b'(none)')
+                return b' (none)'
 
         return b''.join((
-            five.to_bytes(
-                'Command: {!r}\n'
-                'Return code: {}\n'
-                'Expected return code: {}\n'.format(
-                    self.cmd, self.returncode, self.expected_returncode,
-                ),
-            ),
-            b'Output: ', output[0], b'\n',
-            b'Errors: ', output[1],
+            'command: {!r}\n'
+            'return code: {}\n'
+            'expected return code: {}\n'.format(
+                self.cmd, self.returncode, self.expected_returncode,
+            ).encode('UTF-8'),
+            b'stdout:', _indent_or_none(self.stdout), b'\n',
+            b'stderr:', _indent_or_none(self.stderr),
         ))
 
     def to_text(self):
@@ -143,9 +138,7 @@ def cmd_output_b(*cmd, **kwargs):
         returncode = proc.returncode
 
     if retcode is not None and retcode != returncode:
-        raise CalledProcessError(
-            returncode, cmd, retcode, output=(stdout_b, stderr_b),
-        )
+        raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b)
 
     return returncode, stdout_b, stderr_b
 


Test patch:
diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py
--- a/tests/languages/docker_test.py
+++ b/tests/languages/docker_test.py
@@ -10,7 +10,7 @@
 def test_docker_is_running_process_error():
     with mock.patch(
         'pre_commit.languages.docker.cmd_output_b',
-        side_effect=CalledProcessError(*(None,) * 4),
+        side_effect=CalledProcessError(None, None, None, None, None),
     ):
         assert docker.docker_is_running() is False
 
diff --git a/tests/store_test.py b/tests/store_test.py
--- a/tests/store_test.py
+++ b/tests/store_test.py
@@ -125,7 +125,7 @@ def test_clone_shallow_failure_fallback_to_complete(
 
     # Force shallow clone failure
     def fake_shallow_clone(self, *args, **kwargs):
-        raise CalledProcessError(None, None, None)
+        raise CalledProcessError(None, None, None, None, None)
     store._shallow_clone = fake_shallow_clone
 
     ret = store.clone(path, rev)
diff --git a/tests/util_test.py b/tests/util_test.py
--- a/tests/util_test.py
+++ b/tests/util_test.py
@@ -9,6 +9,7 @@
 from pre_commit.util import CalledProcessError
 from pre_commit.util import clean_path_on_failure
 from pre_commit.util import cmd_output
+from pre_commit.util import cmd_output_b
 from pre_commit.util import cmd_output_p
 from pre_commit.util import parse_version
 from pre_commit.util import rmtree
@@ -16,30 +17,26 @@
 
 
 def test_CalledProcessError_str():
-    error = CalledProcessError(
-        1, [str('git'), str('status')], 0, (str('stdout'), str('stderr')),
-    )
+    error = CalledProcessError(1, [str('exe')], 0, b'output', b'errors')
     assert str(error) == (
-        "Command: ['git', 'status']\n"
-        'Return code: 1\n'
-        'Expected return code: 0\n'
-        'Output: \n'
-        '    stdout\n'
-        'Errors: \n'
-        '    stderr'
+        "command: ['exe']\n"
+        'return code: 1\n'
+        'expected return code: 0\n'
+        'stdout:\n'
+        '    output\n'
+        'stderr:\n'
+        '    errors'
     )
 
 
 def test_CalledProcessError_str_nooutput():
-    error = CalledProcessError(
-        1, [str('git'), str('status')], 0, (str(''), str('')),
-    )
+    error = CalledProcessError(1, [str('exe')], 0, b'', b'')
     assert str(error) == (
-        "Command: ['git', 'status']\n"
-        'Return code: 1\n'
-        'Expected return code: 0\n'
-        'Output: (none)\n'
-        'Errors: (none)'
+        "command: ['exe']\n"
+        'return code: 1\n'
+        'expected return code: 0\n'
+        'stdout: (none)\n'
+        'stderr: (none)'
     )
 
 
@@ -90,8 +87,9 @@ def test_cmd_output_exe_not_found():
     assert out == 'Executable `dne` not found'
 
 
-def test_cmd_output_p_exe_not_found():
-    ret, out, _ = cmd_output_p('dne', retcode=None, stderr=subprocess.STDOUT)
+@pytest.mark.parametrize('fn', (cmd_output_b, cmd_output_p))
+def test_cmd_output_exe_not_found_bytes(fn):
+    ret, out, _ = fn('dne', retcode=None, stderr=subprocess.STDOUT)
     assert ret == 1
     assert out == b'Executable `dne` not found'