diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index f441ddd2..5546025d 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -10,16 +10,18 @@ from pre_commit.languages import script from pre_commit.languages import swift from pre_commit.languages import system -# A language implements the following constant and two functions in its module: +# A language implements the following constant and functions in its module: # # # Use None for no environment # ENVIRONMENT_DIR = 'foo_env' # -# def install_environment( -# repo_cmd_runner, -# version='default', -# additional_dependencies=(), -# ): +# def get_default_version(): +# """Return a value to replace the 'default' value for language_version. +# +# return 'default' if there is no better option. +# """ +# +# def install_environment(repo_cmd_runner, version, additional_dependencies): # """Installs a repository in the given repository. Note that the current # working directory will already be inside the repository. # diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 7d3f8d04..59dc1b41 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -14,6 +14,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' +get_default_version = helpers.basic_get_default_version def md5(s): # pragma: windows no cover @@ -55,9 +56,7 @@ def build_docker_image(repo_cmd_runner, **kwargs): # pragma: windows no cover def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), + repo_cmd_runner, version, additional_dependencies, ): # pragma: windows no cover assert repo_cmd_runner.exists('Dockerfile'), ( 'No Dockerfile was found in the hook repository' diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index c0bfbcbc..ee04ca79 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -14,6 +14,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'golangenv' +get_default_version = helpers.basic_get_default_version def get_env_patch(venv): @@ -44,11 +45,7 @@ def guess_go_dir(remote_url): return 'unknown_src_dir' -def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), -): +def install_environment(repo_cmd_runner, version, additional_dependencies): helpers.assert_version_default('golang', version) directory = repo_cmd_runner.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index a6c93de1..6af77e30 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -33,3 +33,7 @@ def assert_no_additional_deps(lang, additional_deps): 'For now, pre-commit does not support ' 'additional_dependencies for {}'.format(lang), ) + + +def basic_get_default_version(): + return 'default' diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index ef557a16..b5f7c56e 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -12,6 +12,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'node_env' +get_default_version = helpers.basic_get_default_version def get_env_patch(venv): # pragma: windows no cover @@ -34,9 +35,7 @@ def in_env(repo_cmd_runner, language_version): # pragma: windows no cover def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), + repo_cmd_runner, version, additional_dependencies, ): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) assert repo_cmd_runner.exists('package.json') diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 314ea090..faba5395 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -2,18 +2,16 @@ from __future__ import unicode_literals import sys +from pre_commit.languages import helpers from pre_commit.xargs import xargs ENVIRONMENT_DIR = None GREP = 'ggrep' if sys.platform == 'darwin' else 'grep' +get_default_version = helpers.basic_get_default_version -def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), -): +def install_environment(repo_cmd_runner, version, additional_dependencies): """Installation for pcre type is a noop.""" raise AssertionError('Cannot install pcre repo.') diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 634abe58..715d585f 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import contextlib -import distutils.spawn import os import sys @@ -9,11 +8,13 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.parse_shebang import find_executable from pre_commit.util import clean_path_on_failure from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'py_env' +get_default_version = helpers.basic_get_default_version def bin_dir(venv): @@ -39,10 +40,53 @@ def in_env(repo_cmd_runner, language_version): yield +def _get_default_version(): # pragma: no cover (platform dependent) + def _norm(path): + _, exe = os.path.split(path.lower()) + exe, _, _ = exe.partition('.exe') + if find_executable(exe) and exe not in {'python', 'pythonw'}: + return exe + + # First attempt from `sys.executable` (or the realpath) + # On linux, I see these common sys.executables: + # + # system `python`: /usr/bin/python -> python2.7 + # system `python2`: /usr/bin/python2 -> python2.7 + # virtualenv v: v/bin/python (will not return from this loop) + # virtualenv v -ppython2: v/bin/python -> python2 + # virtualenv v -ppython2.7: v/bin/python -> python2.7 + # virtualenv v -ppypy: v/bin/python -> v/bin/pypy + for path in {sys.executable, os.path.realpath(sys.executable)}: + exe = _norm(path) + if exe: + return exe + + # Next try the `pythonX.X` executable + exe = 'python{}.{}'.format(*sys.version_info) + if find_executable(exe): + return exe + + # Give a best-effort try for windows + if os.path.exists(r'C:\{}\python.exe'.format(exe.replace('.', ''))): + return exe + + # We tried! + return 'default' + + +def get_default_version(): + # TODO: when dropping python2, use `functools.lru_cache(maxsize=1)` + try: + return get_default_version.cached_version + except AttributeError: + get_default_version.cached_version = _get_default_version() + return get_default_version() + + def norm_version(version): if os.name == 'nt': # pragma: no cover (windows) # Try looking up by name - if distutils.spawn.find_executable(version): + if find_executable(version) and find_executable(version) != version: return version # If it is in the form pythonx.x search in the default @@ -54,11 +98,7 @@ def norm_version(version): return os.path.expanduser(version) -def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), -): +def install_environment(repo_cmd_runner, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index d3896d90..26e303c3 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -16,6 +16,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'rbenv' +get_default_version = helpers.basic_get_default_version def get_env_patch(venv, language_version): # pragma: windows no cover @@ -97,9 +98,7 @@ def _install_ruby(runner, version): # pragma: windows no cover def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), + repo_cmd_runner, version, additional_dependencies, ): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 762ae763..c4b6593d 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -5,13 +5,10 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version -def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), -): +def install_environment(repo_cmd_runner, version, additional_dependencies): """Installation for script type is a noop.""" raise AssertionError('Cannot install script repo.') diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 4d171c5b..a27dfac2 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -10,6 +10,7 @@ from pre_commit.util import clean_path_on_failure from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'swift_env' +get_default_version = helpers.basic_get_default_version BUILD_DIR = '.build' BUILD_CONFIG = 'release' @@ -29,9 +30,7 @@ def in_env(repo_cmd_runner): # pragma: windows no cover def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), + repo_cmd_runner, version, additional_dependencies, ): # pragma: windows no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index c9e1c5dc..31480792 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -5,13 +5,10 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version -def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), -): +def install_environment(repo_cmd_runner, version, additional_dependencies): """Installation for system type is a noop.""" raise AssertionError('Cannot install system repo.') diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 888ad6dd..081f3c60 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -7,6 +7,7 @@ from cached_property import cached_property import pre_commit.constants as C from pre_commit.clientlib import load_manifest +from pre_commit.languages.all import languages logger = logging.getLogger('pre_commit') @@ -38,4 +39,10 @@ class Manifest(object): @cached_property def hooks(self): - return {hook['id']: hook for hook in self.manifest_contents} + ret = {} + for hook in self.manifest_contents: + if hook['language_version'] == 'default': + language = languages[hook['language']] + hook['language_version'] = language.get_default_version() + ret[hook['id']] = hook + return ret diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 73b89cb5..dd1ed27b 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -12,9 +12,7 @@ from pre_commit.languages.all import languages def test_install_environment_argspec(language): expected_argspec = inspect.ArgSpec( args=['repo_cmd_runner', 'version', 'additional_dependencies'], - varargs=None, - keywords=None, - defaults=('default', ()), + varargs=None, keywords=None, defaults=None, ) argspec = inspect.getargspec(languages[language].install_environment) assert argspec == expected_argspec @@ -33,3 +31,12 @@ def test_run_hook_argpsec(language): ) argspec = inspect.getargspec(languages[language].run_hook) assert argspec == expected_argspec + + +@pytest.mark.parametrize('language', all_languages) +def test_get_default_version_argspec(language): + expected_argspec = inspect.ArgSpec( + args=[], varargs=None, keywords=None, defaults=None, + ) + argspec = inspect.getargspec(languages[language].get_default_version) + assert argspec == expected_argspec diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 7db886c5..ada004fc 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -11,8 +11,7 @@ from testing.util import get_head_sha @pytest.yield_fixture def manifest(store, tempdir_factory): path = make_repo(tempdir_factory, 'script_hooks_repo') - head_sha = get_head_sha(path) - repo_path = store.clone(path, head_sha) + repo_path = store.clone(path, get_head_sha(path)) yield Manifest(repo_path, path) @@ -76,3 +75,13 @@ def test_legacy_manifest_warn(store, tempdir_factory, log_warning_mock): 'If `pre-commit autoupdate` does not silence this warning consider ' 'making an issue / pull request.'.format(path) ) + + +def test_default_python_language_version(store, tempdir_factory): + path = make_repo(tempdir_factory, 'python_hooks_repo') + repo_path = store.clone(path, get_head_sha(path)) + manifest = Manifest(repo_path, path) + + # This assertion is difficult as it is version dependent, just assert + # that it is *something* + assert manifest.hooks['foo']['language_version'] != 'default' diff --git a/tests/repository_test.py b/tests/repository_test.py index f91642ee..7131d75b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -442,7 +442,7 @@ def test_venvs(tempdir_factory, store): config = make_config_from_repo(path) repo = Repository.create(config, store) venv, = repo._venvs - assert venv == (mock.ANY, 'python', 'default', []) + assert venv == (mock.ANY, 'python', python.get_default_version(), []) @pytest.mark.integration @@ -452,7 +452,7 @@ def test_additional_dependencies(tempdir_factory, store): config['hooks'][0]['additional_dependencies'] = ['pep8'] repo = Repository.create(config, store) venv, = repo._venvs - assert venv == (mock.ANY, 'python', 'default', ['pep8']) + assert venv == (mock.ANY, 'python', python.get_default_version(), ['pep8']) @pytest.mark.integration @@ -591,7 +591,8 @@ def test_control_c_control_c_on_install(tempdir_factory, store): repo.run_hook(hook, []) # Should have made an environment, however this environment is broken! - assert os.path.exists(repo._cmd_runner.path('py_env-default')) + envdir = 'py_env-{}'.format(python.get_default_version()) + assert repo._cmd_runner.exists(envdir) # However, it should be perfectly runnable (reinstall after botched # install)