Repository: pre-commit/pre-commit (python)

Issue:
Implement `default_language_version` mapping
This would allow you to blanket choose an interpreter version across all hooks implemented in that language.

This is described in some detail [here](https://github.com/pre-commit/pre-commit/issues/644#issuecomment-339770094) though the rough idea would look something like:

```yaml
default_language_version:
    python: python2.7
    ruby: 2.1.5
```

This would give precedence to manually configured (either through `.pre-commit-hooks.yaml` or `.pre-commit-config.yaml`) versions and only overwrite in the case where `default` is not specified.  (precedence: manually specified > default_language_version > `language.get_default_version()`)

Pull request description:
Implement default_language_version
resolves #647 

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4fe852f..9ffdbe9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -27,7 +27,7 @@ repos:
     rev: v1.3.0
     hooks:
     -   id: reorder-python-imports
-        language_version: python2.7
+        language_version: python3
 -   repo: https://github.com/asottile/add-trailing-comma
     rev: v0.7.1
     hooks:
diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py
index c5b9947..d458dae 100644
--- a/pre_commit/clientlib.py
+++ b/pre_commit/clientlib.py
@@ -55,7 +55,7 @@ MANIFEST_HOOK_DICT = cfgv.Map(
     cfgv.Optional('always_run', cfgv.check_bool, False),
     cfgv.Optional('pass_filenames', cfgv.check_bool, True),
     cfgv.Optional('description', cfgv.check_string, ''),
-    cfgv.Optional('language_version', cfgv.check_string, 'default'),
+    cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT),
     cfgv.Optional('log_file', cfgv.check_string, ''),
     cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'),
     cfgv.Optional('require_serial', cfgv.check_bool, False),
@@ -90,8 +90,8 @@ def validate_manifest_main(argv=None):
     return ret
 
 
-_LOCAL = 'local'
-_META = 'meta'
+LOCAL = 'local'
+META = 'meta'
 
 
 class MigrateShaToRev(object):
@@ -100,12 +100,12 @@ class MigrateShaToRev(object):
         return cfgv.Conditional(
             key, cfgv.check_string,
             condition_key='repo',
-            condition_value=cfgv.NotIn(_LOCAL, _META),
+            condition_value=cfgv.NotIn(LOCAL, META),
             ensure_absent=True,
         )
 
     def check(self, dct):
-        if dct.get('repo') in {_LOCAL, _META}:
+        if dct.get('repo') in {LOCAL, META}:
             self._cond('rev').check(dct)
             self._cond('sha').check(dct)
         elif 'sha' in dct and 'rev' in dct:
@@ -159,12 +159,11 @@ _meta = (
 
 META_HOOK_DICT = cfgv.Map(
     'Hook', 'id',
+    cfgv.Required('id', cfgv.check_string),
+    cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))),
+    # language must be system
+    cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'),
     *([
-        cfgv.Required('id', cfgv.check_string),
-        cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))),
-        # language must be system
-        cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'),
-    ] + [
         # default to the hook definition for the meta hooks
         cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id)
         for hook_id, values in _meta
@@ -200,36 +199,36 @@ CONFIG_REPO_DICT = cfgv.Map(
 
     cfgv.ConditionalRecurse(
         'hooks', cfgv.Array(CONFIG_HOOK_DICT),
-        'repo', cfgv.NotIn(_LOCAL, _META),
+        'repo', cfgv.NotIn(LOCAL, META),
     ),
     cfgv.ConditionalRecurse(
         'hooks', cfgv.Array(MANIFEST_HOOK_DICT),
-        'repo', _LOCAL,
+        'repo', LOCAL,
     ),
     cfgv.ConditionalRecurse(
         'hooks', cfgv.Array(META_HOOK_DICT),
-        'repo', _META,
+        'repo', META,
     ),
 
     MigrateShaToRev(),
 )
+DEFAULT_LANGUAGE_VERSION = cfgv.Map(
+    'DefaultLanguageVersion', None,
+    cfgv.NoAdditionalKeys(all_languages),
+    *[cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages]
+)
 CONFIG_SCHEMA = cfgv.Map(
     'Config', None,
 
     cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)),
+    cfgv.OptionalRecurse(
+        'default_language_version', DEFAULT_LANGUAGE_VERSION, {},
+    ),
     cfgv.Optional('exclude', cfgv.check_regex, '^$'),
     cfgv.Optional('fail_fast', cfgv.check_bool, False),
 )
 
 
-def is_local_repo(repo_entry):
-    return repo_entry['repo'] == _LOCAL
-
-
-def is_meta_repo(repo_entry):
-    return repo_entry['repo'] == _META
-
-
 class InvalidConfigError(FatalError):
     pass
 
diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py
index f75a192..99e9605 100644
--- a/pre_commit/commands/autoupdate.py
+++ b/pre_commit/commands/autoupdate.py
@@ -13,10 +13,10 @@ import pre_commit.constants as C
 from pre_commit import output
 from pre_commit.clientlib import CONFIG_SCHEMA
 from pre_commit.clientlib import InvalidManifestError
-from pre_commit.clientlib import is_local_repo
-from pre_commit.clientlib import is_meta_repo
 from pre_commit.clientlib import load_config
 from pre_commit.clientlib import load_manifest
+from pre_commit.clientlib import LOCAL
+from pre_commit.clientlib import META
 from pre_commit.commands.migrate_config import migrate_config
 from pre_commit.util import CalledProcessError
 from pre_commit.util import cmd_output
@@ -123,8 +123,7 @@ def autoupdate(config_file, store, tags_only, repos=()):
 
     for repo_config in input_config['repos']:
         if (
-            is_local_repo(repo_config) or
-            is_meta_repo(repo_config) or
+            repo_config['repo'] in {LOCAL, META} or
             # Skip updating any repo_configs that aren't for the specified repo
             repos and repo_config['repo'] not in repos
         ):
diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py
index 9722643..65818e5 100644
--- a/pre_commit/commands/gc.py
+++ b/pre_commit/commands/gc.py
@@ -7,16 +7,16 @@ import pre_commit.constants as C
 from pre_commit import output
 from pre_commit.clientlib import InvalidConfigError
 from pre_commit.clientlib import InvalidManifestError
-from pre_commit.clientlib import is_local_repo
-from pre_commit.clientlib import is_meta_repo
 from pre_commit.clientlib import load_config
 from pre_commit.clientlib import load_manifest
+from pre_commit.clientlib import LOCAL
+from pre_commit.clientlib import META
 
 
 def _mark_used_repos(store, all_repos, unused_repos, repo):
-    if is_meta_repo(repo):
+    if repo['repo'] == META:
         return
-    elif is_local_repo(repo):
+    elif repo['repo'] == LOCAL:
         for hook in repo['hooks']:
             deps = hook.get('additional_dependencies')
             unused_repos.discard((
diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py
index a5df931..4ff2a41 100644
--- a/pre_commit/commands/install_uninstall.py
+++ b/pre_commit/commands/install_uninstall.py
@@ -6,6 +6,7 @@ import logging
 import os.path
 import sys
 
+import pre_commit.constants as C
 from pre_commit import git
 from pre_commit import output
 from pre_commit.clientlib import load_config
@@ -51,7 +52,7 @@ def shebang():
         py = 'python'
     else:
         py = python.get_default_version()
-        if py == 'default':
+        if py == C.DEFAULT:
             py = 'python'
     return '#!/usr/bin/env {}'.format(py)
 
diff --git a/pre_commit/constants.py b/pre_commit/constants.py
index a8cdc2e..996480a 100644
--- a/pre_commit/constants.py
+++ b/pre_commit/constants.py
@@ -22,3 +22,5 @@ VERSION = importlib_metadata.version('pre_commit')
 
 # `manual` is not invoked by any installed git hook.  See #719
 STAGES = ('commit', 'commit-msg', 'manual', 'push')
+
+DEFAULT = 'default'
diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py
index fecce47..6d85ddf 100644
--- a/pre_commit/languages/all.py
+++ b/pre_commit/languages/all.py
@@ -35,8 +35,7 @@ from pre_commit.languages import system
 #
 #     Args:
 #         prefix - `Prefix` bound to the repository.
-#         version - A version specified in the hook configuration or
-#             'default'.
+#         version - A version specified in the hook configuration or 'default'.
 #     """
 #
 # def run_hook(hook, file_args):
diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py
index e5f3a36..59a53b4 100644
--- a/pre_commit/languages/docker.py
+++ b/pre_commit/languages/docker.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
 import hashlib
 import os
 
+import pre_commit.constants as C
 from pre_commit import five
 from pre_commit.languages import helpers
 from pre_commit.util import CalledProcessError
@@ -62,7 +63,7 @@ def install_environment(
     assert_docker_available()
 
     directory = prefix.path(
-        helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
+        helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
     )
 
     # Docker doesn't really have relevant disk environment, but pre-commit
diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py
index 92d5d36..e19df88 100644
--- a/pre_commit/languages/golang.py
+++ b/pre_commit/languages/golang.py
@@ -4,6 +4,7 @@ import contextlib
 import os.path
 import sys
 
+import pre_commit.constants as C
 from pre_commit import git
 from pre_commit.envcontext import envcontext
 from pre_commit.envcontext import Var
@@ -27,7 +28,7 @@ def get_env_patch(venv):
 @contextlib.contextmanager
 def in_env(prefix):
     envdir = prefix.path(
-        helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
+        helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
     )
     with envcontext(get_env_patch(envdir)):
         yield
@@ -52,7 +53,7 @@ def guess_go_dir(remote_url):
 def install_environment(prefix, version, additional_dependencies):
     helpers.assert_version_default('golang', version)
     directory = prefix.path(
-        helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
+        helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
     )
 
     with clean_path_on_failure(directory):
diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py
index faff143..0915f41 100644
--- a/pre_commit/languages/helpers.py
+++ b/pre_commit/languages/helpers.py
@@ -7,10 +7,10 @@ import shlex
 
 import six
 
+import pre_commit.constants as C
 from pre_commit.util import cmd_output
 from pre_commit.xargs import xargs
 
-
 FIXED_RANDOM_SEED = 1542676186
 
 
@@ -30,7 +30,7 @@ def to_cmd(hook):
 
 
 def assert_version_default(binary, version):
-    if version != 'default':
+    if version != C.DEFAULT:
         raise AssertionError(
             'For now, pre-commit requires system-installed {}'.format(binary),
         )
@@ -45,7 +45,7 @@ def assert_no_additional_deps(lang, additional_deps):
 
 
 def basic_get_default_version():
-    return 'default'
+    return C.DEFAULT
 
 
 def basic_healthy(prefix, language_version):
diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py
index 07f785e..b313bf5 100644
--- a/pre_commit/languages/node.py
+++ b/pre_commit/languages/node.py
@@ -4,6 +4,7 @@ import contextlib
 import os
 import sys
 
+import pre_commit.constants as C
 from pre_commit.envcontext import envcontext
 from pre_commit.envcontext import Var
 from pre_commit.languages import helpers
@@ -57,7 +58,7 @@ def install_environment(prefix, version, additional_dependencies):
         cmd = [
             sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir,
         ]
-        if version != 'default':
+        if version != C.DEFAULT:
             cmd.extend(['-n', version])
         cmd_output(*cmd)
 
diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py
index fab5450..46aa059 100644
--- a/pre_commit/languages/python.py
+++ b/pre_commit/languages/python.py
@@ -4,6 +4,7 @@ import contextlib
 import os
 import sys
 
+import pre_commit.constants as C
 from pre_commit.envcontext import envcontext
 from pre_commit.envcontext import UNSET
 from pre_commit.envcontext import Var
@@ -76,7 +77,7 @@ def _get_default_version():  # pragma: no cover (platform dependent)
         return exe
 
     # We tried!
-    return 'default'
+    return C.DEFAULT
 
 
 def get_default_version():
@@ -134,7 +135,7 @@ def py_interface(_dir, _make_venv):
 
         env_dir = prefix.path(directory)
         with clean_path_on_failure(env_dir):
-            if version != 'default':
+            if version != C.DEFAULT:
                 python = norm_version(version)
             else:
                 python = os.path.realpath(sys.executable)
diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py
index 04a7415..c721b3c 100644
--- a/pre_commit/languages/ruby.py
+++ b/pre_commit/languages/ruby.py
@@ -6,6 +6,7 @@ import os.path
 import shutil
 import tarfile
 
+import pre_commit.constants as C
 from pre_commit.envcontext import envcontext
 from pre_commit.envcontext import Var
 from pre_commit.languages import helpers
@@ -32,7 +33,7 @@ def get_env_patch(venv, language_version):  # pragma: windows no cover
             ),
         ),
     )
-    if language_version != 'default':
+    if language_version != C.DEFAULT:
         patches += (('RBENV_VERSION', language_version),)
     return patches
 
@@ -52,14 +53,14 @@ def _extract_resource(filename, dest):
             tf.extractall(dest)
 
 
-def _install_rbenv(prefix, version='default'):  # pragma: windows no cover
+def _install_rbenv(prefix, version=C.DEFAULT):  # pragma: windows no cover
     directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
 
     _extract_resource('rbenv.tar.gz', prefix.path('.'))
     shutil.move(prefix.path('rbenv'), prefix.path(directory))
 
     # Only install ruby-build if the version is specified
-    if version != 'default':
+    if version != C.DEFAULT:
         plugins_dir = prefix.path(directory, 'plugins')
         _extract_resource('ruby-download.tar.gz', plugins_dir)
         _extract_resource('ruby-build.tar.gz', plugins_dir)
@@ -84,7 +85,7 @@ def _install_rbenv(prefix, version='default'):  # pragma: windows no cover
         )
 
         # If we aren't using the system ruby, add a version here
-        if version != 'default':
+        if version != C.DEFAULT:
             activate_file.write('export RBENV_VERSION="{}"\n'.format(version))
 
 
@@ -109,7 +110,7 @@ def install_environment(
             # Need to call this before installing so rbenv's directories are
             # set up
             helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-'))
-            if version != 'default':
+            if version != C.DEFAULT:
                 _install_ruby(prefix, version)
             # Need to call this after installing to set up the shims
             helpers.run_setup_cmd(prefix, ('rbenv', 'rehash'))
diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py
index e81fbad..e09d007 100644
--- a/pre_commit/languages/rust.py
+++ b/pre_commit/languages/rust.py
@@ -5,6 +5,7 @@ import os.path
 
 import toml
 
+import pre_commit.constants as C
 from pre_commit.envcontext import envcontext
 from pre_commit.envcontext import Var
 from pre_commit.languages import helpers
@@ -29,7 +30,7 @@ def get_env_patch(target_dir):
 @contextlib.contextmanager
 def in_env(prefix):
     target_dir = prefix.path(
-        helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
+        helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
     )
     with envcontext(get_env_patch(target_dir)):
         yield
@@ -50,7 +51,7 @@ def _add_dependencies(cargo_toml_path, additional_dependencies):
 def install_environment(prefix, version, additional_dependencies):
     helpers.assert_version_default('rust', version)
     directory = prefix.path(
-        helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
+        helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
     )
 
     # There are two cases where we might want to specify more dependencies:
diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py
index 5841f25..3f5a92f 100644
--- a/pre_commit/languages/swift.py
+++ b/pre_commit/languages/swift.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
 import contextlib
 import os
 
+import pre_commit.constants as C
 from pre_commit.envcontext import envcontext
 from pre_commit.envcontext import Var
 from pre_commit.languages import helpers
@@ -24,7 +25,7 @@ def get_env_patch(venv):  # pragma: windows no cover
 @contextlib.contextmanager
 def in_env(prefix):  # pragma: windows no cover
     envdir = prefix.path(
-        helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
+        helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
     )
     with envcontext(get_env_patch(envdir)):
         yield
@@ -36,7 +37,7 @@ def install_environment(
     helpers.assert_version_default('swift', version)
     helpers.assert_no_additional_deps('swift', additional_dependencies)
     directory = prefix.path(
-        helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
+        helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
     )
 
     # Build the swift package
diff --git a/pre_commit/repository.py b/pre_commit/repository.py
index a654d08..76001fa 100644
--- a/pre_commit/repository.py
+++ b/pre_commit/repository.py
@@ -8,10 +8,10 @@ import os
 
 import pre_commit.constants as C
 from pre_commit import five
-from pre_commit.clientlib import is_local_repo
-from pre_commit.clientlib import is_meta_repo
 from pre_commit.clientlib import load_manifest
+from pre_commit.clientlib import LOCAL
 from pre_commit.clientlib import MANIFEST_HOOK_DICT
+from pre_commit.clientlib import META
 from pre_commit.languages.all import languages
 from pre_commit.languages.helpers import environment_dir
 from pre_commit.prefix import Prefix
@@ -111,7 +111,9 @@ class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)):
         return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS})
 
 
-def _hook(*hook_dicts):
+def _hook(*hook_dicts, **kwargs):
+    root_config = kwargs.pop('root_config')
+    assert not kwargs, kwargs
     ret, rest = dict(hook_dicts[0]), hook_dicts[1:]
     for dct in rest:
         ret.update(dct)
@@ -127,14 +129,16 @@ def _hook(*hook_dicts):
         )
         exit(1)
 
-    if ret['language_version'] == 'default':
-        language = languages[ret['language']]
-        ret['language_version'] = language.get_default_version()
+    lang = ret['language']
+    if ret['language_version'] == C.DEFAULT:
+        ret['language_version'] = root_config['default_language_version'][lang]
+    if ret['language_version'] == C.DEFAULT:
+        ret['language_version'] = languages[lang].get_default_version()
 
     return ret
 
 
-def _non_cloned_repository_hooks(repo_config, store):
+def _non_cloned_repository_hooks(repo_config, store, root_config):
     def _prefix(language_name, deps):
         language = languages[language_name]
         # pcre / pygrep / script / system / docker_image do not have
@@ -148,13 +152,13 @@ def _non_cloned_repository_hooks(repo_config, store):
         Hook.create(
             repo_config['repo'],
             _prefix(hook['language'], hook['additional_dependencies']),
-            _hook(hook),
+            _hook(hook, root_config=root_config),
         )
         for hook in repo_config['hooks']
     )
 
 
-def _cloned_repository_hooks(repo_config, store):
+def _cloned_repository_hooks(repo_config, store, root_config):
     repo, rev = repo_config['repo'], repo_config['rev']
     manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE)
     by_id = {hook['id']: hook for hook in load_manifest(manifest_path)}
@@ -169,7 +173,10 @@ def _cloned_repository_hooks(repo_config, store):
             )
             exit(1)
 
-    hook_dcts = [_hook(by_id[h['id']], h) for h in repo_config['hooks']]
+    hook_dcts = [
+        _hook(by_id[hook['id']], hook, root_config=root_config)
+        for hook in repo_config['hooks']
+    ]
     return tuple(
         Hook.create(
             repo_config['repo'],
@@ -180,11 +187,11 @@ def _cloned_repository_hooks(repo_config, store):
     )
 
 
-def repository_hooks(repo_config, store):
-    if is_local_repo(repo_config) or is_meta_repo(repo_config):
-        return _non_cloned_repository_hooks(repo_config, store)
+def _repository_hooks(repo_config, store, root_config):
+    if repo_config['repo'] in {LOCAL, META}:
+        return _non_cloned_repository_hooks(repo_config, store, root_config)
     else:
-        return _cloned_repository_hooks(repo_config, store)
+        return _cloned_repository_hooks(repo_config, store, root_config)
 
 
 def install_hook_envs(hooks, store):
@@ -201,17 +208,13 @@ def install_hook_envs(hooks, store):
         return
     with store.exclusive_lock():
         # Another process may have already completed this work
-        need_installed = _need_installed()
-        if not need_installed:  # pragma: no cover (race)
-            return
-
-        for hook in need_installed:
+        for hook in _need_installed():
             hook.install()
 
 
-def all_hooks(config, store):
+def all_hooks(root_config, store):
     return tuple(
         hook
-        for repo in config['repos']
-        for hook in repository_hooks(repo, store)
+        for repo in root_config['repos']
+        for hook in _repository_hooks(repo, store, root_config)
     )
diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py
index 3b4a25f..e2686f0 100644
--- a/pre_commit/xargs.py
+++ b/pre_commit/xargs.py
@@ -2,11 +2,11 @@ from __future__ import absolute_import
 from __future__ import division
 from __future__ import unicode_literals
 
+import concurrent.futures
 import contextlib
 import math
 import sys
 
-import concurrent.futures
 import six
 
 from pre_commit import parse_shebang


Test patch:
diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py
index 1f691c2..fd7f051 100644
--- a/tests/clientlib_test.py
+++ b/tests/clientlib_test.py
@@ -7,7 +7,7 @@ from pre_commit.clientlib import check_type_tag
 from pre_commit.clientlib import CONFIG_HOOK_DICT
 from pre_commit.clientlib import CONFIG_REPO_DICT
 from pre_commit.clientlib import CONFIG_SCHEMA
-from pre_commit.clientlib import is_local_repo
+from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION
 from pre_commit.clientlib import MANIFEST_SCHEMA
 from pre_commit.clientlib import MigrateShaToRev
 from pre_commit.clientlib import validate_config_main
@@ -30,10 +30,6 @@ def test_check_type_tag_failures(value):
         check_type_tag(value)
 
 
-def test_is_local_repo():
-    assert is_local_repo({'repo': 'local'})
-
-
 @pytest.mark.parametrize(
     ('args', 'expected_output'),
     (
@@ -250,6 +246,20 @@ def test_migrate_to_sha_ok():
         {'repo': 'meta', 'hooks': [{'id': 'identity', 'name': False}]},
     ),
 )
-def test_meta_hook_invalid_id(config_repo):
+def test_meta_hook_invalid(config_repo):
     with pytest.raises(cfgv.ValidationError):
         cfgv.validate(config_repo, CONFIG_REPO_DICT)
+
+
+@pytest.mark.parametrize(
+    'mapping',
+    (
+        # invalid language key
+        {'pony': '1.0'},
+        # not a string for version
+        {'python': 3},
+    ),
+)
+def test_default_language_version_invalid(mapping):
+    with pytest.raises(cfgv.ValidationError):
+        cfgv.validate(mapping, DEFAULT_LANGUAGE_VERSION)
diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py
index 2a01850..d252850 100644
--- a/tests/commands/gc_test.py
+++ b/tests/commands/gc_test.py
@@ -110,10 +110,10 @@ def test_gc_config_with_missing_hook(
     path = make_repo(tempdir_factory, 'script_hooks_repo')
     write_config('.', make_config_from_repo(path))
     store.mark_config_used(C.CONFIG_FILE)
+    # to trigger a clone
+    all_hooks(load_config(C.CONFIG_FILE), store)
 
     with modify_config() as config:
-        # just to trigger a clone
-        all_hooks(config, store)
         # add a hook which does not exist, make sure we don't crash
         config['repos'][0]['hooks'].append({'id': 'does-not-exist'})
 
diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py
index 2faa191..608fe38 100644
--- a/tests/commands/install_uninstall_test.py
+++ b/tests/commands/install_uninstall_test.py
@@ -53,13 +53,13 @@ def test_shebang_windows():
 
 def test_shebang_otherwise():
     with mock.patch.object(sys, 'platform', 'posix'):
-        assert 'default' not in shebang()
+        assert C.DEFAULT not in shebang()
 
 
 def test_shebang_returns_default():
     with mock.patch.object(sys, 'platform', 'posix'):
         with mock.patch.object(
-            python, 'get_default_version', return_value='default',
+            python, 'get_default_version', return_value=C.DEFAULT,
         ):
             assert shebang() == '#!/usr/bin/env python'
 
diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py
index 831e0d5..629322c 100644
--- a/tests/languages/helpers_test.py
+++ b/tests/languages/helpers_test.py
@@ -8,6 +8,7 @@ import sys
 import mock
 import pytest
 
+import pre_commit.constants as C
 from pre_commit.languages import helpers
 from pre_commit.prefix import Prefix
 from pre_commit.util import CalledProcessError
@@ -15,7 +16,7 @@ from testing.auto_namedtuple import auto_namedtuple
 
 
 def test_basic_get_default_version():
-    assert helpers.basic_get_default_version() == 'default'
+    assert helpers.basic_get_default_version() == C.DEFAULT
 
 
 def test_basic_healthy():
diff --git a/tests/repository_test.py b/tests/repository_test.py
index 25fe244..d237da2 100644
--- a/tests/repository_test.py
+++ b/tests/repository_test.py
@@ -12,7 +12,7 @@ import pytest
 import pre_commit.constants as C
 from pre_commit import five
 from pre_commit import parse_shebang
-from pre_commit.clientlib import CONFIG_REPO_DICT
+from pre_commit.clientlib import CONFIG_SCHEMA
 from pre_commit.clientlib import load_manifest
 from pre_commit.languages import golang
 from pre_commit.languages import helpers
@@ -22,9 +22,9 @@ from pre_commit.languages import python
 from pre_commit.languages import ruby
 from pre_commit.languages import rust
 from pre_commit.prefix import Prefix
+from pre_commit.repository import all_hooks
 from pre_commit.repository import Hook
 from pre_commit.repository import install_hook_envs
-from pre_commit.repository import repository_hooks
 from pre_commit.util import cmd_output
 from testing.fixtures import make_config_from_repo
 from testing.fixtures import make_repo
@@ -43,15 +43,21 @@ def _norm_out(b):
     return b.replace(b'\r\n', b'\n')
 
 
-def _get_hook(config, store, hook_id):
-    config = cfgv.validate(config, CONFIG_REPO_DICT)
-    config = cfgv.apply_defaults(config, CONFIG_REPO_DICT)
-    hooks = repository_hooks(config, store)
-    install_hook_envs(hooks, store)
+def _get_hook_no_install(repo_config, store, hook_id):
+    config = {'repos': [repo_config]}
+    config = cfgv.validate(config, CONFIG_SCHEMA)
+    config = cfgv.apply_defaults(config, CONFIG_SCHEMA)
+    hooks = all_hooks(config, store)
     hook, = [hook for hook in hooks if hook.id == hook_id]
     return hook
 
 
+def _get_hook(repo_config, store, hook_id):
+    hook = _get_hook_no_install(repo_config, store, hook_id)
+    install_hook_envs([hook], store)
+    return hook
+
+
 def _test_hook_repo(
         tempdir_factory,
         store,
@@ -81,7 +87,7 @@ def test_python_hook_default_version(tempdir_factory, store):
     # make sure that this continues to work for platforms where default
     # language detection does not work
     with mock.patch.object(
-            python, 'get_default_version', return_value='default',
+            python, 'get_default_version', return_value=C.DEFAULT,
     ):
         test_python_hook(tempdir_factory, store)
 
@@ -278,7 +284,7 @@ def test_additional_rust_cli_dependencies_installed(
     config['hooks'][0]['additional_dependencies'] = [dep]
     hook = _get_hook(config, store, 'rust-hook')
     binaries = os.listdir(hook.prefix.path(
-        helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin',
+        helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin',
     ))
     # normalize for windows
     binaries = [os.path.splitext(binary)[0] for binary in binaries]
@@ -295,7 +301,7 @@ def test_additional_rust_lib_dependencies_installed(
     config['hooks'][0]['additional_dependencies'] = deps
     hook = _get_hook(config, store, 'rust-hook')
     binaries = os.listdir(hook.prefix.path(
-        helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin',
+        helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin',
     ))
     # normalize for windows
     binaries = [os.path.splitext(binary)[0] for binary in binaries]
@@ -494,7 +500,7 @@ def test_additional_golang_dependencies_installed(
     config['hooks'][0]['additional_dependencies'] = deps
     hook = _get_hook(config, store, 'golang-hook')
     binaries = os.listdir(hook.prefix.path(
-        helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin',
+        helpers.environment_dir(golang.ENVIRONMENT_DIR, C.DEFAULT), 'bin',
     ))
     # normalize for windows
     binaries = [os.path.splitext(binary)[0] for binary in binaries]
@@ -588,7 +594,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store):
     """Regression test for #186."""
     path = make_repo(tempdir_factory, 'python_hooks_repo')
     config = make_config_from_repo(path)
-    hooks = repository_hooks(config, store)
+    hooks = [_get_hook_no_install(config, store, 'foo')]
 
     class MyKeyboardInterrupt(KeyboardInterrupt):
         pass
@@ -686,22 +692,42 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store):
     assert ret2[1] == b'bar\nHello World\n'
 
 
-def test_local_python_repo(store):
+@pytest.fixture
+def local_python_config():
     # Make a "local" hooks repo that just installs our other hooks repo
     repo_path = get_resource_path('python_hooks_repo')
     manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE))
     hooks = [
         dict(hook, additional_dependencies=[repo_path]) for hook in manifest
     ]
-    config = {'repo': 'local', 'hooks': hooks}
-    hook = _get_hook(config, store, 'foo')
+    return {'repo': 'local', 'hooks': hooks}
+
+
+def test_local_python_repo(store, local_python_config):
+    hook = _get_hook(local_python_config, store, 'foo')
     # language_version should have been adjusted to the interpreter version
-    assert hook.language_version != 'default'
+    assert hook.language_version != C.DEFAULT
     ret = hook.run(('filename',))
     assert ret[0] == 0
     assert _norm_out(ret[1]) == b"['filename']\nHello World\n"
 
 
+def test_default_language_version(store, local_python_config):
+    config = {
+        'default_language_version': {'python': 'fake'},
+        'repos': [local_python_config],
+    }
+
+    # `language_version` was not set, should default
+    hook, = all_hooks(config, store)
+    assert hook.language_version == 'fake'
+
+    # `language_version` is set, should not default
+    config['repos'][0]['hooks'][0]['language_version'] = 'fake2'
+    hook, = all_hooks(config, store)
+    assert hook.language_version == 'fake2'
+
+
 def test_hook_id_not_present(tempdir_factory, store, fake_log_handler):
     path = make_repo(tempdir_factory, 'script_hooks_repo')
     config = make_config_from_repo(path)
@@ -760,7 +786,7 @@ def test_manifest_hooks(tempdir_factory, store):
         files='',
         id='bash_hook',
         language='script',
-        language_version='default',
+        language_version=C.DEFAULT,
         log_file='',
         minimum_pre_commit_version='0',
         name='Bash hook',
diff --git a/tests/xargs_test.py b/tests/xargs_test.py
index ed65ed4..0e91f9b 100644
--- a/tests/xargs_test.py
+++ b/tests/xargs_test.py
@@ -2,10 +2,10 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
+import concurrent.futures
 import sys
 import time
 
-import concurrent.futures
 import mock
 import pytest
 import six