Skip to content

Commit 3cc5aa0

Browse files
authored
Merge pull request pre-commit#616 from pre-commit/fail_fast
Implement `fail_fast`.
2 parents 94dde26 + a821172 commit 3cc5aa0

File tree

8 files changed

+95
-5
lines changed

8 files changed

+95
-5
lines changed

pre_commit/clientlib.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def validate_manifest_main(argv=None):
130130
'Config', None,
131131

132132
schema.RequiredRecurse('repos', schema.Array(CONFIG_REPO_DICT)),
133+
schema.Optional('fail_fast', schema.check_bool, False),
133134
)
134135

135136

pre_commit/commands/autoupdate.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99

1010
import pre_commit.constants as C
1111
from pre_commit import output
12+
from pre_commit.clientlib import CONFIG_SCHEMA
1213
from pre_commit.clientlib import is_local_repo
1314
from pre_commit.clientlib import load_config
1415
from pre_commit.commands.migrate_config import migrate_config
1516
from pre_commit.repository import Repository
17+
from pre_commit.schema import remove_defaults
1618
from pre_commit.util import CalledProcessError
1719
from pre_commit.util import cmd_output
1820
from pre_commit.util import cwd
@@ -71,6 +73,7 @@ def _update_repo(repo_config, runner, tags_only):
7173

7274
def _write_new_config_file(path, output):
7375
original_contents = open(path).read()
76+
output = remove_defaults(output, CONFIG_SCHEMA)
7477
new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS)
7578

7679
lines = original_contents.splitlines(True)
@@ -95,7 +98,7 @@ def _write_new_config_file(path, output):
9598
# If we failed to intelligently rewrite the sha lines, fall back to the
9699
# pretty-formatted yaml output
97100
to_write = ''.join(lines)
98-
if ordered_load(to_write) != output:
101+
if remove_defaults(ordered_load(to_write), CONFIG_SCHEMA) != output:
99102
to_write = new_contents
100103

101104
with open(path, 'w') as f:

pre_commit/commands/run.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,15 @@ def _compute_cols(hooks, verbose):
169169
return max(cols, 80)
170170

171171

172-
def _run_hooks(repo_hooks, args, environ):
172+
def _run_hooks(config, repo_hooks, args, environ):
173173
"""Actually run the hooks."""
174174
skips = _get_skips(environ)
175175
cols = _compute_cols([hook for _, hook in repo_hooks], args.verbose)
176176
retval = 0
177177
for repo, hook in repo_hooks:
178178
retval |= _run_single_hook(hook, repo, args, skips, cols)
179+
if retval and config['fail_fast']:
180+
break
179181
if (
180182
retval and
181183
args.show_diff_on_failure and
@@ -251,4 +253,4 @@ def run(runner, args, environ=os.environ):
251253
if not hook['stages'] or args.hook_stage in hook['stages']
252254
]
253255

254-
return _run_hooks(repo_hooks, args, environ)
256+
return _run_hooks(runner.config, repo_hooks, args, environ)

pre_commit/runner.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,14 @@ def git_dir(self):
3737
def config_file_path(self):
3838
return os.path.join(self.git_root, self.config_file)
3939

40+
@cached_property
41+
def config(self):
42+
return load_config(self.config_file_path)
43+
4044
@cached_property
4145
def repositories(self):
4246
"""Returns a tuple of the configured repositories."""
43-
repos = load_config(self.config_file_path)['repos']
47+
repos = self.config['repos']
4448
repos = tuple(Repository.create(x, self.store) for x in repos)
4549
for repo in repos:
4650
repo.require_installed()

pre_commit/schema.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ def _apply_default_optional(self, dct):
6464
dct.setdefault(self.key, self.default)
6565

6666

67+
def _remove_default_optional(self, dct):
68+
if dct.get(self.key, MISSING) == self.default:
69+
del dct[self.key]
70+
71+
6772
def _require_key(self, dct):
6873
if self.key not in dct:
6974
raise ValidationError('Missing required key: {}'.format(self.key))
@@ -85,6 +90,10 @@ def _apply_default_required_recurse(self, dct):
8590
dct[self.key] = apply_defaults(dct[self.key], self.schema)
8691

8792

93+
def _remove_default_required_recurse(self, dct):
94+
dct[self.key] = remove_defaults(dct[self.key], self.schema)
95+
96+
8897
def _check_conditional(self, dct):
8998
if dct.get(self.condition_key, MISSING) == self.condition_value:
9099
_check_required(self, dct)
@@ -110,25 +119,30 @@ def _check_conditional(self, dct):
110119
Required = collections.namedtuple('Required', ('key', 'check_fn'))
111120
Required.check = _check_required
112121
Required.apply_default = _dct_noop
122+
Required.remove_default = _dct_noop
113123
RequiredRecurse = collections.namedtuple('RequiredRecurse', ('key', 'schema'))
114124
RequiredRecurse.check = _check_required
115125
RequiredRecurse.check_fn = _check_fn_required_recurse
116126
RequiredRecurse.apply_default = _apply_default_required_recurse
127+
RequiredRecurse.remove_default = _remove_default_required_recurse
117128
Optional = collections.namedtuple('Optional', ('key', 'check_fn', 'default'))
118129
Optional.check = _check_optional
119130
Optional.apply_default = _apply_default_optional
131+
Optional.remove_default = _remove_default_optional
120132
OptionalNoDefault = collections.namedtuple(
121133
'OptionalNoDefault', ('key', 'check_fn'),
122134
)
123135
OptionalNoDefault.check = _check_optional
124136
OptionalNoDefault.apply_default = _dct_noop
137+
OptionalNoDefault.remove_default = _dct_noop
125138
Conditional = collections.namedtuple(
126139
'Conditional',
127140
('key', 'check_fn', 'condition_key', 'condition_value', 'ensure_absent'),
128141
)
129142
Conditional.__new__.__defaults__ = (False,)
130143
Conditional.check = _check_conditional
131144
Conditional.apply_default = _dct_noop
145+
Conditional.remove_default = _dct_noop
132146

133147

134148
class Map(collections.namedtuple('Map', ('object_name', 'id_key', 'items'))):
@@ -158,6 +172,12 @@ def apply_defaults(self, v):
158172
item.apply_default(ret)
159173
return ret
160174

175+
def remove_defaults(self, v):
176+
ret = v.copy()
177+
for item in self.items:
178+
item.remove_default(ret)
179+
return ret
180+
161181

162182
class Array(collections.namedtuple('Array', ('of',))):
163183
__slots__ = ()
@@ -174,6 +194,9 @@ def check(self, v):
174194
def apply_defaults(self, v):
175195
return [apply_defaults(val, self.of) for val in v]
176196

197+
def remove_defaults(self, v):
198+
return [remove_defaults(val, self.of) for val in v]
199+
177200

178201
class Not(object):
179202
def __init__(self, val):
@@ -238,6 +261,10 @@ def apply_defaults(v, schema):
238261
return schema.apply_defaults(v)
239262

240263

264+
def remove_defaults(v, schema):
265+
return schema.remove_defaults(v)
266+
267+
241268
def load_from_filename(filename, schema, load_strategy, exc_tp):
242269
with reraise_as(exc_tp):
243270
if not os.path.exists(filename):

tests/commands/autoupdate_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def test_autoupdate_local_hooks(tempdir_factory):
275275
runner = Runner(path, C.CONFIG_FILE)
276276
assert autoupdate(runner, tags_only=False) == 0
277277
new_config_writen = load_config(runner.config_file_path)
278-
assert len(new_config_writen) == 1
278+
assert len(new_config_writen['repos']) == 1
279279
assert new_config_writen['repos'][0] == config
280280

281281

tests/commands/run_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,3 +729,18 @@ def test_pass_filenames(
729729
)
730730
assert expected_out + b'\nHello World' in printed
731731
assert (b'foo.py' in printed) == pass_filenames
732+
733+
734+
def test_fail_fast(
735+
cap_out, repo_with_failing_hook, mock_out_store_directory,
736+
):
737+
with cwd(repo_with_failing_hook):
738+
with modify_config() as config:
739+
# More than one hook
740+
config['fail_fast'] = True
741+
config['repos'][0]['hooks'] *= 2
742+
stage_a_file()
743+
744+
ret, printed = _do_run(cap_out, repo_with_failing_hook, _get_opts())
745+
# it should have only run one hook
746+
assert printed.count(b'Failing hook') == 1

tests/schema_test.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from pre_commit.schema import Not
2222
from pre_commit.schema import Optional
2323
from pre_commit.schema import OptionalNoDefault
24+
from pre_commit.schema import remove_defaults
2425
from pre_commit.schema import Required
2526
from pre_commit.schema import RequiredRecurse
2627
from pre_commit.schema import validate
@@ -280,6 +281,37 @@ def test_apply_defaults_map_in_list():
280281
assert ret == [{'key': False}]
281282

282283

284+
def test_remove_defaults_copies_object():
285+
val = {'key': False}
286+
ret = remove_defaults(val, map_optional)
287+
assert ret is not val
288+
289+
290+
def test_remove_defaults_removes_defaults():
291+
ret = remove_defaults({'key': False}, map_optional)
292+
assert ret == {}
293+
294+
295+
def test_remove_defaults_nothing_to_remove():
296+
ret = remove_defaults({}, map_optional)
297+
assert ret == {}
298+
299+
300+
def test_remove_defaults_does_not_change_non_default():
301+
ret = remove_defaults({'key': True}, map_optional)
302+
assert ret == {'key': True}
303+
304+
305+
def test_remove_defaults_map_in_list():
306+
ret = remove_defaults([{'key': False}], Array(map_optional))
307+
assert ret == [{}]
308+
309+
310+
def test_remove_defaults_does_nothing_on_non_optional():
311+
ret = remove_defaults({'key': True}, map_required)
312+
assert ret == {'key': True}
313+
314+
283315
nested_schema_required = Map(
284316
'Repository', 'repo',
285317
Required('repo', check_any),
@@ -310,6 +342,12 @@ def test_apply_defaults_nested():
310342
assert ret == {'repo': 'repo1', 'hooks': [{'key': False}]}
311343

312344

345+
def test_remove_defaults_nested():
346+
val = {'repo': 'repo1', 'hooks': [{'key': False}]}
347+
ret = remove_defaults(val, nested_schema_optional)
348+
assert ret == {'repo': 'repo1', 'hooks': [{}]}
349+
350+
313351
class Error(Exception):
314352
pass
315353

0 commit comments

Comments
 (0)