Skip to content

Commit

Permalink
show color in hook outputs when attached to a tty
Browse files Browse the repository at this point in the history
  • Loading branch information
asottile committed Oct 13, 2019
1 parent c8620f3 commit 7c3404e
Show file tree
Hide file tree
Showing 27 changed files with 200 additions and 76 deletions.
9 changes: 6 additions & 3 deletions pre_commit/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def _hook_msg_start(hook, verbose):
NO_FILES = '(no files to check)'


def _run_single_hook(classifier, hook, args, skips, cols):
def _run_single_hook(classifier, hook, args, skips, cols, use_color):
filenames = classifier.filenames_for_hook(hook)

if hook.language == 'pcre':
Expand Down Expand Up @@ -118,7 +118,8 @@ def _run_single_hook(classifier, hook, args, skips, cols):
sys.stdout.flush()

diff_before = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None)
retcode, out = hook.run(tuple(filenames) if hook.pass_filenames else ())
filenames = tuple(filenames) if hook.pass_filenames else ()
retcode, out = hook.run(filenames, use_color)
diff_after = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None)

file_modifications = diff_before != diff_after
Expand Down Expand Up @@ -203,7 +204,9 @@ def _run_hooks(config, hooks, args, environ):
classifier = Classifier(filenames)
retval = 0
for hook in hooks:
retval |= _run_single_hook(classifier, hook, args, skips, cols)
retval |= _run_single_hook(
classifier, hook, args, skips, cols, args.color,
)
if retval and config['fail_fast']:
break
if retval and args.show_diff_on_failure and git.has_diff():
Expand Down
5 changes: 3 additions & 2 deletions pre_commit/languages/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,17 @@
# version - A version specified in the hook configuration or 'default'.
# """
#
# def run_hook(hook, file_args):
# def run_hook(hook, file_args, color):
# """Runs a hook and returns the returncode and output of running that
# hook.
#
# Args:
# hook - `Hook`
# file_args - The files to be run
# color - whether the hook should be given a pty (when supported)
#
# Returns:
# (returncode, stdout, stderr)
# (returncode, output)
# """

languages = {
Expand Down
8 changes: 4 additions & 4 deletions pre_commit/languages/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ def docker_cmd(): # pragma: windows no cover
)


def run_hook(hook, file_args): # pragma: windows no cover
def run_hook(hook, file_args, color): # pragma: windows no cover
assert_docker_available()
# Rebuild the docker image in case it has gone missing, as many people do
# automated cleanup of docker images.
build_docker_image(hook.prefix, pull=False)

hook_cmd = helpers.to_cmd(hook)
entry_exe, cmd_rest = hook_cmd[0], hook_cmd[1:]
hook_cmd = hook.cmd
entry_exe, cmd_rest = hook.cmd[0], hook_cmd[1:]

entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix))
cmd = docker_cmd() + entry_tag + cmd_rest
return helpers.run_xargs(hook, cmd, file_args)
return helpers.run_xargs(hook, cmd, file_args, color=color)
6 changes: 3 additions & 3 deletions pre_commit/languages/docker_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
install_environment = helpers.no_install


def run_hook(hook, file_args): # pragma: windows no cover
def run_hook(hook, file_args, color): # pragma: windows no cover
assert_docker_available()
cmd = docker_cmd() + helpers.to_cmd(hook)
return helpers.run_xargs(hook, cmd, file_args)
cmd = docker_cmd() + hook.cmd
return helpers.run_xargs(hook, cmd, file_args, color=color)
2 changes: 1 addition & 1 deletion pre_commit/languages/fail.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
install_environment = helpers.no_install


def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
out = hook.entry.encode('UTF-8') + b'\n\n'
out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n'
return 1, out
4 changes: 2 additions & 2 deletions pre_commit/languages/golang.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@ def install_environment(prefix, version, additional_dependencies):
rmtree(pkgdir)


def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
with in_env(hook.prefix):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
10 changes: 3 additions & 7 deletions pre_commit/languages/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import multiprocessing
import os
import random
import shlex

import six

Expand All @@ -25,10 +24,6 @@ def environment_dir(ENVIRONMENT_DIR, language_version):
return '{}-{}'.format(ENVIRONMENT_DIR, language_version)


def to_cmd(hook):
return tuple(shlex.split(hook.entry)) + tuple(hook.args)


def assert_version_default(binary, version):
if version != C.DEFAULT:
raise AssertionError(
Expand Down Expand Up @@ -83,8 +78,9 @@ def _shuffled(seq):
return seq


def run_xargs(hook, cmd, file_args):
def run_xargs(hook, cmd, file_args, **kwargs):
# Shuffle the files so that they more evenly fill out the xargs partitions,
# but do it deterministically in case a hook cares about ordering.
file_args = _shuffled(file_args)
return xargs(cmd, file_args, target_concurrency=target_concurrency(hook))
kwargs['target_concurrency'] = target_concurrency(hook)
return xargs(cmd, file_args, **kwargs)
4 changes: 2 additions & 2 deletions pre_commit/languages/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@ def install_environment(
)


def run_hook(hook, file_args): # pragma: windows no cover
def run_hook(hook, file_args, color): # pragma: windows no cover
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
4 changes: 2 additions & 2 deletions pre_commit/languages/pcre.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
install_environment = helpers.no_install


def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
# For PCRE the entry is the regular expression to match
cmd = (GREP, '-H', '-n', '-P') + tuple(hook.args) + (hook.entry,)

# Grep usually returns 0 for matches, and nonzero for non-matches so we
# negate it here.
return xargs(cmd, file_args, negate=True)
return xargs(cmd, file_args, negate=True, color=color)
4 changes: 2 additions & 2 deletions pre_commit/languages/pygrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ def _process_filename_at_once(pattern, filename):
return retv


def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,)
return xargs(exe, file_args)
return xargs(exe, file_args, color=color)


def main(argv=None):
Expand Down
4 changes: 2 additions & 2 deletions pre_commit/languages/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ def healthy(prefix, language_version):
)
return retcode == 0

def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

def install_environment(prefix, version, additional_dependencies):
additional_dependencies = tuple(additional_dependencies)
Expand Down
4 changes: 2 additions & 2 deletions pre_commit/languages/ruby.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,6 @@ def install_environment(
)


def run_hook(hook, file_args): # pragma: windows no cover
def run_hook(hook, file_args, color): # pragma: windows no cover
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
4 changes: 2 additions & 2 deletions pre_commit/languages/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,6 @@ def install_environment(prefix, version, additional_dependencies):
)


def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
with in_env(hook.prefix):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
6 changes: 3 additions & 3 deletions pre_commit/languages/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
install_environment = helpers.no_install


def run_hook(hook, file_args):
cmd = helpers.to_cmd(hook)
def run_hook(hook, file_args, color):
cmd = hook.cmd
cmd = (hook.prefix.path(cmd[0]),) + cmd[1:]
return helpers.run_xargs(hook, cmd, file_args)
return helpers.run_xargs(hook, cmd, file_args, color=color)
4 changes: 2 additions & 2 deletions pre_commit/languages/swift.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ def install_environment(
)


def run_hook(hook, file_args): # pragma: windows no cover
def run_hook(hook, file_args, color): # pragma: windows no cover
with in_env(hook.prefix):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
4 changes: 2 additions & 2 deletions pre_commit/languages/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
install_environment = helpers.no_install


def run_hook(hook, file_args):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
def run_hook(hook, file_args, color):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
9 changes: 7 additions & 2 deletions pre_commit/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
import logging
import os
import shlex

import pre_commit.constants as C
from pre_commit import five
Expand Down Expand Up @@ -54,6 +55,10 @@ def _write_state(prefix, venv, state):
class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)):
__slots__ = ()

@property
def cmd(self):
return tuple(shlex.split(self.entry)) + tuple(self.args)

@property
def install_key(self):
return (
Expand Down Expand Up @@ -95,9 +100,9 @@ def install(self):
# Write our state to indicate we're installed
_write_state(self.prefix, venv, _state(self.additional_dependencies))

def run(self, file_args):
def run(self, file_args, color):
lang = languages[self.language]
return lang.run_hook(self, file_args)
return lang.run_hook(self, file_args, color)

@classmethod
def create(cls, src, prefix, dct):
Expand Down
87 changes: 76 additions & 11 deletions pre_commit/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,29 +117,28 @@ def to_text(self):
__str__ = to_text


def cmd_output_b(*cmd, **kwargs):
retcode = kwargs.pop('retcode', 0)

popen_kwargs = {
'stdin': subprocess.PIPE,
'stdout': subprocess.PIPE,
'stderr': subprocess.PIPE,
}

def _cmd_kwargs(*cmd, **kwargs):
# py2/py3 on windows are more strict about the types here
cmd = tuple(five.n(arg) for arg in cmd)
kwargs['env'] = {
five.n(key): five.n(value)
for key, value in kwargs.pop('env', {}).items()
} or None
popen_kwargs.update(kwargs)
for arg in ('stdin', 'stdout', 'stderr'):
kwargs.setdefault(arg, subprocess.PIPE)
return cmd, kwargs


def cmd_output_b(*cmd, **kwargs):
retcode = kwargs.pop('retcode', 0)
cmd, kwargs = _cmd_kwargs(*cmd, **kwargs)

try:
cmd = parse_shebang.normalize_cmd(cmd)
except parse_shebang.ExecutableNotFoundError as e:
returncode, stdout_b, stderr_b = e.to_output()
else:
proc = subprocess.Popen(cmd, **popen_kwargs)
proc = subprocess.Popen(cmd, **kwargs)
stdout_b, stderr_b = proc.communicate()
returncode = proc.returncode

Expand All @@ -158,6 +157,72 @@ def cmd_output(*cmd, **kwargs):
return returncode, stdout, stderr


if os.name != 'nt': # pragma: windows no cover
from os import openpty
import termios

class Pty(object):
def __init__(self):
self.r = self.w = None

def __enter__(self):
self.r, self.w = openpty()

# tty flags normally change \n to \r\n
attrs = termios.tcgetattr(self.r)
attrs[1] &= ~(termios.ONLCR | termios.OPOST)
termios.tcsetattr(self.r, termios.TCSANOW, attrs)

return self

def close_w(self):
if self.w is not None:
os.close(self.w)
self.w = None

def close_r(self):
assert self.r is not None
os.close(self.r)
self.r = None

def __exit__(self, exc_type, exc_value, traceback):
self.close_w()
self.close_r()

def cmd_output_p(*cmd, **kwargs):
assert kwargs.pop('retcode') is None
assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr']
cmd, kwargs = _cmd_kwargs(*cmd, **kwargs)

try:
cmd = parse_shebang.normalize_cmd(cmd)
except parse_shebang.ExecutableNotFoundError as e:
return e.to_output()

with open(os.devnull) as devnull, Pty() as pty:
kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w})
proc = subprocess.Popen(cmd, **kwargs)
pty.close_w()

buf = b''
while True:
try:
bts = os.read(pty.r, 4096)
except OSError as e:
if e.errno == errno.EIO:
bts = b''
else:
raise
else:
buf += bts
if not bts:
break

return proc.wait(), buf, None
else: # pragma: no cover
cmd_output_p = cmd_output_b


def rmtree(path):
"""On windows, rmtree fails for readonly dirs."""
def handle_remove_readonly(func, path, exc):
Expand Down
5 changes: 4 additions & 1 deletion pre_commit/xargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from pre_commit import parse_shebang
from pre_commit.util import cmd_output_b
from pre_commit.util import cmd_output_p


def _environ_size(_env=None):
Expand Down Expand Up @@ -108,9 +109,11 @@ def xargs(cmd, varargs, **kwargs):
negate: Make nonzero successful and zero a failure
target_concurrency: Target number of partitions to run concurrently
"""
color = kwargs.pop('color', False)
negate = kwargs.pop('negate', False)
target_concurrency = kwargs.pop('target_concurrency', 1)
max_length = kwargs.pop('_max_length', _get_platform_max_length())
cmd_fn = cmd_output_p if color else cmd_output_b
retcode = 0
stdout = b''

Expand All @@ -122,7 +125,7 @@ def xargs(cmd, varargs, **kwargs):
partitions = partition(cmd, varargs, target_concurrency, max_length)

def run_cmd_partition(run_cmd):
return cmd_output_b(
return cmd_fn(
*run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs
)

Expand Down
Loading

0 comments on commit 7c3404e

Please sign in to comment.