Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pre_commit/clientlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def validate_manifest_main(argv=None):
'Config', None,

schema.RequiredRecurse('repos', schema.Array(CONFIG_REPO_DICT)),
schema.Optional('fail_fast', schema.check_bool, False),
)


Expand Down
5 changes: 4 additions & 1 deletion pre_commit/commands/autoupdate.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@

import pre_commit.constants as C
from pre_commit import output
from pre_commit.clientlib import CONFIG_SCHEMA
from pre_commit.clientlib import is_local_repo
from pre_commit.clientlib import load_config
from pre_commit.commands.migrate_config import migrate_config
from pre_commit.repository import Repository
from pre_commit.schema import remove_defaults
from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
from pre_commit.util import cwd
Expand Down Expand Up @@ -71,6 +73,7 @@ def _update_repo(repo_config, runner, tags_only):

def _write_new_config_file(path, output):
original_contents = open(path).read()
output = remove_defaults(output, CONFIG_SCHEMA)
new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS)

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

with open(path, 'w') as f:
Expand Down
6 changes: 4 additions & 2 deletions pre_commit/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,15 @@ def _compute_cols(hooks, verbose):
return max(cols, 80)


def _run_hooks(repo_hooks, args, environ):
def _run_hooks(config, repo_hooks, args, environ):
"""Actually run the hooks."""
skips = _get_skips(environ)
cols = _compute_cols([hook for _, hook in repo_hooks], args.verbose)
retval = 0
for repo, hook in repo_hooks:
retval |= _run_single_hook(hook, repo, args, skips, cols)
if retval and config['fail_fast']:
break
if (
retval and
args.show_diff_on_failure and
Expand Down Expand Up @@ -251,4 +253,4 @@ def run(runner, args, environ=os.environ):
if not hook['stages'] or args.hook_stage in hook['stages']
]

return _run_hooks(repo_hooks, args, environ)
return _run_hooks(runner.config, repo_hooks, args, environ)
6 changes: 5 additions & 1 deletion pre_commit/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@ def git_dir(self):
def config_file_path(self):
return os.path.join(self.git_root, self.config_file)

@cached_property
def config(self):
return load_config(self.config_file_path)

@cached_property
def repositories(self):
"""Returns a tuple of the configured repositories."""
repos = load_config(self.config_file_path)['repos']
repos = self.config['repos']
repos = tuple(Repository.create(x, self.store) for x in repos)
for repo in repos:
repo.require_installed()
Expand Down
27 changes: 27 additions & 0 deletions pre_commit/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ def _apply_default_optional(self, dct):
dct.setdefault(self.key, self.default)


def _remove_default_optional(self, dct):
if dct.get(self.key, MISSING) == self.default:
del dct[self.key]


def _require_key(self, dct):
if self.key not in dct:
raise ValidationError('Missing required key: {}'.format(self.key))
Expand All @@ -85,6 +90,10 @@ def _apply_default_required_recurse(self, dct):
dct[self.key] = apply_defaults(dct[self.key], self.schema)


def _remove_default_required_recurse(self, dct):
dct[self.key] = remove_defaults(dct[self.key], self.schema)


def _check_conditional(self, dct):
if dct.get(self.condition_key, MISSING) == self.condition_value:
_check_required(self, dct)
Expand All @@ -110,25 +119,30 @@ def _check_conditional(self, dct):
Required = collections.namedtuple('Required', ('key', 'check_fn'))
Required.check = _check_required
Required.apply_default = _dct_noop
Required.remove_default = _dct_noop
RequiredRecurse = collections.namedtuple('RequiredRecurse', ('key', 'schema'))
RequiredRecurse.check = _check_required
RequiredRecurse.check_fn = _check_fn_required_recurse
RequiredRecurse.apply_default = _apply_default_required_recurse
RequiredRecurse.remove_default = _remove_default_required_recurse
Optional = collections.namedtuple('Optional', ('key', 'check_fn', 'default'))
Optional.check = _check_optional
Optional.apply_default = _apply_default_optional
Optional.remove_default = _remove_default_optional
OptionalNoDefault = collections.namedtuple(
'OptionalNoDefault', ('key', 'check_fn'),
)
OptionalNoDefault.check = _check_optional
OptionalNoDefault.apply_default = _dct_noop
OptionalNoDefault.remove_default = _dct_noop
Conditional = collections.namedtuple(
'Conditional',
('key', 'check_fn', 'condition_key', 'condition_value', 'ensure_absent'),
)
Conditional.__new__.__defaults__ = (False,)
Conditional.check = _check_conditional
Conditional.apply_default = _dct_noop
Conditional.remove_default = _dct_noop


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

def remove_defaults(self, v):
ret = v.copy()
for item in self.items:
item.remove_default(ret)
return ret


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

def remove_defaults(self, v):
return [remove_defaults(val, self.of) for val in v]


class Not(object):
def __init__(self, val):
Expand Down Expand Up @@ -238,6 +261,10 @@ def apply_defaults(v, schema):
return schema.apply_defaults(v)


def remove_defaults(v, schema):
return schema.remove_defaults(v)


def load_from_filename(filename, schema, load_strategy, exc_tp):
with reraise_as(exc_tp):
if not os.path.exists(filename):
Expand Down
2 changes: 1 addition & 1 deletion tests/commands/autoupdate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ def test_autoupdate_local_hooks(tempdir_factory):
runner = Runner(path, C.CONFIG_FILE)
assert autoupdate(runner, tags_only=False) == 0
new_config_writen = load_config(runner.config_file_path)
assert len(new_config_writen) == 1
assert len(new_config_writen['repos']) == 1
assert new_config_writen['repos'][0] == config


Expand Down
15 changes: 15 additions & 0 deletions tests/commands/run_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,3 +729,18 @@ def test_pass_filenames(
)
assert expected_out + b'\nHello World' in printed
assert (b'foo.py' in printed) == pass_filenames


def test_fail_fast(
cap_out, repo_with_failing_hook, mock_out_store_directory,
):
with cwd(repo_with_failing_hook):
with modify_config() as config:
# More than one hook
config['fail_fast'] = True
config['repos'][0]['hooks'] *= 2
stage_a_file()

ret, printed = _do_run(cap_out, repo_with_failing_hook, _get_opts())
# it should have only run one hook
assert printed.count(b'Failing hook') == 1
38 changes: 38 additions & 0 deletions tests/schema_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from pre_commit.schema import Not
from pre_commit.schema import Optional
from pre_commit.schema import OptionalNoDefault
from pre_commit.schema import remove_defaults
from pre_commit.schema import Required
from pre_commit.schema import RequiredRecurse
from pre_commit.schema import validate
Expand Down Expand Up @@ -280,6 +281,37 @@ def test_apply_defaults_map_in_list():
assert ret == [{'key': False}]


def test_remove_defaults_copies_object():
val = {'key': False}
ret = remove_defaults(val, map_optional)
assert ret is not val


def test_remove_defaults_removes_defaults():
ret = remove_defaults({'key': False}, map_optional)
assert ret == {}


def test_remove_defaults_nothing_to_remove():
ret = remove_defaults({}, map_optional)
assert ret == {}


def test_remove_defaults_does_not_change_non_default():
ret = remove_defaults({'key': True}, map_optional)
assert ret == {'key': True}


def test_remove_defaults_map_in_list():
ret = remove_defaults([{'key': False}], Array(map_optional))
assert ret == [{}]


def test_remove_defaults_does_nothing_on_non_optional():
ret = remove_defaults({'key': True}, map_required)
assert ret == {'key': True}


nested_schema_required = Map(
'Repository', 'repo',
Required('repo', check_any),
Expand Down Expand Up @@ -310,6 +342,12 @@ def test_apply_defaults_nested():
assert ret == {'repo': 'repo1', 'hooks': [{'key': False}]}


def test_remove_defaults_nested():
val = {'repo': 'repo1', 'hooks': [{'key': False}]}
ret = remove_defaults(val, nested_schema_optional)
assert ret == {'repo': 'repo1', 'hooks': [{}]}


class Error(Exception):
pass

Expand Down