Repository: pre-commit/pre-commit

Issue:
Non-ascii prints in error handler without tty cause stacktrace
```
23:00:13 style runtests: commands[0] | pre-commit run --all-files
23:00:13 [INFO] Installing environment for git@git.yelpcorp.com:mirrors/pre-commit/mirrors-jshint.
23:00:13 [INFO] Once installed this environment will be reused.
23:00:13 [INFO] This may take a few minutes...
23:01:33 Traceback (most recent call last):
23:01:33   File ".tox/style/bin/pre-commit", line 11, in <module>
23:01:33     sys.exit(main())
23:01:33   File ".../.tox/style/local/lib/python2.7/site-packages/pre_commit/main.py", line 157, in main
23:01:33     'Command {0} failed to exit with a returncode'.format(args.command)
23:01:33   File "/usr/lib64/python2.7/contextlib.py", line 35, in __exit__
23:01:33     self.gen.throw(type, value, traceback)
23:01:33   File ".../.tox/style/local/lib/python2.7/site-packages/pre_commit/error_handler.py", line 41, in error_handler
23:01:33     traceback.format_exc(),
23:01:33   File ".../.tox/style/local/lib/python2.7/site-packages/pre_commit/error_handler.py", line 21, in _log_and_exit
23:01:33     print_fn(error_msg)
23:01:33 UnicodeEncodeError: 'ascii' codec can't encode characters in position 735-737: ordinal not in range(128)
```



Discussion:
I found a separate bug about writing that same stacktrace to the logfile.  I'll be fixing both it seems!


Resolving patch:
diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py
--- a/pre_commit/error_handler.py
+++ b/pre_commit/error_handler.py
@@ -7,7 +7,9 @@
 import os.path
 import traceback
 
+from pre_commit import five
 from pre_commit.errors import FatalError
+from pre_commit.output import sys_stdout_write_wrapper
 from pre_commit.store import Store
 
 
@@ -16,15 +18,15 @@ class PreCommitSystemExit(SystemExit):
     pass
 
 
-def _log_and_exit(msg, exc, formatted, print_fn=print):
-    error_msg = '{0}: {1}: {2}'.format(msg, type(exc).__name__, exc)
-    print_fn(error_msg)
-    print_fn('Check the log at ~/.pre-commit/pre-commit.log')
+def _log_and_exit(msg, exc, formatted, write_fn=sys_stdout_write_wrapper):
+    error_msg = '{0}: {1}: {2}\n'.format(msg, type(exc).__name__, exc)
+    write_fn(error_msg)
+    write_fn('Check the log at ~/.pre-commit/pre-commit.log\n')
     store = Store()
     store.require_created()
-    with io.open(os.path.join(store.directory, 'pre-commit.log'), 'w') as log:
-        log.write(error_msg + '\n')
-        log.write(formatted + '\n')
+    with io.open(os.path.join(store.directory, 'pre-commit.log'), 'wb') as log:
+        log.write(five.to_bytes(error_msg))
+        log.write(five.to_bytes(formatted) + b'\n')
     raise PreCommitSystemExit(1)
 
 


Test patch:
diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py
--- a/tests/error_handler_test.py
+++ b/tests/error_handler_test.py
@@ -1,15 +1,18 @@
+# -*- coding: utf-8 -*-
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
 import io
 import os.path
 import re
+import sys
 
 import mock
 import pytest
 
 from pre_commit import error_handler
 from pre_commit.errors import FatalError
+from pre_commit.util import cmd_output
 
 
 @pytest.yield_fixture
@@ -72,17 +75,17 @@ def test_error_handler_uncaught_error(mocked_log_and_exit):
 
 
 def test_log_and_exit(mock_out_store_directory):
-    mocked_print = mock.Mock()
+    mocked_write = mock.Mock()
     with pytest.raises(error_handler.PreCommitSystemExit):
         error_handler._log_and_exit(
             'msg', FatalError('hai'), "I'm a stacktrace",
-            print_fn=mocked_print,
+            write_fn=mocked_write,
         )
 
-    printed = '\n'.join(call[0][0] for call in mocked_print.call_args_list)
+    printed = ''.join(call[0][0] for call in mocked_write.call_args_list)
     assert printed == (
         'msg: FatalError: hai\n'
-        'Check the log at ~/.pre-commit/pre-commit.log'
+        'Check the log at ~/.pre-commit/pre-commit.log\n'
     )
 
     log_file = os.path.join(mock_out_store_directory, 'pre-commit.log')
@@ -92,3 +95,25 @@ def test_log_and_exit(mock_out_store_directory):
         'msg: FatalError: hai\n'
         "I'm a stacktrace\n"
     )
+
+
+def test_error_handler_non_ascii_exception(mock_out_store_directory):
+    with pytest.raises(error_handler.PreCommitSystemExit):
+        with error_handler.error_handler():
+            raise ValueError('☃')
+
+
+def test_error_handler_no_tty(tempdir_factory):
+    output = cmd_output(
+        sys.executable, '-c',
+        'from __future__ import unicode_literals\n'
+        'from pre_commit.error_handler import error_handler\n'
+        'with error_handler():\n'
+        '    raise ValueError("\\u2603")\n',
+        env=dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()),
+        retcode=1,
+    )
+    assert output[1].replace('\r', '') == (
+        'An unexpected error has occurred: ValueError: ☃\n'
+        'Check the log at ~/.pre-commit/pre-commit.log\n'
+    )