Merge pull request #489 from pre-commit/local_language_hooks

Local language hooks
This commit is contained in:
Anthony Sottile 2017-02-16 12:51:57 -05:00 committed by GitHub
commit 0478c6008c
15 changed files with 240 additions and 221 deletions

View file

@ -32,7 +32,7 @@ def _update_repo(repo_config, runner, tags_only):
""" """
repo = Repository.create(repo_config, runner.store) repo = Repository.create(repo_config, runner.store)
with cwd(repo.repo_path_getter.repo_path): with cwd(repo._repo_path):
cmd_output('git', 'fetch') cmd_output('git', 'fetch')
tag_cmd = ('git', 'describe', 'origin/master', '--tags') tag_cmd = ('git', 'describe', 'origin/master', '--tags')
if tags_only: if tags_only:

View file

@ -13,15 +13,14 @@ logger = logging.getLogger('pre_commit')
class Manifest(object): class Manifest(object):
def __init__(self, repo_path_getter, repo_url): def __init__(self, repo_path, repo_url):
self.repo_path_getter = repo_path_getter self.repo_path = repo_path
self.repo_url = repo_url self.repo_url = repo_url
@cached_property @cached_property
def manifest_contents(self): def manifest_contents(self):
repo_path = self.repo_path_getter.repo_path default_path = os.path.join(self.repo_path, C.MANIFEST_FILE)
default_path = os.path.join(repo_path, C.MANIFEST_FILE) legacy_path = os.path.join(self.repo_path, C.MANIFEST_FILE_LEGACY)
legacy_path = os.path.join(repo_path, C.MANIFEST_FILE_LEGACY)
if os.path.exists(legacy_path) and not os.path.exists(default_path): if os.path.exists(legacy_path) and not os.path.exists(default_path):
logger.warning( logger.warning(
'{} uses legacy {} to provide hooks.\n' '{} uses legacy {} to provide hooks.\n'

View file

@ -31,41 +31,99 @@ _pre_commit_version = pkg_resources.parse_version(
INSTALLED_STATE_VERSION = '1' INSTALLED_STATE_VERSION = '1'
def _state(additional_deps):
return {'additional_dependencies': sorted(additional_deps)}
def _state_filename(cmd_runner, venv):
return cmd_runner.path(venv, '.install_state_v' + INSTALLED_STATE_VERSION)
def _read_installed_state(cmd_runner, venv):
filename = _state_filename(cmd_runner, venv)
if not os.path.exists(filename):
return None
else:
return json.loads(io.open(filename).read())
def _write_installed_state(cmd_runner, venv, state):
state_filename = _state_filename(cmd_runner, venv)
staging = state_filename + 'staging'
with io.open(staging, 'w') as state_file:
state_file.write(five.to_text(json.dumps(state)))
# Move the file into place atomically to indicate we've installed
os.rename(staging, state_filename)
def _installed(cmd_runner, language_name, language_version, additional_deps):
language = languages[language_name]
venv = environment_dir(language.ENVIRONMENT_DIR, language_version)
return (
venv is None or
_read_installed_state(cmd_runner, venv) == _state(additional_deps)
)
def _install_all(venvs, repo_url):
"""Tuple of (cmd_runner, language, version, deps)"""
need_installed = tuple(
(cmd_runner, language_name, version, deps)
for cmd_runner, language_name, version, deps in venvs
if not _installed(cmd_runner, language_name, version, deps)
)
if need_installed:
logger.info(
'Installing environment for {}.'.format(repo_url)
)
logger.info('Once installed this environment will be reused.')
logger.info('This may take a few minutes...')
for cmd_runner, language_name, version, deps in need_installed:
language = languages[language_name]
venv = environment_dir(language.ENVIRONMENT_DIR, version)
# There's potentially incomplete cleanup from previous runs
# Clean it up!
if cmd_runner.exists(venv):
shutil.rmtree(cmd_runner.path(venv))
language.install_environment(cmd_runner, version, deps)
# Write our state to indicate we're installed
state = _state(deps)
_write_installed_state(cmd_runner, venv, state)
class Repository(object): class Repository(object):
def __init__(self, repo_config, repo_path_getter): def __init__(self, repo_config, store):
self.repo_config = repo_config self.repo_config = repo_config
self.repo_path_getter = repo_path_getter self.store = store
self.__installed = False self.__installed = False
@classmethod @classmethod
def create(cls, config, store): def create(cls, config, store):
if is_local_hooks(config): if is_local_hooks(config):
return LocalRepository(config) return LocalRepository(config, store)
else: else:
repo_path_getter = store.get_repo_path_getter( return cls(config, store)
config['repo'], config['sha']
)
return cls(config, repo_path_getter)
@cached_property @cached_property
def repo_url(self): def _repo_path(self):
return self.repo_config['repo'] return self.store.clone(
self.repo_config['repo'], self.repo_config['sha'],
)
@cached_property @cached_property
def languages(self): def _cmd_runner(self):
return { return PrefixedCommandRunner(self._repo_path)
(hook['language'], hook['language_version'])
for _, hook in self.hooks def _cmd_runner_from_deps(self, language_name, deps):
} return self._cmd_runner
@cached_property @cached_property
def additional_dependencies(self): def manifest(self):
dep_dict = defaultdict(lambda: defaultdict(_UniqueList)) return Manifest(self._repo_path, self.repo_config['repo'])
for _, hook in self.hooks:
dep_dict[hook['language']][hook['language_version']].update(
hook.get('additional_dependencies', []),
)
return dep_dict
@cached_property @cached_property
def hooks(self): def hooks(self):
@ -97,108 +155,49 @@ class Repository(object):
) )
@cached_property @cached_property
def manifest(self): def _venvs(self):
return Manifest(self.repo_path_getter, self.repo_url) deps_dict = defaultdict(_UniqueList)
for _, hook in self.hooks:
@cached_property deps_dict[(hook['language'], hook['language_version'])].update(
def cmd_runner(self): hook.get('additional_dependencies', []),
return PrefixedCommandRunner(self.repo_path_getter.repo_path) )
ret = []
for (language, version), deps in deps_dict.items():
ret.append((self._cmd_runner, language, version, deps))
return tuple(ret)
def require_installed(self): def require_installed(self):
if self.__installed: if not self.__installed:
return _install_all(self._venvs, self.repo_config['repo'])
self.__installed = True
self.install()
self.__installed = True
def install(self):
"""Install the hook repository."""
def state(language_name, language_version):
return {
'additional_dependencies': sorted(
self.additional_dependencies[
language_name
][language_version],
)
}
def state_filename(venv, suffix=''):
return self.cmd_runner.path(
venv, '.install_state_v' + INSTALLED_STATE_VERSION + suffix,
)
def read_state(venv):
if not os.path.exists(state_filename(venv)):
return None
else:
return json.loads(io.open(state_filename(venv)).read())
def write_state(venv, language_name, language_version):
with io.open(
state_filename(venv, suffix='staging'), 'w',
) as state_file:
state_file.write(five.to_text(json.dumps(
state(language_name, language_version),
)))
# Move the file into place atomically to indicate we've installed
os.rename(
state_filename(venv, suffix='staging'),
state_filename(venv),
)
def language_is_installed(language_name, language_version):
language = languages[language_name]
venv = environment_dir(language.ENVIRONMENT_DIR, language_version)
return (
venv is None or
read_state(venv) == state(language_name, language_version)
)
if not all(
language_is_installed(language_name, language_version)
for language_name, language_version in self.languages
):
logger.info(
'Installing environment for {}.'.format(self.repo_url)
)
logger.info('Once installed this environment will be reused.')
logger.info('This may take a few minutes...')
for language_name, language_version in self.languages:
if language_is_installed(language_name, language_version):
continue
language = languages[language_name]
venv = environment_dir(language.ENVIRONMENT_DIR, language_version)
# There's potentially incomplete cleanup from previous runs
# Clean it up!
if self.cmd_runner.exists(venv):
shutil.rmtree(self.cmd_runner.path(venv))
language.install_environment(
self.cmd_runner, language_version,
self.additional_dependencies[language_name][language_version],
)
# Write our state to indicate we're installed
write_state(venv, language_name, language_version)
def run_hook(self, hook, file_args): def run_hook(self, hook, file_args):
"""Run a hook. """Run a hook.
Args: :param dict hook:
hook - Hook dictionary :param tuple file_args: all the files to run the hook on
file_args - List of files to run
""" """
self.require_installed() self.require_installed()
return languages[hook['language']].run_hook( language_name = hook['language']
self.cmd_runner, hook, file_args, deps = hook.get('additional_dependencies', [])
) cmd_runner = self._cmd_runner_from_deps(language_name, deps)
return languages[language_name].run_hook(cmd_runner, hook, file_args)
class LocalRepository(Repository): class LocalRepository(Repository):
def __init__(self, repo_config): def _cmd_runner_from_deps(self, language_name, deps):
super(LocalRepository, self).__init__(repo_config, None) """local repositories have a cmd runner per hook"""
language = languages[language_name]
# pcre / script / system do not have environments so they work out
# of the current directory
if language.ENVIRONMENT_DIR is None:
return PrefixedCommandRunner(git.get_root())
else:
return PrefixedCommandRunner(self.store.make_local(deps))
@cached_property
def manifest(self):
raise NotImplementedError
@cached_property @cached_property
def hooks(self): def hooks(self):
@ -208,12 +207,17 @@ class LocalRepository(Repository):
) )
@cached_property @cached_property
def cmd_runner(self): def _venvs(self):
return PrefixedCommandRunner(git.get_root()) ret = []
for _, hook in self.hooks:
@cached_property language = hook['language']
def manifest(self): version = hook['language_version']
raise NotImplementedError deps = hook.get('additional_dependencies', [])
ret.append((
self._cmd_runner_from_deps(language, deps),
language, version, deps,
))
return tuple(ret)
class _UniqueList(list): class _UniqueList(list):

View file

@ -0,0 +1 @@
*

View file

@ -0,0 +1,3 @@
package main
func main() {}

View file

@ -0,0 +1,4 @@
{
"name": "pre_commit_dummy_package",
"version": "0.0.0"
}

View file

@ -0,0 +1,5 @@
Gem::Specification.new do |s|
s.name = 'pre_commit_dummy_package'
s.version = '0.0.0'
s.authors = ['Anthony Sottile']
end

View file

@ -0,0 +1,4 @@
from setuptools import setup
setup(name='pre-commit-dummy-package', version='0.0.0')

View file

@ -12,8 +12,10 @@ from cached_property import cached_property
from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.prefixed_command_runner import PrefixedCommandRunner
from pre_commit.util import clean_path_on_failure from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output from pre_commit.util import cmd_output
from pre_commit.util import copy_tree_to_path
from pre_commit.util import cwd from pre_commit.util import cwd
from pre_commit.util import no_git_env from pre_commit.util import no_git_env
from pre_commit.util import resource_filename
logger = logging.getLogger('pre_commit') logger = logging.getLogger('pre_commit')
@ -35,16 +37,6 @@ def _get_default_directory():
class Store(object): class Store(object):
get_default_directory = staticmethod(_get_default_directory) get_default_directory = staticmethod(_get_default_directory)
class RepoPathGetter(object):
def __init__(self, repo, ref, store):
self._repo = repo
self._ref = ref
self._store = store
@cached_property
def repo_path(self):
return self._store.clone(self._repo, self._ref)
def __init__(self, directory=None): def __init__(self, directory=None):
if directory is None: if directory is None:
directory = self.get_default_directory() directory = self.get_default_directory()
@ -91,45 +83,55 @@ class Store(object):
def require_created(self): def require_created(self):
"""Require the pre-commit file store to be created.""" """Require the pre-commit file store to be created."""
if self.__created: if not self.__created:
return self._create()
self.__created = True
self._create() def _new_repo(self, repo, ref, make_strategy):
self.__created = True
def clone(self, url, ref):
"""Clone the given url and checkout the specific ref."""
self.require_created() self.require_created()
# Check if we already exist # Check if we already exist
with sqlite3.connect(self.db_path) as db: with sqlite3.connect(self.db_path) as db:
result = db.execute( result = db.execute(
'SELECT path FROM repos WHERE repo = ? AND ref = ?', 'SELECT path FROM repos WHERE repo = ? AND ref = ?',
[url, ref], [repo, ref],
).fetchone() ).fetchone()
if result: if result:
return result[0] return result[0]
logger.info('Initializing environment for {}.'.format(url)) logger.info('Initializing environment for {}.'.format(repo))
dir = tempfile.mkdtemp(prefix='repo', dir=self.directory) directory = tempfile.mkdtemp(prefix='repo', dir=self.directory)
with clean_path_on_failure(dir): with clean_path_on_failure(directory):
cmd_output( make_strategy(directory)
'git', 'clone', '--no-checkout', url, dir, env=no_git_env(),
)
with cwd(dir):
cmd_output('git', 'reset', ref, '--hard', env=no_git_env())
# Update our db with the created repo # Update our db with the created repo
with sqlite3.connect(self.db_path) as db: with sqlite3.connect(self.db_path) as db:
db.execute( db.execute(
'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)',
[url, ref, dir], [repo, ref, directory],
) )
return dir return directory
def get_repo_path_getter(self, repo, ref): def clone(self, repo, ref):
return self.RepoPathGetter(repo, ref, self) """Clone the given url and checkout the specific ref."""
def clone_strategy(directory):
cmd_output(
'git', 'clone', '--no-checkout', repo, directory,
env=no_git_env(),
)
with cwd(directory):
cmd_output('git', 'reset', ref, '--hard', env=no_git_env())
return self._new_repo(repo, ref, clone_strategy)
def make_local(self, deps):
def make_local_strategy(directory):
copy_tree_to_path(resource_filename('empty_template'), directory)
return self._new_repo(
'local:{}'.format(','.join(sorted(deps))), 'N/A',
make_local_strategy,
)
@cached_property @cached_property
def cmd_runner(self): def cmd_runner(self):

View file

@ -204,3 +204,21 @@ def rmtree(path):
else: else:
raise raise
shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly) shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly)
def copy_tree_to_path(src_dir, dest_dir):
"""Copies all of the things inside src_dir to an already existing dest_dir.
This looks eerily similar to shutil.copytree, but copytree has no option
for not creating dest_dir.
"""
names = os.listdir(src_dir)
for name in names:
srcname = os.path.join(src_dir, name)
destname = os.path.join(dest_dir, name)
if os.path.isdir(srcname):
shutil.copytree(srcname, destname)
else:
shutil.copy(srcname, destname)

View file

@ -34,7 +34,9 @@ setup(
'resources/rbenv.tar.gz', 'resources/rbenv.tar.gz',
'resources/ruby-build.tar.gz', 'resources/ruby-build.tar.gz',
'resources/ruby-download.tar.gz', 'resources/ruby-download.tar.gz',
] 'resources/empty_template/*',
'resources/empty_template/.npmignore',
],
}, },
install_requires=[ install_requires=[
'aspy.yaml', 'aspy.yaml',

View file

@ -15,8 +15,8 @@ from pre_commit.clientlib.validate_config import validate_config_extra
from pre_commit.clientlib.validate_manifest import load_manifest from pre_commit.clientlib.validate_manifest import load_manifest
from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.jsonschema_extensions import apply_defaults
from pre_commit.util import cmd_output from pre_commit.util import cmd_output
from pre_commit.util import copy_tree_to_path
from pre_commit.util import cwd from pre_commit.util import cwd
from testing.util import copy_tree_to_path
from testing.util import get_head_sha from testing.util import get_head_sha
from testing.util import get_resource_path from testing.util import get_resource_path

View file

@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os.path import os.path
import shutil
import jsonschema import jsonschema
import pytest import pytest
@ -20,24 +19,6 @@ def get_resource_path(path):
return os.path.join(TESTING_DIR, 'resources', path) return os.path.join(TESTING_DIR, 'resources', path)
def copy_tree_to_path(src_dir, dest_dir):
"""Copies all of the things inside src_dir to an already existing dest_dir.
This looks eerily similar to shutil.copytree, but copytree has no option
for not creating dest_dir.
"""
names = os.listdir(src_dir)
for name in names:
srcname = os.path.join(src_dir, name)
destname = os.path.join(dest_dir, name)
if os.path.isdir(srcname):
shutil.copytree(srcname, destname)
else:
shutil.copy(srcname, destname)
def get_head_sha(dir): def get_head_sha(dir):
with cwd(dir): with cwd(dir):
return cmd_output('git', 'rev-parse', 'HEAD')[1].strip() return cmd_output('git', 'rev-parse', 'HEAD')[1].strip()

View file

@ -12,8 +12,8 @@ from testing.util import get_head_sha
def manifest(store, tempdir_factory): def manifest(store, tempdir_factory):
path = make_repo(tempdir_factory, 'script_hooks_repo') path = make_repo(tempdir_factory, 'script_hooks_repo')
head_sha = get_head_sha(path) head_sha = get_head_sha(path)
repo_path_getter = store.get_repo_path_getter(path, head_sha) repo_path = store.clone(path, head_sha)
yield Manifest(repo_path_getter, path) yield Manifest(repo_path, path)
def test_manifest_contents(manifest): def test_manifest_contents(manifest):
@ -54,9 +54,8 @@ def test_hooks(manifest):
def test_legacy_manifest_warn(store, tempdir_factory, log_warning_mock): def test_legacy_manifest_warn(store, tempdir_factory, log_warning_mock):
path = make_repo(tempdir_factory, 'legacy_hooks_yaml_repo') path = make_repo(tempdir_factory, 'legacy_hooks_yaml_repo')
head_sha = get_head_sha(path) head_sha = get_head_sha(path)
repo_path_getter = store.get_repo_path_getter(path, head_sha) repo_path = store.clone(path, head_sha)
Manifest(repo_path, path).manifest_contents
Manifest(repo_path_getter, path).manifest_contents
# Should have printed a warning # Should have printed a warning
assert log_warning_mock.call_args_list[0][0][0] == ( assert log_warning_mock.call_args_list[0][0][0] == (

View file

@ -11,11 +11,10 @@ import mock
import pkg_resources import pkg_resources
import pytest import pytest
from pre_commit import constants as C
from pre_commit import five from pre_commit import five
from pre_commit import parse_shebang from pre_commit import parse_shebang
from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_manifest import load_manifest
from pre_commit.clientlib.validate_config import validate_config_extra
from pre_commit.jsonschema_extensions import apply_defaults
from pre_commit.languages import golang from pre_commit.languages import golang
from pre_commit.languages import helpers from pre_commit.languages import helpers
from pre_commit.languages import node from pre_commit.languages import node
@ -30,6 +29,7 @@ from testing.fixtures import git_dir
from testing.fixtures import make_config_from_repo from testing.fixtures import make_config_from_repo
from testing.fixtures import make_repo from testing.fixtures import make_repo
from testing.fixtures import modify_manifest from testing.fixtures import modify_manifest
from testing.util import get_resource_path
from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_docker
from testing.util import skipif_cant_run_swift from testing.util import skipif_cant_run_swift
from testing.util import skipif_slowtests_false from testing.util import skipif_slowtests_false
@ -51,9 +51,9 @@ def _test_hook_repo(
path = make_repo(tempdir_factory, repo_path) path = make_repo(tempdir_factory, repo_path)
config = make_config_from_repo(path, **(config_kwargs or {})) config = make_config_from_repo(path, **(config_kwargs or {}))
repo = Repository.create(config, store) repo = Repository.create(config, store)
hook_dict = [ hook_dict, = [
hook for repo_hook_id, hook in repo.hooks if repo_hook_id == hook_id hook for repo_hook_id, hook in repo.hooks if repo_hook_id == hook_id
][0] ]
ret = repo.run_hook(hook_dict, args) ret = repo.run_hook(hook_dict, args)
assert ret[0] == expected_return_code assert ret[0] == expected_return_code
assert ret[1].replace(b'\r\n', b'\n') == expected assert ret[1].replace(b'\r\n', b'\n') == expected
@ -438,32 +438,13 @@ def test_lots_of_files(tempdir_factory, store):
) )
@pytest.fixture
def mock_repo_config():
config = {
'repo': 'git@github.com:pre-commit/pre-commit-hooks',
'sha': '5e713f8878b7d100c0e059f8cc34be4fc2e8f897',
'hooks': [{
'id': 'pyflakes',
'files': '\\.py$',
}],
}
config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA)
validate_config_extra(config_wrapped)
return config_wrapped[0]
def test_repo_url(mock_repo_config):
repo = Repository(mock_repo_config, None)
assert repo.repo_url == 'git@github.com:pre-commit/pre-commit-hooks'
@pytest.mark.integration @pytest.mark.integration
def test_languages(tempdir_factory, store): def test_venvs(tempdir_factory, store):
path = make_repo(tempdir_factory, 'python_hooks_repo') path = make_repo(tempdir_factory, 'python_hooks_repo')
config = make_config_from_repo(path) config = make_config_from_repo(path)
repo = Repository.create(config, store) repo = Repository.create(config, store)
assert repo.languages == {('python', 'default')} venv, = repo._venvs
assert venv == (mock.ANY, 'python', 'default', [])
@pytest.mark.integration @pytest.mark.integration
@ -472,7 +453,8 @@ def test_additional_dependencies(tempdir_factory, store):
config = make_config_from_repo(path) config = make_config_from_repo(path)
config['hooks'][0]['additional_dependencies'] = ['pep8'] config['hooks'][0]['additional_dependencies'] = ['pep8']
repo = Repository.create(config, store) repo = Repository.create(config, store)
assert repo.additional_dependencies['python']['default'] == ['pep8'] venv, = repo._venvs
assert venv == (mock.ANY, 'python', 'default', ['pep8'])
@pytest.mark.integration @pytest.mark.integration
@ -481,11 +463,11 @@ def test_additional_dependencies_duplicated(
): ):
path = make_repo(tempdir_factory, 'ruby_hooks_repo') path = make_repo(tempdir_factory, 'ruby_hooks_repo')
config = make_config_from_repo(path) config = make_config_from_repo(path)
config['hooks'][0]['additional_dependencies'] = [ deps = ['thread_safe', 'tins', 'thread_safe']
'thread_safe', 'tins', 'thread_safe'] config['hooks'][0]['additional_dependencies'] = deps
repo = Repository.create(config, store) repo = Repository.create(config, store)
assert repo.additional_dependencies['ruby']['default'] == [ venv, = repo._venvs
'thread_safe', 'tins'] assert venv == (mock.ANY, 'ruby', 'default', ['thread_safe', 'tins'])
@pytest.mark.integration @pytest.mark.integration
@ -495,7 +477,7 @@ def test_additional_python_dependencies_installed(tempdir_factory, store):
config['hooks'][0]['additional_dependencies'] = ['mccabe'] config['hooks'][0]['additional_dependencies'] = ['mccabe']
repo = Repository.create(config, store) repo = Repository.create(config, store)
repo.require_installed() repo.require_installed()
with python.in_env(repo.cmd_runner, 'default'): with python.in_env(repo._cmd_runner, 'default'):
output = cmd_output('pip', 'freeze', '-l')[1] output = cmd_output('pip', 'freeze', '-l')[1]
assert 'mccabe' in output assert 'mccabe' in output
@ -512,7 +494,7 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store):
repo = Repository.create(config, store) repo = Repository.create(config, store)
repo.require_installed() repo.require_installed()
# We should see our additional dependency installed # We should see our additional dependency installed
with python.in_env(repo.cmd_runner, 'default'): with python.in_env(repo._cmd_runner, 'default'):
output = cmd_output('pip', 'freeze', '-l')[1] output = cmd_output('pip', 'freeze', '-l')[1]
assert 'mccabe' in output assert 'mccabe' in output
@ -528,7 +510,7 @@ def test_additional_ruby_dependencies_installed(
config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins'] config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins']
repo = Repository.create(config, store) repo = Repository.create(config, store)
repo.require_installed() repo.require_installed()
with ruby.in_env(repo.cmd_runner, 'default'): with ruby.in_env(repo._cmd_runner, 'default'):
output = cmd_output('gem', 'list', '--local')[1] output = cmd_output('gem', 'list', '--local')[1]
assert 'thread_safe' in output assert 'thread_safe' in output
assert 'tins' in output assert 'tins' in output
@ -546,7 +528,7 @@ def test_additional_node_dependencies_installed(
config['hooks'][0]['additional_dependencies'] = ['lodash'] config['hooks'][0]['additional_dependencies'] = ['lodash']
repo = Repository.create(config, store) repo = Repository.create(config, store)
repo.require_installed() repo.require_installed()
with node.in_env(repo.cmd_runner, 'default'): with node.in_env(repo._cmd_runner, 'default'):
cmd_output('npm', 'config', 'set', 'global', 'true') cmd_output('npm', 'config', 'set', 'global', 'true')
output = cmd_output('npm', 'ls')[1] output = cmd_output('npm', 'ls')[1]
assert 'lodash' in output assert 'lodash' in output
@ -563,7 +545,7 @@ def test_additional_golang_dependencies_installed(
config['hooks'][0]['additional_dependencies'] = deps config['hooks'][0]['additional_dependencies'] = deps
repo = Repository.create(config, store) repo = Repository.create(config, store)
repo.require_installed() repo.require_installed()
binaries = os.listdir(repo.cmd_runner.path( binaries = os.listdir(repo._cmd_runner.path(
helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin', helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin',
)) ))
# normalize for windows # normalize for windows
@ -611,7 +593,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store):
repo.run_hook(hook, []) repo.run_hook(hook, [])
# Should have made an environment, however this environment is broken! # Should have made an environment, however this environment is broken!
assert os.path.exists(repo.cmd_runner.path('py_env-default')) assert os.path.exists(repo._cmd_runner.path('py_env-default'))
# However, it should be perfectly runnable (reinstall after botched # However, it should be perfectly runnable (reinstall after botched
# install) # install)
@ -684,6 +666,21 @@ def test_local_repository():
assert len(local_repo.hooks) == 1 assert len(local_repo.hooks) == 1
def test_local_python_repo(store):
# 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}
repo = Repository.create(config, store)
(_, hook), = repo.hooks
ret = repo.run_hook(hook, ('filename',))
assert ret[0] == 0
assert ret[1].replace(b'\r\n', b'\n') == b"['filename']\nHello World\n"
@pytest.yield_fixture @pytest.yield_fixture
def fake_log_handler(): def fake_log_handler():
handler = mock.Mock(level=logging.INFO) handler = mock.Mock(level=logging.INFO)
@ -699,7 +696,7 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler):
config['hooks'][0]['id'] = 'i-dont-exist' config['hooks'][0]['id'] = 'i-dont-exist'
repo = Repository.create(config, store) repo = Repository.create(config, store)
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
repo.install() repo.require_installed()
assert fake_log_handler.handle.call_args[0][0].msg == ( assert fake_log_handler.handle.call_args[0][0].msg == (
'`i-dont-exist` is not present in repository {}. ' '`i-dont-exist` is not present in repository {}. '
'Typo? Perhaps it is introduced in a newer version? ' 'Typo? Perhaps it is introduced in a newer version? '
@ -714,7 +711,7 @@ def test_too_new_version(tempdir_factory, store, fake_log_handler):
config = make_config_from_repo(path) config = make_config_from_repo(path)
repo = Repository.create(config, store) repo = Repository.create(config, store)
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
repo.install() repo.require_installed()
msg = fake_log_handler.handle.call_args[0][0].msg msg = fake_log_handler.handle.call_args[0][0].msg
assert re.match( assert re.match(
r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but '
@ -734,4 +731,4 @@ def test_versions_ok(tempdir_factory, store, version):
manifest[0]['minimum_pre_commit_version'] = version manifest[0]['minimum_pre_commit_version'] = version
config = make_config_from_repo(path) config = make_config_from_repo(path)
# Should succeed # Should succeed
Repository.create(config, store).install() Repository.create(config, store).require_installed()