Skip to content

Commit ecf82ed

Browse files
committed
Use bytes for sys.stdout.write in PY2. Closes pre-commit#161.
1 parent 37d3dc0 commit ecf82ed

File tree

4 files changed

+53
-1
lines changed

4 files changed

+53
-1
lines changed

pre_commit/commands/run.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pre_commit import color
1010
from pre_commit.logging_handler import LoggingHandler
1111
from pre_commit.output import get_hook_message
12+
from pre_commit.output import sys_stdout_write_wrapper
1213
from pre_commit.staged_files_only import staged_files_only
1314
from pre_commit.util import noop_context
1415

@@ -125,7 +126,7 @@ def _has_unmerged_paths(runner):
125126
return bool(stdout.strip())
126127

127128

128-
def run(runner, args, write=sys.stdout.write, environ=os.environ):
129+
def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ):
129130
# Set up our logging handler
130131
logger.addHandler(LoggingHandler(args.color, write=write))
131132
logger.setLevel(logging.INFO)

pre_commit/output.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import unicode_literals
22

33
import subprocess
4+
import sys
45

56
from pre_commit import color
7+
from pre_commit import five
68

79

810
# TODO: smell: import side-effects
@@ -70,3 +72,14 @@ def get_hook_message(
7072
postfix,
7173
color.format_color(end_msg, end_color, use_color),
7274
)
75+
76+
77+
def sys_stdout_write_wrapper(s, stream=sys.stdout):
78+
"""Python 2.6 chokes on unicode being passed to sys.stdout.write.
79+
80+
This is an adapter because PY2 is ok with bytes and PY3 requires text.
81+
"""
82+
assert type(s) is five.text
83+
if five.PY2: # pragma: no cover (PY2)
84+
s = s.encode('UTF-8')
85+
stream.write(s)

tests/commands/run_test.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
# -*- coding: UTF-8 -*-
12
from __future__ import unicode_literals
23

34
import io
45
import mock
56
import os
67
import os.path
78
import pytest
9+
import subprocess
810
from plumbum import local
911

12+
from pre_commit.commands.install_uninstall import install
1013
from pre_commit.commands.run import _get_skips
1114
from pre_commit.commands.run import _has_unmerged_paths
1215
from pre_commit.commands.run import run
@@ -225,3 +228,30 @@ def test_multiple_hooks_same_id(
225228
ret, output = _do_run(repo_with_passing_hook, _get_opts())
226229
assert ret == 0
227230
assert output.count('Bash hook') == 2
231+
232+
233+
def test_stdout_write_bug_py26(
234+
repo_with_failing_hook, mock_out_store_directory, tmpdir_factory,
235+
):
236+
with local.cwd(repo_with_failing_hook):
237+
# Add bash hook on there again
238+
with io.open('.pre-commit-config.yaml', 'a+') as config_file:
239+
config_file.write(' args: ["☃"]\n')
240+
local['git']('add', '.pre-commit-config.yaml')
241+
stage_a_file()
242+
243+
install(Runner(repo_with_failing_hook))
244+
245+
# Don't want to write to home directory
246+
env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()})
247+
# Have to use subprocess because pytest monkeypatches sys.stdout
248+
_, stdout, _ = local['git'].run(
249+
('commit', '-m', 'Commit!'),
250+
# git commit puts pre-commit to stderr
251+
stderr=subprocess.STDOUT,
252+
env=env,
253+
retcode=None,
254+
)
255+
assert 'UnicodeEncodeError' not in stdout
256+
# Doesn't actually happen, but a reasonable assertion
257+
assert 'UnicodeDecodeError' not in stdout

tests/output_test.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from __future__ import unicode_literals
22

3+
import mock
34
import pytest
45

56
from pre_commit import color
67
from pre_commit.output import get_hook_message
8+
from pre_commit.output import sys_stdout_write_wrapper
79

810

911
@pytest.mark.parametrize(
@@ -77,3 +79,9 @@ def test_make_sure_postfix_is_not_colored():
7779
assert ret == (
7880
'start' + '.' * 6 + 'post ' + color.RED + 'end' + color.NORMAL + '\n'
7981
)
82+
83+
84+
def test_sys_stdout_write_wrapper_writes():
85+
fake_stream = mock.Mock()
86+
sys_stdout_write_wrapper('hello world', fake_stream)
87+
assert fake_stream.write.call_count == 1

0 commit comments

Comments
 (0)