Skip to content

Commit b85a674

Browse files
committed
Make additional_dependencies rollforward safe
1 parent c1c3f3b commit b85a674

3 files changed

Lines changed: 67 additions & 14 deletions

File tree

pre_commit/repository.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from __future__ import unicode_literals
22

3+
import io
4+
import json
35
import logging
6+
import os
47
import shutil
58
from collections import defaultdict
69

710
import pkg_resources
811
from cached_property import cached_property
912

13+
from pre_commit import five
1014
from pre_commit import git
1115
from pre_commit.clientlib.validate_config import is_local_hooks
1216
from pre_commit.clientlib.validate_manifest import MANIFEST_JSON_SCHEMA
@@ -23,6 +27,9 @@
2327
pkg_resources.get_distribution('pre-commit').version
2428
)
2529

30+
# Bump when installation changes in a backwards / forwards incompatible way
31+
INSTALLED_STATE_VERSION = '1'
32+
2633

2734
class Repository(object):
2835
def __init__(self, repo_config, repo_path_getter):
@@ -110,14 +117,45 @@ def require_installed(self):
110117

111118
def install(self):
112119
"""Install the hook repository."""
120+
def state(language_name, language_version):
121+
return {
122+
'additional_dependencies': sorted(
123+
self.additional_dependencies[
124+
language_name
125+
][language_version],
126+
)
127+
}
128+
129+
def state_filename(venv, suffix=''):
130+
return self.cmd_runner.path(
131+
venv, '.install_state_v' + INSTALLED_STATE_VERSION + suffix,
132+
)
133+
134+
def read_state(venv):
135+
if not os.path.exists(state_filename(venv)):
136+
return None
137+
else:
138+
return json.loads(io.open(state_filename(venv)).read())
139+
140+
def write_state(venv, language_name, language_version):
141+
with io.open(
142+
state_filename(venv, suffix='staging'), 'w',
143+
) as state_file:
144+
state_file.write(five.to_text(json.dumps(
145+
state(language_name, language_version),
146+
)))
147+
# Move the file into place atomically to indicate we've installed
148+
os.rename(
149+
state_filename(venv, suffix='staging'),
150+
state_filename(venv),
151+
)
152+
113153
def language_is_installed(language_name, language_version):
114154
language = languages[language_name]
115-
directory = environment_dir(
116-
language.ENVIRONMENT_DIR, language_version,
117-
)
155+
venv = environment_dir(language.ENVIRONMENT_DIR, language_version)
118156
return (
119-
directory is None or
120-
self.cmd_runner.exists(directory, '.installed')
157+
venv is None or
158+
read_state(venv) == state(language_name, language_version)
121159
)
122160

123161
if not all(
@@ -131,24 +169,23 @@ def language_is_installed(language_name, language_version):
131169
logger.info('This may take a few minutes...')
132170

133171
for language_name, language_version in self.languages:
134-
language = languages[language_name]
135172
if language_is_installed(language_name, language_version):
136173
continue
137174

138-
directory = environment_dir(
139-
language.ENVIRONMENT_DIR, language_version,
140-
)
175+
language = languages[language_name]
176+
venv = environment_dir(language.ENVIRONMENT_DIR, language_version)
177+
141178
# There's potentially incomplete cleanup from previous runs
142179
# Clean it up!
143-
if self.cmd_runner.exists(directory):
144-
shutil.rmtree(self.cmd_runner.path(directory))
180+
if self.cmd_runner.exists(venv):
181+
shutil.rmtree(self.cmd_runner.path(venv))
145182

146183
language.install_environment(
147184
self.cmd_runner, language_version,
148185
self.additional_dependencies[language_name][language_version],
149186
)
150-
# Touch the .installed file (atomic) to indicate we've installed
151-
open(self.cmd_runner.path(directory, '.installed'), 'w').close()
187+
# Write our state to indicate we're installed
188+
write_state(venv, language_name, language_version)
152189

153190
def run_hook(self, hook, file_args):
154191
"""Run a hook.

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
'nodeenv>=0.11.1',
4747
'ordereddict',
4848
'pyyaml',
49-
'simplejson',
5049
'virtualenv',
5150
],
5251
entry_points={

tests/repository_test.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,23 @@ def test_additional_python_dependencies_installed(tempdir_factory, store):
360360
assert 'mccabe' in output
361361

362362

363+
@pytest.mark.integration
364+
def test_additional_dependencies_roll_forward(tempdir_factory, store):
365+
path = make_repo(tempdir_factory, 'python_hooks_repo')
366+
config = make_config_from_repo(path)
367+
# Run the repo once without additional_dependencies
368+
repo = Repository.create(config, store)
369+
repo.run_hook(repo.hooks[0][1], [])
370+
# Now run it with additional_dependencies
371+
config['hooks'][0]['additional_dependencies'] = ['mccabe']
372+
repo = Repository.create(config, store)
373+
repo.run_hook(repo.hooks[0][1], [])
374+
# We should see our additional dependency installed
375+
with python.in_env(repo.cmd_runner, 'default') as env:
376+
output = env.run('pip freeze -l')[1]
377+
assert 'mccabe' in output
378+
379+
363380
@xfailif_windows_no_ruby
364381
@pytest.mark.integration
365382
def test_additional_ruby_dependencies_installed(

0 commit comments

Comments
 (0)