Repository: pre-commit/pre-commit

Issue:
Display hook ids when running pre-commit
 The ideal output would look something like this:

[flake8] Flake8..........................................Passed



Discussion:

Resolving patch:
diff --git a/pre_commit/commands.py b/pre_commit/commands.py
--- a/pre_commit/commands.py
+++ b/pre_commit/commands.py
@@ -5,7 +5,6 @@
 import pkg_resources
 import shutil
 import stat
-import subprocess
 import sys
 from asottile.ordereddict import OrderedDict
 from asottile.yaml import ordered_dump
@@ -19,6 +18,7 @@
 from pre_commit.clientlib.validate_config import load_config
 from pre_commit.jsonschema_extensions import remove_defaults
 from pre_commit.logging_handler import LoggingHandler
+from pre_commit.output import get_hook_message
 from pre_commit.repository import Repository
 from pre_commit.staged_files_only import staged_files_only
 from pre_commit.util import noop_context
@@ -26,10 +26,6 @@
 
 logger = logging.getLogger('pre_commit')
 
-COLS = int(subprocess.Popen(['tput', 'cols'], stdout=subprocess.PIPE).communicate()[0])
-
-PASS_FAIL_LENGTH = 6
-
 
 def install(runner):
     """Install the pre-commit hooks."""
@@ -107,7 +103,8 @@ def autoupdate(runner):
     )
 
     for repo_config in input_configs:
-        print('Updating {0}...'.format(repo_config['repo']), end='')
+        sys.stdout.write('Updating {0}...'.format(repo_config['repo']))
+        sys.stdout.flush()
         try:
             new_repo_config = _update_repository(repo_config)
         except RepositoryCannotBeUpdatedError as error:
@@ -149,34 +146,30 @@ def _get_skips(environ):
     return set(skip.strip() for skip in skips.split(',') if skip.strip())
 
 
-def _print_no_files_skipped(hook, write, args):
-    no_files_msg = '(no files to check) '
-    skipped_msg = 'Skipped'
-    write(
-        '{0}{1}{2}{3}\n'.format(
-            hook['name'],
-            '.' * (
-                COLS -
-                len(hook['name']) -
-                len(no_files_msg) -
-                len(skipped_msg) -
-                1
-            ),
-            no_files_msg,
-            color.format_color(skipped_msg, color.TURQUOISE, args.color),
-        )
+def _hook_msg_start(hook, verbose):
+    return '{0}{1}'.format(
+        '[{0}] '.format(hook['id']) if verbose else '',
+        hook['name'],
     )
 
 
+def _print_no_files_skipped(hook, write, args):
+    write(get_hook_message(
+        _hook_msg_start(hook, args.verbose),
+        postfix='(no files to check) ',
+        end_msg='Skipped',
+        end_color=color.TURQUOISE,
+        use_color=args.color,
+    ))
+
+
 def _print_user_skipped(hook, write, args):
-    skipped_msg = 'Skipped'
-    write(
-        '{0}{1}{2}\n'.format(
-            hook['name'],
-            '.' * (COLS - len(hook['name']) - len(skipped_msg) - 1),
-            color.format_color(skipped_msg, color.YELLOW, args.color),
-        ),
-    )
+    write(get_hook_message(
+        _hook_msg_start(hook, args.verbose),
+        end_msg='Skipped',
+        end_color=color.YELLOW,
+        use_color=args.color,
+    ))
 
 
 def _run_single_hook(runner, repository, hook_id, args, write, skips=set()):
@@ -199,12 +192,7 @@ def _run_single_hook(runner, repository, hook_id, args, write, skips=set()):
 
     # Print the hook and the dots first in case the hook takes hella long to
     # run.
-    write(
-        '{0}{1}'.format(
-            hook['name'],
-            '.' * (COLS - len(hook['name']) - PASS_FAIL_LENGTH - 1),
-        ),
-    )
+    write(get_hook_message(_hook_msg_start(hook, args.verbose), end_len=6))
     sys.stdout.flush()
 
     retcode, stdout, stderr = repository.run_hook(
diff --git a/pre_commit/output.py b/pre_commit/output.py
new file mode 100644
--- /dev/null
+++ b/pre_commit/output.py
@@ -0,0 +1,66 @@
+import subprocess
+
+from pre_commit import color
+
+
+# TODO: smell: import side-effects
+COLS = int(
+    subprocess.Popen(['tput', 'cols'], stdout=subprocess.PIPE).communicate()[0]
+)
+
+
+def get_hook_message(
+    start,
+    postfix='',
+    end_msg=None,
+    end_len=0,
+    end_color=None,
+    use_color=None,
+    cols=COLS,
+):
+    """Prints a message for running a hook.
+
+    This currently supports three approaches:
+
+    # Print `start` followed by dots, leaving 6 characters at the end
+    >>> print_hook_message('start', end_len=6)
+    start...............................................................
+
+    # Print `start` followed by dots with the end message colored if coloring is
+    # specified and a newline afterwards
+    >>> print_hook_message(
+        'start',
+        end_msg='end',
+        end_color=color.RED,
+        use_color=True,
+    )
+    start...................................................................end
+
+    # Print `start` followed by dots, followed by the `postfix` message
+    # uncolored, followed by the `end_msg` colored if specified and a newline
+    # afterwards
+    >>> print_hook_message(
+        'start',
+        postfix='postfix ',
+        end_msg='end',
+        end_color=color.RED,
+        use_color=True,
+    )
+    start...........................................................postfix end
+    """
+    if bool(end_msg) == bool(end_len):
+        raise ValueError('Expected one of (`end_msg`, `end_len`)')
+    if end_msg is not None and (end_color is None or use_color is None):
+        raise ValueError(
+            '`end_color` and `use_color` are required with `end_msg`'
+        )
+
+    if end_len:
+        return start + '.' * (cols - len(start) - end_len - 1)
+    else:
+        return '{0}{1}{2}{3}\n'.format(
+            start,
+            '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1),
+            postfix,
+            color.format_color(end_msg, end_color, use_color),
+        )


Test patch:
diff --git a/tests/commands_test.py b/tests/commands_test.py
--- a/tests/commands_test.py
+++ b/tests/commands_test.py
@@ -330,3 +330,13 @@ def test_skip_hook(repo_with_passing_hook):
     )
     for msg in ('Bash hook', 'Skipped'):
         assert msg in printed
+
+
+def test_hook_id_not_in_non_verbose_output(repo_with_passing_hook):
+    ret, printed = _do_run(repo_with_passing_hook, _get_opts(verbose=False))
+    assert '[bash_hook]' not in printed
+
+
+def test_hook_id_in_verbose_output(repo_with_passing_hook):
+    ret, printed = _do_run(repo_with_passing_hook, _get_opts(verbose=True))
+    assert '[bash_hook] Bash hook' in printed
diff --git a/tests/output_test.py b/tests/output_test.py
new file mode 100644
--- /dev/null
+++ b/tests/output_test.py
@@ -0,0 +1,77 @@
+import pytest
+
+from pre_commit import color
+from pre_commit.output import get_hook_message
+
+
+@pytest.mark.parametrize(
+    'kwargs',
+    (
+        # both end_msg and end_len
+        {'end_msg': 'end', 'end_len': 1, 'end_color': '', 'use_color': True},
+        # Neither end_msg nor end_len
+        {},
+        # Neither color option for end_msg
+        {'end_msg': 'end'},
+        # No use_color for end_msg
+        {'end_msg': 'end', 'end_color': ''},
+        # No end_color for end_msg
+        {'end_msg': 'end', 'use_color': ''},
+    ),
+)
+def test_get_hook_message_raises(kwargs):
+    with pytest.raises(ValueError):
+        get_hook_message('start', **kwargs)
+
+
+def test_case_with_end_len():
+    ret = get_hook_message('start', end_len=5, cols=15)
+    assert ret == 'start' + '.' * 4
+
+
+def test_case_with_end_msg():
+    ret = get_hook_message(
+        'start',
+        end_msg='end',
+        end_color='',
+        use_color=False,
+        cols=15,
+    )
+    assert ret == 'start' + '.' * 6 + 'end' + '\n'
+
+
+def test_case_with_end_msg_using_color():
+    ret = get_hook_message(
+        'start',
+        end_msg='end',
+        end_color=color.RED,
+        use_color=True,
+        cols=15,
+    )
+    assert ret == 'start' + '.' * 6 + color.RED + 'end' + color.NORMAL + '\n'
+
+
+def test_case_with_postfix_message():
+    ret = get_hook_message(
+        'start',
+        postfix='post ',
+        end_msg='end',
+        end_color='',
+        use_color=False,
+        cols=20,
+    )
+    assert ret == 'start' + '.' * 6 + 'post ' + 'end' + '\n'
+
+
+def test_make_sure_postfix_is_not_colored():
+    ret = get_hook_message(
+        'start',
+        postfix='post ',
+        end_msg='end',
+        end_color=color.RED,
+        use_color=True,
+        cols=20,
+    )
+    assert ret == (
+        'start' + '.' * 6 + 'post ' + color.RED + 'end' + color.NORMAL + '\n'
+    )