Repository: pre-commit/pre-commit

Issue:
Running pre-commit with Python installed from Windows Store raises UnicodeDecodeError
I think it's a special use case and maybe related to the [known issues of this kind of installation](https://docs.python.org/3.7/using/windows.html#known-issues), but still interesting to track it in issues isn't?

And the kind of error surprised me: `UnicodeDecodeError`.

**Reproduce**

1. Install Python through Windows Store
2. Create a virtualenv
3. Install pre-commit and run the hooks

**Environment**

- Windows 10 64 bits
- Python 3.7.6 installed from Windows Store (see: https://docs.python.org/3.7/using/windows.html#windows-store)

**Trace**

```python
[WARNING] Unstaged files detected.
[INFO] Stashing unstaged files to C:\Users\username/.cache\pre-commit\patch1583836330.
[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Initializing environment for https://github.com/python/black.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
[INFO] Restored changes from C:\Users\username/.cache\pre-commit\patch1583836330.
Traceback (most recent call last):
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\error_handler.py", line 54, in error_handler
    yield
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\main.py", line 371, in main
    return run(args.config, store, args)
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\commands\run.py", line 337, in run
    install_hook_envs(hooks, store)
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\repository.py", line 200, in install_hook_envs
    _hook_install(hook)
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\repository.py", line 83, in _hook_install
    hook.prefix, hook.language_version, hook.additional_dependencies,
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\languages\python.py", line 192, in install_environment
    _make_venv(env_dir, python)
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\languages\python.py", line 204, in make_venv
    cmd_output_b(*cmd, env=env, cwd='/')
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\util.py", line 140, in cmd_output_b
    raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b)
pre_commit.util.CalledProcessError: <exception str() failed>

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.7_3.7.1776.0_x64__qbz5n2kfra8p0\lib\runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.7_3.7.1776.0_x64__qbz5n2kfra8p0\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "D:\username\doculents\git\test-project\.venv\Scripts\pre-commit.exe\__main__.py", line 7, in <module>
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\main.py", line 384, in main
    f'Command {args.command} failed to exit with a returncode',
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.7_3.7.1776.0_x64__qbz5n2kfra8p0\lib\contextlib.py", line 130, in __exit__
    self.gen.throw(type, value, traceback)
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\error_handler.py", line 62, in error_handler
    _log_and_exit(msg, e, traceback.format_exc())
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\error_handler.py", line 18, in _log_and_exit
    error_msg = f'{msg}: {type(exc).__name__}: {exc}'
  File "d:\username\doculents\git\test-project\.venv\lib\site-packages\pre_commit\util.py", line 115, in __str__
    return self.__bytes__().decode()
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe8 in position 341: invalid continuation byte
```

Discussion:

Patch:
diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py
index 0ea7ed3..b095ba2 100644
--- a/pre_commit/error_handler.py
+++ b/pre_commit/error_handler.py
@@ -14,14 +14,24 @@ class FatalError(RuntimeError):
     pass
 
 
+def _exception_to_bytes(exc: BaseException) -> bytes:
+    with contextlib.suppress(TypeError):
+        return bytes(exc)  # type: ignore
+    with contextlib.suppress(Exception):
+        return str(exc).encode()
+    return f'<unprintable {type(exc).__name__} object>'.encode()
+
+
 def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None:
-    error_msg = f'{msg}: {type(exc).__name__}: {exc}'
-    output.write_line(error_msg)
+    error_msg = f'{msg}: {type(exc).__name__}: '.encode()
+    error_msg += _exception_to_bytes(exc)
+    output.write_line_b(error_msg)
     log_path = os.path.join(Store().directory, 'pre-commit.log')
     output.write_line(f'Check the log at {log_path}')
 
     with open(log_path, 'wb') as log:
         _log_line = functools.partial(output.write_line, stream=log)
+        _log_line_b = functools.partial(output.write_line_b, stream=log)
 
         _log_line('### version information')
         _log_line()
@@ -39,7 +49,7 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None:
         _log_line('### error information')
         _log_line()
         _log_line('```')
-        _log_line(error_msg)
+        _log_line_b(error_msg)
         _log_line('```')
         _log_line()
         _log_line('```')


Test patch:
diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py
index a8626f7..833bb8f 100644
--- a/tests/error_handler_test.py
+++ b/tests/error_handler_test.py
@@ -6,6 +6,7 @@ from unittest import mock
 import pytest
 
 from pre_commit import error_handler
+from pre_commit.util import CalledProcessError
 from testing.util import cmd_output_mocked_pre_commit_home
 
 
@@ -135,6 +136,22 @@ def test_error_handler_non_ascii_exception(mock_store_dir):
             raise ValueError('☃')
 
 
+def test_error_handler_non_utf8_exception(mock_store_dir):
+    with pytest.raises(SystemExit):
+        with error_handler.error_handler():
+            raise CalledProcessError(1, ('exe',), 0, b'error: \xa0\xe1', b'')
+
+
+def test_error_handler_non_stringable_exception(mock_store_dir):
+    class C(Exception):
+        def __str__(self):
+            raise RuntimeError('not today!')
+
+    with pytest.raises(SystemExit):
+        with error_handler.error_handler():
+            raise C()
+
+
 def test_error_handler_no_tty(tempdir_factory):
     pre_commit_home = tempdir_factory.get()
     ret, out, _ = cmd_output_mocked_pre_commit_home(