Repository: pre-commit/pre-commit

Issue:
Failures when hook ids are non-ascii
```
$ pre-commit run ☃
An unexpected error has occurred: UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 0: ordinal not in range(128)
Check the log at ~/.pre-commit/pre-commit.log
$ cat ~/.pre-commit/pre-commit.log 
An unexpected error has occurred: UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 0: ordinal not in range(128)
Traceback (most recent call last):
  File "/home/asottile/workspace/pre-commit/pre_commit/error_handler.py", line 34, in error_handler
    yield
  File "/home/asottile/workspace/pre-commit/pre_commit/main.py", line 129, in main
    return run(runner, args)
  File "/home/asottile/workspace/pre-commit/pre_commit/commands/run.py", line 163, in run
    return _run_hook(runner, args, write=write)
  File "/home/asottile/workspace/pre-commit/pre_commit/commands/run.py", line 133, in _run_hook
    write('No hook with id `{0}`\n'.format(hook_id))
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 0: ordinal not in range(128)
```



Discussion:
Same error here, after running a `mvn test` invoking JUnit tests.
I don't have time to write a minimal test case right now sadly.


Resolving patch:
diff --git a/pre_commit/five.py b/pre_commit/five.py
--- a/pre_commit/five.py
+++ b/pre_commit/five.py
@@ -20,3 +20,11 @@ def n(s):
             return s
         else:
             return s.decode('UTF-8')
+
+
+def to_text(s):
+    return s if isinstance(s, text) else s.decode('UTF-8')
+
+
+def to_bytes(s):
+    return s if isinstance(s, bytes) else s.encode('UTF-8')
diff --git a/pre_commit/main.py b/pre_commit/main.py
--- a/pre_commit/main.py
+++ b/pre_commit/main.py
@@ -7,6 +7,7 @@
 import pkg_resources
 
 from pre_commit import color
+from pre_commit import five
 from pre_commit.commands.autoupdate import autoupdate
 from pre_commit.commands.clean import clean
 from pre_commit.commands.install_uninstall import install
@@ -25,6 +26,7 @@
 
 def main(argv=None):
     argv = argv if argv is not None else sys.argv[1:]
+    argv = [five.to_text(arg) for arg in argv]
     parser = argparse.ArgumentParser()
 
     # http://stackoverflow.com/a/8521644/812183
diff --git a/pre_commit/output.py b/pre_commit/output.py
--- a/pre_commit/output.py
+++ b/pre_commit/output.py
@@ -77,12 +77,8 @@ def get_hook_message(
         )
 
 
-def sys_stdout_write_wrapper(s, stream=sys.stdout):
-    """Python 2.6 chokes on unicode being passed to sys.stdout.write.
+stdout_byte_stream = getattr(sys.stdout, 'buffer', sys.stdout)
 
-    This is an adapter because PY2 is ok with bytes and PY3 requires text.
-    """
-    assert type(s) is five.text
-    if five.PY2:  # pragma: no cover (PY2)
-        s = s.encode('UTF-8')
-    stream.write(s)
+
+def sys_stdout_write_wrapper(s, stream=stdout_byte_stream):
+    stream.write(five.to_bytes(s))


Test patch:
diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py
--- a/tests/commands/install_uninstall_test.py
+++ b/tests/commands/install_uninstall_test.py
@@ -105,7 +105,7 @@ def _get_commit_output(
     cmd_output('git', 'add', touch_file)
     # Don't want to write to home directory
     home = home or tmpdir_factory.get()
-    env = dict(env_base, **{'PRE_COMMIT_HOME': home})
+    env = dict(env_base, PRE_COMMIT_HOME=home)
     return cmd_output(
         'git', 'commit', '-m', 'Commit!', '--allow-empty',
         # git commit puts pre-commit to stderr
@@ -414,7 +414,7 @@ def test_installed_from_venv(tmpdir_factory):
 def _get_push_output(tmpdir_factory):
     # Don't want to write to home directory
     home = tmpdir_factory.get()
-    env = dict(os.environ, **{'PRE_COMMIT_HOME': home})
+    env = dict(os.environ, PRE_COMMIT_HOME=home)
     return cmd_output(
         'git', 'push', 'origin', 'HEAD:new_branch',
         # git commit puts pre-commit to stderr
diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py
--- a/tests/commands/run_test.py
+++ b/tests/commands/run_test.py
@@ -5,6 +5,7 @@
 import os
 import os.path
 import subprocess
+import sys
 
 import mock
 import pytest
@@ -274,6 +275,22 @@ def test_multiple_hooks_same_id(
     assert output.count('Bash hook') == 2
 
 
+def test_non_ascii_hook_id(
+        repo_with_passing_hook, mock_out_store_directory, tmpdir_factory,
+):
+    with cwd(repo_with_passing_hook):
+        install(Runner(repo_with_passing_hook))
+        # Don't want to write to home directory
+        env = dict(os.environ, PRE_COMMIT_HOME=tmpdir_factory.get())
+        _, stdout, _ = cmd_output(
+            sys.executable, '-m', 'pre_commit.main', 'run', '☃',
+            env=env, retcode=None,
+        )
+        assert 'UnicodeDecodeError' not in stdout
+        # Doesn't actually happen, but a reasonable assertion
+        assert 'UnicodeEncodeError' not in stdout
+
+
 def test_stdout_write_bug_py26(
         repo_with_failing_hook, mock_out_store_directory, tmpdir_factory,
 ):
@@ -289,7 +306,7 @@ def test_stdout_write_bug_py26(
         install(Runner(repo_with_failing_hook))
 
         # Don't want to write to home directory
-        env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()})
+        env = dict(os.environ, PRE_COMMIT_HOME=tmpdir_factory.get())
         # Have to use subprocess because pytest monkeypatches sys.stdout
         _, stdout, _ = cmd_output(
             'git', 'commit', '-m', 'Commit!',
@@ -329,7 +346,7 @@ def test_lots_of_files(mock_out_store_directory, tmpdir_factory):
         install(Runner(git_path))
 
         # Don't want to write to home directory
-        env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()})
+        env = dict(os.environ, PRE_COMMIT_HOME=tmpdir_factory.get())
         cmd_output(
             'git', 'commit', '-m', 'Commit!',
             # git commit puts pre-commit to stderr